Skip to main content
Glama
Dashboard.tsx20.8 kB
import React, { useState, useEffect } from 'react'; import { BarChart3, Users, MessageSquare, FileText, TrendingUp, Activity, ArrowUp, ArrowDown, Sparkles, Clock, Target, Zap, Calendar, Globe, Settings, Bell, Filter, MoreHorizontal, TrendingDown, Eye, UserCheck, Bot } from 'lucide-react'; import TenantLayout from '@/components/layout/TenantLayout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { useAuth } from '@/context/AuthContext'; import { WelcomeDashboard } from '@/components/EmptyStates'; import { motion } from 'framer-motion'; // Type definitions for API responses interface DashboardMetrics { totalConversations: number; thisMonthConversations: number; averageRating: number; resolutionRate: number; knowledgeBaseDocuments: number; } interface Conversation { id: string; session_token: string; started_at: string; ended_at?: string; resolved: boolean; rating?: number; feedback?: string; first_message?: string; message_count: number; } const DashboardCard = ({ title, value, icon, change, changeType, gradient, description, trend }: { title: string; value: string; icon: React.ReactNode; change?: string; changeType?: 'positive' | 'negative' | 'neutral'; gradient: string; description?: string; trend?: number[]; }) => { return ( <motion.div whileHover={{ y: -5, scale: 1.02 }} transition={{ type: "spring", stiffness: 300 }} > <Card className="relative overflow-hidden bg-white border-0 shadow-soft hover:shadow-large transition-all duration-300"> {/* Background gradient overlay */} <div className={`absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl ${gradient} opacity-10 rounded-full blur-2xl`}></div> <CardHeader className="flex flex-row items-center justify-between pb-3 relative z-10"> <div className="space-y-1"> <CardTitle className="text-sm font-medium text-gray-600">{title}</CardTitle> {description && ( <p className="text-xs text-gray-500">{description}</p> )} </div> <div className={`w-14 h-14 rounded-2xl bg-gradient-to-br ${gradient} flex items-center justify-center text-white shadow-lg`}> {icon} </div> </CardHeader> <CardContent className="relative z-10"> <div className="flex items-end justify-between"> <div> <div className="text-3xl font-bold text-gray-900 mb-1">{value}</div> {change && ( <div className="flex items-center space-x-1"> {changeType === 'positive' && <ArrowUp className="h-4 w-4 text-green-500" />} {changeType === 'negative' && <ArrowDown className="h-4 w-4 text-red-500" />} <span className={`text-sm font-medium ${ changeType === 'positive' ? 'text-green-600' : changeType === 'negative' ? 'text-red-600' : 'text-gray-600' }`}> {change} </span> <span className="text-sm text-gray-500">vs last month</span> </div> )} </div> {trend && ( <div className="flex items-end space-x-1 h-8"> {trend.map((value, index) => ( <div key={index} className={`w-1.5 bg-gradient-to-t ${gradient} rounded-full opacity-60`} style={{ height: `${value}%` }} /> ))} </div> )} </div> </CardContent> </Card> </motion.div> ); }; const ActivityCard = ({ conversation, index }: { conversation: Conversation; index: number }) => { const getSentimentConfig = (rating?: number) => { if (!rating) return { color: 'bg-gray-500', bgColor: 'bg-gray-50', textColor: 'text-gray-700' }; if (rating >= 4) return { color: 'bg-green-500', bgColor: 'bg-green-50', textColor: 'text-green-700' }; if (rating <= 2) return { color: 'bg-red-500', bgColor: 'bg-red-50', textColor: 'text-red-700' }; return { color: 'bg-yellow-500', bgColor: 'bg-yellow-50', textColor: 'text-yellow-700' }; }; const sentimentConfig = getSentimentConfig(conversation.rating); const timeAgo = conversation.started_at ? new Date(conversation.started_at).toLocaleDateString() : 'Unknown'; return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.1 }} className="group" > <div className="flex items-start space-x-4 p-4 rounded-xl bg-gray-50/50 hover:bg-white hover:shadow-soft transition-all duration-300 border border-gray-100/50 group-hover:border-primary-200"> <div className={`w-3 h-3 rounded-full mt-3 ${sentimentConfig.color} shadow-sm`} /> <div className="flex-1 space-y-3"> <div className="flex items-center justify-between"> <div className="flex items-center space-x-3"> <p className="font-semibold text-gray-900">Session #{conversation.id}</p> <div className={`px-2 py-1 rounded-full text-xs font-medium ${sentimentConfig.bgColor} ${sentimentConfig.textColor}`}> {conversation.rating ? `★ ${conversation.rating}` : 'No rating'} </div> </div> <span className={`text-xs px-3 py-1 rounded-full font-medium transition-all ${ conversation.resolved ? 'bg-green-100 text-green-700 group-hover:bg-green-200' : 'bg-orange-100 text-orange-700 group-hover:bg-orange-200' }`}> {conversation.resolved ? '✓ Resolved' : '⏳ Pending'} </span> </div> <p className="text-sm text-gray-700 line-clamp-2 leading-relaxed"> {conversation.first_message || 'No message content'} </p> <div className="flex items-center justify-between text-xs text-gray-500"> <div className="flex items-center space-x-4"> <span className="flex items-center"> <Clock className="h-3 w-3 mr-1" /> {timeAgo} </span> <span className="flex items-center"> <MessageSquare className="h-3 w-3 mr-1" /> {conversation.message_count} messages </span> </div> <button className="opacity-0 group-hover:opacity-100 transition-opacity text-primary-600 hover:text-primary-700"> <Eye className="h-3 w-3" /> </button> </div> </div> </div> </motion.div> ); }; const EmptyStateCard = ({ title, description, icon }: { title: string; description: string; icon: React.ReactNode }) => ( <Card className="bg-white border-2 border-dashed border-gray-200"> <CardContent className="flex flex-col items-center justify-center p-8 text-center"> <div className="w-12 h-12 bg-gray-100 rounded-lg flex items-center justify-center mb-4"> {icon} </div> <h3 className="text-lg font-semibold text-gray-900 mb-2">{title}</h3> <p className="text-gray-500 text-sm">{description}</p> </CardContent> </Card> ); const Dashboard: React.FC = () => { const { user } = useAuth(); const [metrics, setMetrics] = useState<DashboardMetrics | null>(null); const [conversations, setConversations] = useState<Conversation[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); // Check if user is new (created less than 24 hours ago) const isNewUser = user?.createdAt ? (new Date().getTime() - new Date(user.createdAt).getTime()) < 24 * 60 * 60 * 1000 : true; const hasAnyData = metrics && ( metrics.totalConversations > 0 || metrics.knowledgeBaseDocuments > 0 || conversations.length > 0 ); useEffect(() => { const fetchDashboardData = async () => { const token = localStorage.getItem('auth-token'); if (!token) return; try { setLoading(true); setError(null); // Fetch metrics const metricsResponse = await fetch(`${import.meta.env.VITE_API_URL}/analytics/metrics`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (!metricsResponse.ok) { throw new Error('Failed to fetch metrics'); } const metricsData = await metricsResponse.json(); setMetrics(metricsData); // Fetch recent conversations const conversationsResponse = await fetch(`${import.meta.env.VITE_API_URL}/analytics/chat-history?limit=5`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (conversationsResponse.ok) { const conversationsData = await conversationsResponse.json(); setConversations(conversationsData.conversations || []); } } catch (err) { console.error('Error fetching dashboard data:', err); setError(err instanceof Error ? err.message : 'Failed to load dashboard data'); // Set empty data for new users setMetrics({ totalConversations: 0, thisMonthConversations: 0, averageRating: 0, resolutionRate: 0, knowledgeBaseDocuments: 0 }); setConversations([]); } finally { setLoading(false); } }; fetchDashboardData(); }, [user]); if (loading) { return ( <TenantLayout> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50"> <div className="flex items-center justify-center h-64"> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div> </div> </div> </TenantLayout> ); } return ( <TenantLayout> <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-blue-50"> <div className="space-y-8 pb-8"> {/* Modern Header */} <div className="bg-white border-b border-gray-100 -mx-6 px-6 py-6"> <div className="flex flex-col lg:flex-row justify-between items-start lg:items-center"> <div className="space-y-3"> <div className="flex items-center space-x-4"> <h1 className="text-4xl font-bold bg-gradient-to-r from-gray-900 via-primary-600 to-purple-600 bg-clip-text text-transparent"> {isNewUser || !hasAnyData ? 'Welcome!' : 'Dashboard'} </h1> {hasAnyData && ( <div className="flex items-center bg-green-100 text-green-700 px-3 py-1.5 rounded-full shadow-sm"> <div className="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></div> <span className="text-sm font-medium">All Systems Active</span> </div> )} </div> <p className="text-lg text-gray-600 max-w-2xl"> {isNewUser || !hasAnyData ? `Let's get your AI assistant set up, ${user?.name}! Your journey to automated customer support starts here.` : `Here's what's happening with your AI support system, ${user?.name}. Everything is running smoothly.` } </p> </div> <div className="mt-6 lg:mt-0 flex items-center space-x-4"> <div className="text-right"> <div className="flex items-center space-x-2 text-sm text-gray-500"> <Calendar className="h-4 w-4" /> <span>Last updated</span> </div> <p className="font-semibold text-gray-900">{new Date().toLocaleString()}</p> </div> <div className="flex space-x-2"> <button className="p-2 hover:bg-gray-100 rounded-lg transition-colors"> <Bell className="h-5 w-5 text-gray-600" /> </button> <button className="p-2 hover:bg-gray-100 rounded-lg transition-colors"> <Settings className="h-5 w-5 text-gray-600" /> </button> </div> </div> </div> </div> <div className="px-6"> {/* Show welcome dashboard for new users or users with no data */} {(isNewUser || !hasAnyData) ? ( <WelcomeDashboard userName={user?.name || 'User'} /> ) : ( /* Existing dashboard for users with data */ <> {/* Enhanced KPI Cards */} <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4 mb-8"> <DashboardCard title="Total Conversations" value={metrics?.totalConversations?.toString() || "0"} icon={<MessageSquare className="h-6 w-6" />} change={metrics?.thisMonthConversations ? `+${metrics.thisMonthConversations} this month` : undefined} changeType="positive" gradient="from-blue-500 to-cyan-500" description="All time conversations" /> <DashboardCard title="Avg. Response Time" value="< 1 min" icon={<Zap className="h-6 w-6" />} change="AI-powered" changeType="positive" gradient="from-green-500 to-emerald-500" description="Instant AI responses" /> <DashboardCard title="Resolution Rate" value={metrics?.resolutionRate ? `${Math.round(metrics.resolutionRate)}%` : "0%"} icon={<Target className="h-6 w-6" />} change={metrics?.averageRating ? `★ ${metrics.averageRating.toFixed(1)} avg rating` : undefined} changeType="positive" gradient="from-purple-500 to-pink-500" description="Customer satisfaction" /> <DashboardCard title="Knowledge Base" value={`${metrics?.knowledgeBaseDocuments || 0} docs`} icon={<FileText className="h-6 w-6" />} change={metrics?.knowledgeBaseDocuments ? "AI training active" : "Add documents to start"} changeType="neutral" gradient="from-orange-500 to-red-500" description="Training documents" /> </div> {/* Main Content Grid */} <div className="grid gap-8 lg:grid-cols-3"> {/* Recent Conversations */} <div className="lg:col-span-2"> <Card className="bg-white border-0 shadow-soft"> <CardHeader className="flex flex-row items-center justify-between border-b border-gray-100"> <div> <CardTitle className="text-xl font-semibold text-gray-900 flex items-center"> <Activity className="h-5 w-5 mr-2 text-primary-600" /> Recent Conversations </CardTitle> <p className="text-gray-500 mt-1">Latest customer interactions across all channels</p> </div> </CardHeader> <CardContent className="p-0"> {conversations.length > 0 ? ( <div className="space-y-1 p-6"> {conversations.map((conversation, index) => ( <ActivityCard key={conversation.id} conversation={conversation} index={index} /> ))} </div> ) : ( <div className="p-8"> <EmptyStateCard title="No conversations yet" description="Conversations will appear here once customers start chatting with your AI assistant." icon={<MessageSquare className="h-6 w-6 text-gray-400" />} /> </div> )} </CardContent> </Card> </div> {/* Quick Stats & System Status */} <div className="space-y-6"> {/* System Status */} <Card className="bg-white border-0 shadow-soft"> <CardHeader> <CardTitle className="text-lg font-semibold text-gray-900 flex items-center"> <Bot className="h-5 w-5 mr-2 text-primary-600" /> AI Assistant Status </CardTitle> </CardHeader> <CardContent className="space-y-4"> <div className="flex items-center justify-between p-3 bg-green-50 rounded-lg"> <div className="flex items-center space-x-3"> <div className="w-3 h-3 bg-green-500 rounded-full"></div> <span className="font-medium text-green-900">AI Assistant</span> </div> <span className="text-green-600 text-sm font-medium">Online</span> </div> <div className="flex items-center justify-between p-3 bg-green-50 rounded-lg"> <div className="flex items-center space-x-3"> <div className="w-3 h-3 bg-green-500 rounded-full"></div> <span className="font-medium text-green-900">Chat Widget</span> </div> <span className="text-green-600 text-sm font-medium">Active</span> </div> <div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg"> <div className="flex items-center space-x-3"> <div className="w-3 h-3 bg-blue-500 rounded-full"></div> <span className="font-medium text-blue-900">Knowledge Base</span> </div> <span className="text-blue-600 text-sm font-medium"> {metrics?.knowledgeBaseDocuments || 0} docs </span> </div> </CardContent> </Card> {/* Quick Actions */} <Card className="bg-white border-0 shadow-soft"> <CardHeader> <CardTitle className="text-lg font-semibold text-gray-900">Quick Actions</CardTitle> </CardHeader> <CardContent className="space-y-3"> <button className="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors flex items-center space-x-3"> <FileText className="h-4 w-4 text-primary-600" /> <span className="text-sm font-medium">Add Knowledge Base Document</span> </button> <button className="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors flex items-center space-x-3"> <Settings className="h-4 w-4 text-primary-600" /> <span className="text-sm font-medium">Configure Chat Widget</span> </button> <button className="w-full text-left p-3 rounded-lg hover:bg-gray-50 transition-colors flex items-center space-x-3"> <BarChart3 className="h-4 w-4 text-primary-600" /> <span className="text-sm font-medium">View Full Analytics</span> </button> </CardContent> </Card> </div> </div> </> )} </div> </div> </div> </TenantLayout> ); }; export default Dashboard;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ChiragPatankar/MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server