Skip to main content
Glama
ChatHistory.tsx23.4 kB
import React, { useState, useEffect } from 'react'; import { MessageSquare, Search, Filter, Calendar, ThumbsUp, ThumbsDown, Clock, CheckCircle, XCircle, TrendingUp, Users, Eye, Archive, Download, Bot, Sparkles, Settings, Bell } from 'lucide-react'; import TenantLayout from '@/components/layout/TenantLayout'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { formatDate } from '@/lib/utils'; import { motion } from 'framer-motion'; import { useAuth } from '@/context/AuthContext'; import { useNavigate } from 'react-router-dom'; // Types interface ChatSession { id: string; session_token: string; started_at: string; ended_at?: string; resolved: boolean; rating?: number; feedback?: string; first_message?: string; message_count: number; } const StatsCard = ({ icon, title, value, description, gradient, trend }: { icon: React.ReactNode; title: string; value: string; description: string; gradient: string; trend?: string; }) => ( <motion.div whileHover={{ scale: 1.02, y: -5 }} transition={{ type: "spring", stiffness: 300 }} > <Card className="bg-white border-0 shadow-soft hover:shadow-large transition-all duration-300 overflow-hidden"> <div className={`absolute top-0 right-0 w-24 h-24 bg-gradient-to-bl ${gradient} opacity-10 rounded-full blur-xl`}></div> <CardContent className="p-6 relative"> <div className="flex items-center justify-between"> <div className="flex items-center space-x-4"> <div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${gradient} flex items-center justify-center text-white shadow-lg`}> {icon} </div> <div> <h3 className="text-2xl font-bold text-gray-900">{value}</h3> <p className="text-sm text-gray-600 font-medium">{title}</p> <p className="text-xs text-gray-500">{description}</p> </div> </div> {trend && ( <div className="text-right"> <div className="flex items-center text-green-600 text-sm"> <TrendingUp className="h-3 w-3 mr-1" /> {trend} </div> <p className="text-xs text-gray-500">vs last week</p> </div> )} </div> </CardContent> </Card> </motion.div> ); const ChatCard = ({ chat, index, onView, onDownload }: { chat: ChatSession; index: number; onView: (chatId: string) => void; onDownload: (chatId: string) => void; }) => { const getSentimentConfig = (rating?: number) => { if (!rating) { return { icon: <div className="w-4 h-4 rounded-full bg-gray-400"></div>, color: 'text-gray-600', bg: 'bg-gray-100', text: 'No Rating', border: 'border-gray-200' }; } if (rating >= 4) { return { icon: <ThumbsUp className="h-4 w-4" />, color: 'text-green-600', bg: 'bg-green-100', text: 'Positive', border: 'border-green-200' }; } if (rating <= 2) { return { icon: <ThumbsDown className="h-4 w-4" />, color: 'text-red-600', bg: 'bg-red-100', text: 'Negative', border: 'border-red-200' }; } return { icon: <div className="w-4 h-4 rounded-full bg-yellow-400"></div>, color: 'text-yellow-600', bg: 'bg-yellow-100', text: 'Neutral', border: 'border-yellow-200' }; }; const getSatisfactionStars = (rating?: number) => { return Array.from({ length: 5 }, (_, i) => ( <div key={i} className={`w-3 h-3 ${rating && i < rating ? 'text-yellow-400' : 'text-gray-300'}`} > ★ </div> )); }; const sentimentConfig = getSentimentConfig(chat.rating); const timeAgo = chat.started_at ? new Date(chat.started_at).toLocaleDateString() : 'Unknown'; return ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: index * 0.1, duration: 0.5 }} whileHover={{ y: -2 }} className="group" > <Card className="bg-white border-0 shadow-soft hover:shadow-large transition-all duration-300 overflow-hidden"> <CardContent className="p-6"> <div className="flex items-start justify-between mb-4"> <div className="flex items-start space-x-4"> <div className="w-12 h-12 rounded-full bg-gradient-to-br from-blue-500 to-purple-500 flex items-center justify-center text-white font-semibold shadow-lg"> <Bot className="h-6 w-6" /> </div> <div className="flex-1"> <h3 className="font-semibold text-gray-900 group-hover:text-primary-600 transition-colors"> Chat Session #{chat.id} </h3> <p className="text-sm text-gray-500">{chat.session_token.substring(0, 16)}...</p> <p className="text-sm text-gray-700 mt-2 line-clamp-2"> {chat.first_message || 'No message content available'} </p> </div> </div> <div className="flex items-center space-x-2"> <div className={`flex items-center space-x-1 px-3 py-1.5 rounded-full text-xs font-medium ${sentimentConfig.bg} ${sentimentConfig.color} border ${sentimentConfig.border}`}> {sentimentConfig.icon} <span>{sentimentConfig.text}</span> </div> </div> </div> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4"> <div className="flex items-center space-x-2 text-sm text-gray-600"> <Clock className="h-4 w-4" /> <span>{chat.ended_at ? 'Completed' : 'Active'}</span> </div> <div className="flex items-center space-x-2 text-sm text-gray-600"> <MessageSquare className="h-4 w-4" /> <span>{chat.message_count} messages</span> </div> <div className="flex items-center space-x-2 text-sm text-gray-600"> <Calendar className="h-4 w-4" /> <span>{timeAgo}</span> </div> <div className="flex items-center space-x-1"> {getSatisfactionStars(chat.rating)} </div> </div> <div className="flex items-center justify-between pt-4 border-t border-gray-100"> <div className="flex items-center space-x-3"> <span className={`text-xs px-3 py-1.5 rounded-full font-medium ${ chat.resolved ? 'bg-green-100 text-green-700' : 'bg-orange-100 text-orange-700' }`}> {chat.resolved ? ( <> <CheckCircle className="h-3 w-3 inline mr-1" /> Resolved </> ) : ( <> <XCircle className="h-3 w-3 inline mr-1" /> Pending </> )} </span> </div> <div className="flex items-center space-x-2"> <button onClick={() => onView(chat.id)} className="p-2 hover:bg-gray-100 rounded-lg transition-colors group/btn" title="View chat details" > <Eye className="h-4 w-4 text-gray-600 group-hover/btn:text-primary-600" /> </button> <button onClick={() => onDownload(chat.id)} className="p-2 hover:bg-gray-100 rounded-lg transition-colors group/btn" title="Download chat transcript" > <Download className="h-4 w-4 text-gray-600 group-hover/btn:text-primary-600" /> </button> </div> </div> </CardContent> </Card> </motion.div> ); }; const EmptyStateCard = () => ( <motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} className="col-span-full" > <Card className="bg-white border-2 border-dashed border-gray-200"> <CardContent className="flex flex-col items-center justify-center p-12 text-center"> <div className="w-16 h-16 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl flex items-center justify-center mb-6"> <MessageSquare className="h-8 w-8 text-white" /> </div> <h3 className="text-xl font-semibold text-gray-900 mb-3">No Chat History Yet</h3> <p className="text-gray-500 mb-8 max-w-md"> Once customers start chatting with your AI assistant, all conversation history will appear here. You'll be able to review interactions and improve your service. </p> <div className="flex items-center space-x-4"> <Button variant="outline" className="btn-secondary"> <Bot className="h-4 w-4 mr-2" /> Test Chat Widget </Button> <Button className="btn-modern"> <Sparkles className="h-4 w-4 mr-2" /> View Setup Guide </Button> </div> </CardContent> </Card> </motion.div> ); const ChatHistoryPage: React.FC = () => { const { user } = useAuth(); const navigate = useNavigate(); const [chatHistory, setChatHistory] = useState<ChatSession[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [searchTerm, setSearchTerm] = useState(''); const [sentimentFilter, setSentimentFilter] = useState<string | null>(null); const [statusFilter, setStatusFilter] = useState<string>('all'); const [showNotifications, setShowNotifications] = useState(false); const [downloadingChats, setDownloadingChats] = useState<Set<string>>(new Set()); // Mock notifications for chat history const notifications = [ { id: 1, title: 'New Chat Session', message: 'A customer started a new chat conversation', time: '5 min ago', type: 'info' }, { id: 2, title: 'Chat Resolved', message: 'Chat session #1234 was successfully resolved', time: '15 min ago', type: 'success' }, { id: 3, title: 'High Rating', message: 'Received 5-star rating for chat session #1235', time: '1 hour ago', type: 'success' } ]; useEffect(() => { const fetchChatHistory = async () => { const token = localStorage.getItem('auth-token'); if (!token) { setLoading(false); return; } try { setLoading(true); setError(null); const response = await fetch(`${import.meta.env.VITE_API_URL}/analytics/chat-history`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error('Failed to fetch chat history'); } const data = await response.json(); setChatHistory(data.conversations || []); } catch (err) { console.error('Error fetching chat history:', err); setError(err instanceof Error ? err.message : 'Failed to load chat history'); setChatHistory([]); } finally { setLoading(false); } }; fetchChatHistory(); }, [user]); const filteredChats = chatHistory.filter(chat => { const matchesSearch = chat.first_message?.toLowerCase().includes(searchTerm.toLowerCase()) || chat.session_token.toLowerCase().includes(searchTerm.toLowerCase()); const matchesSentiment = !sentimentFilter || (sentimentFilter === 'positive' && chat.rating && chat.rating >= 4) || (sentimentFilter === 'negative' && chat.rating && chat.rating <= 2) || (sentimentFilter === 'neutral' && chat.rating && chat.rating === 3); const matchesStatus = !statusFilter || (statusFilter === 'resolved' && chat.resolved) || (statusFilter === 'pending' && !chat.resolved); return matchesSearch && matchesSentiment && matchesStatus; }); const totalChats = chatHistory.length; const resolvedChats = chatHistory.filter(chat => chat.resolved).length; const averageRating = chatHistory.length > 0 ? chatHistory.filter(chat => chat.rating).reduce((sum, chat) => sum + (chat.rating || 0), 0) / chatHistory.filter(chat => chat.rating).length : 0; const handleViewChat = (chatId: string) => { // Navigate to chat details view navigate(`/chat-history/${chatId}`); }; const handleDownloadChat = async (chatId: string) => { setDownloadingChats(prev => new Set(prev).add(chatId)); try { // Simulate download process await new Promise(resolve => setTimeout(resolve, 1500)); // In a real app, this would trigger file download console.log(`Downloading chat transcript for session ${chatId}`); // Create mock download const element = document.createElement('a'); element.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(`Chat Session ${chatId} Transcript\n\nThis is a mock download of the chat transcript.`); element.download = `chat-session-${chatId}.txt`; document.body.appendChild(element); element.click(); document.body.removeChild(element); } catch (error) { console.error('Failed to download chat:', error); } finally { setDownloadingChats(prev => { const newSet = new Set(prev); newSet.delete(chatId); return newSet; }); } }; const handleSettings = () => { navigate('/settings'); }; 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-gradient-to-r from-white via-green-50 to-blue-50 border-b border-gray-100 shadow-soft -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"> Chat History </h1> {chatHistory.length > 0 && ( <div className="flex items-center bg-blue-100 text-blue-700 px-3 py-1.5 rounded-full shadow-sm"> <MessageSquare className="h-4 w-4 mr-2" /> <span className="text-sm font-medium">{totalChats} Conversations</span> </div> )} </div> <p className="text-lg text-gray-600 max-w-2xl"> Review all customer conversations and AI assistant interactions to improve your support quality. </p> <div className="flex items-center text-sm text-gray-500"> <span>Last updated: {new Date().toLocaleString()}</span> </div> </div> <div className="mt-6 lg:mt-0 flex items-center space-x-3"> <Button onClick={handleSettings} variant="outline" className="btn-secondary" > <Settings className="h-4 w-4 mr-2" /> Settings </Button> <div className="relative"> <Button onClick={() => setShowNotifications(!showNotifications)} variant="outline" className="btn-secondary relative" > <Bell className="h-4 w-4 mr-2" /> Notifications {notifications.length > 0 && ( <div className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center"> {notifications.length} </div> )} </Button> {showNotifications && ( <div className="absolute right-0 mt-2 w-80 bg-white border border-gray-200 rounded-xl shadow-large z-50"> <div className="p-4 border-b border-gray-100"> <h3 className="font-semibold text-gray-900">Chat History Notifications</h3> </div> <div className="max-h-64 overflow-y-auto"> {notifications.map((notification) => ( <div key={notification.id} className="p-4 border-b border-gray-50 hover:bg-gray-50"> <div className="flex items-start space-x-3"> <div className={`w-2 h-2 rounded-full mt-2 ${ notification.type === 'success' ? 'bg-green-500' : notification.type === 'warning' ? 'bg-yellow-500' : 'bg-blue-500' }`}></div> <div className="flex-1"> <p className="text-sm font-medium text-gray-900">{notification.title}</p> <p className="text-sm text-gray-600 mt-1">{notification.message}</p> <p className="text-xs text-gray-500 mt-2">{notification.time}</p> </div> </div> </div> ))} </div> </div> )} </div> </div> </div> {/* Click outside to close notifications */} {showNotifications && ( <div className="fixed inset-0 z-40" onClick={() => setShowNotifications(false)} /> )} </div> <div className="px-6"> {chatHistory.length === 0 ? ( /* Empty State */ <EmptyStateCard /> ) : ( <> {/* Stats Cards */} <div className="grid md:grid-cols-4 gap-6 mb-8"> <StatsCard icon={<MessageSquare className="h-6 w-6" />} title="Total Conversations" value={totalChats.toString()} description="All time chat sessions" gradient="from-blue-500 to-cyan-500" trend="+12%" /> <StatsCard icon={<CheckCircle className="h-6 w-6" />} title="Resolved Issues" value={resolvedChats.toString()} description="Successfully completed" gradient="from-green-500 to-emerald-500" trend="+8%" /> <StatsCard icon={<Users className="h-6 w-6" />} title="Customer Satisfaction" value={averageRating > 0 ? `${averageRating.toFixed(1)}/5` : 'N/A'} description="Average rating" gradient="from-purple-500 to-pink-500" trend="+0.3" /> <StatsCard icon={<Bot className="h-6 w-6" />} title="AI Performance" value={`${Math.round((resolvedChats / totalChats) * 100)}%`} description="Resolution rate" gradient="from-orange-500 to-red-500" trend="+5%" /> </div> {/* Search and Filter Bar */} <div className="flex flex-col lg:flex-row items-center justify-between space-y-4 lg:space-y-0 mb-8"> <div className="flex items-center space-x-4 w-full lg:w-auto"> <div className="relative flex-1 lg:flex-none lg:w-80"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" /> <input type="text" placeholder="Search conversations..." value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full pl-10 pr-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors" /> </div> <select value={sentimentFilter || ''} onChange={(e) => setSentimentFilter(e.target.value || null)} className="px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors" > <option value="">All Ratings</option> <option value="positive">Positive (4-5)</option> <option value="neutral">Neutral (3)</option> <option value="negative">Negative (1-2)</option> </select> <select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} className="px-4 py-3 border border-gray-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 transition-colors" > <option value="all">All Status</option> <option value="resolved">Resolved</option> <option value="pending">Pending</option> </select> </div> </div> {/* Chat History Grid */} <div className="grid gap-6"> {filteredChats.map((chat, index) => ( <ChatCard key={chat.id} chat={chat} index={index} onView={handleViewChat} onDownload={handleDownloadChat} /> ))} </div> {filteredChats.length === 0 && chatHistory.length > 0 && ( <div className="text-center py-12"> <Search className="h-12 w-12 text-gray-400 mx-auto mb-4" /> <h3 className="text-lg font-semibold text-gray-900 mb-2">No conversations found</h3> <p className="text-gray-500">Try adjusting your search terms or filters.</p> </div> )} </> )} </div> </div> </div> </TenantLayout> ); }; export default ChatHistoryPage;

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