Skip to main content
Glama

Ultra MCP

usage.tsx14.8 kB
import { createFileRoute } from '@tanstack/react-router'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { trpc } from '@/lib/trpc'; import { useState } from 'react'; import { Calendar, Clock, DollarSign, Hash, AlertCircle, CheckCircle, ChevronRight, Search, Filter, Download, Trash2, Eye, X } from 'lucide-react'; export const Route = createFileRoute('/usage')({ component: Usage, }); function Usage() { const [selectedChat, setSelectedChat] = useState<string | null>(null); const [searchQuery, setSearchQuery] = useState(''); const [filterProvider, setFilterProvider] = useState<string>(''); const [filterStatus, setFilterStatus] = useState<'all' | 'success' | 'error'>('all'); const [page, setPage] = useState(0); const limit = 20; // Fetch chat list const { data: chatList, isLoading, refetch } = trpc.chats.list.useQuery({ searchQuery: searchQuery || undefined, provider: filterProvider || undefined, status: filterStatus, limit, offset: page * limit, sortBy: 'timestamp', sortOrder: 'desc', }); // Fetch chat detail when selected const { data: chatDetail } = trpc.chats.detail.useQuery( { id: selectedChat! }, { enabled: !!selectedChat } ); // Delete mutation const deleteMutation = trpc.chats.delete.useMutation({ onSuccess: () => { refetch(); setSelectedChat(null); }, }); const formatDate = (date: Date | string) => { return new Date(date).toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }); }; const formatCost = (cost: number | null) => { if (!cost) return '$0.00'; return `$${cost.toFixed(4)}`; }; const formatDuration = (ms: number | null) => { if (!ms) return '0ms'; if (ms < 1000) return `${ms}ms`; return `${(ms / 1000).toFixed(2)}s`; }; const getProviderColor = (provider: string) => { const colors: Record<string, string> = { openai: 'bg-green-500', gemini: 'bg-blue-500', azure: 'bg-purple-500', grok: 'bg-orange-500', 'openai-compatible': 'bg-gray-500', }; return colors[provider] || 'bg-gray-400'; }; return ( <div className="p-8"> <div className="mb-8"> <h2 className="text-3xl font-bold tracking-tight">Chat History</h2> <p className="text-muted-foreground"> Detailed history of all AI interactions </p> </div> {/* Filters and Search */} <Card className="mb-6"> <CardContent className="p-4"> <div className="flex flex-wrap gap-4"> <div className="flex-1 min-w-[200px]"> <div className="relative"> <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" /> <input type="text" placeholder="Search prompts, responses, models..." className="w-full pl-10 pr-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} /> </div> </div> <select className="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" value={filterProvider} onChange={(e) => setFilterProvider(e.target.value)} > <option value="">All Providers</option> <option value="openai">OpenAI</option> <option value="gemini">Gemini</option> <option value="azure">Azure</option> <option value="grok">Grok</option> <option value="openai-compatible">OpenAI Compatible</option> </select> <select className="px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500" value={filterStatus} onChange={(e) => setFilterStatus(e.target.value as any)} > <option value="all">All Status</option> <option value="success">Success</option> <option value="error">Error</option> </select> <button className="px-4 py-2 border rounded-lg hover:bg-gray-50 flex items-center gap-2"> <Download className="h-4 w-4" /> Export </button> </div> </CardContent> </Card> {/* Chat List */} <Card> <CardHeader> <CardTitle>Chat Sessions</CardTitle> </CardHeader> <CardContent> {isLoading ? ( <div className="text-center py-8 text-muted-foreground"> Loading chat history... </div> ) : chatList?.data.length === 0 ? ( <div className="text-center py-8 text-muted-foreground"> No chats found </div> ) : ( <> <div className="space-y-2"> {chatList?.data.map((chat) => ( <div key={chat.id} className="border rounded-lg p-4 hover:bg-gray-50 cursor-pointer transition-colors" onClick={() => setSelectedChat(chat.id)} > <div className="flex items-start justify-between"> <div className="flex-1"> <div className="flex items-center gap-3 mb-2"> <span className={`${getProviderColor(chat.provider)} text-white text-xs px-2 py-1 rounded`}> {chat.provider} </span> <span className="text-sm font-medium">{chat.model}</span> {chat.toolName && ( <span className="text-xs bg-gray-100 px-2 py-1 rounded"> {chat.toolName} </span> )} {chat.status === 'success' ? ( <CheckCircle className="h-4 w-4 text-green-500" /> ) : ( <AlertCircle className="h-4 w-4 text-red-500" /> )} </div> <p className="text-sm text-muted-foreground line-clamp-2 mb-2"> {chat.preview} </p> <div className="flex items-center gap-4 text-xs text-muted-foreground"> <span className="flex items-center gap-1"> <Calendar className="h-3 w-3" /> {formatDate(chat.timestamp)} </span> <span className="flex items-center gap-1"> <Hash className="h-3 w-3" /> {chat.totalTokens || 0} tokens </span> <span className="flex items-center gap-1"> <DollarSign className="h-3 w-3" /> {formatCost(chat.estimatedCost)} </span> <span className="flex items-center gap-1"> <Clock className="h-3 w-3" /> {formatDuration(chat.durationMs)} </span> </div> </div> <div className="flex items-center gap-2"> <button onClick={(e) => { e.stopPropagation(); setSelectedChat(chat.id); }} className="p-2 hover:bg-gray-100 rounded" > <Eye className="h-4 w-4" /> </button> <button onClick={(e) => { e.stopPropagation(); if (confirm('Delete this chat?')) { deleteMutation.mutate({ id: chat.id }); } }} className="p-2 hover:bg-red-50 text-red-500 rounded" > <Trash2 className="h-4 w-4" /> </button> <ChevronRight className="h-4 w-4 text-muted-foreground" /> </div> </div> </div> ))} </div> {/* Pagination */} {chatList?.pagination && ( <div className="flex items-center justify-between mt-6"> <div className="text-sm text-muted-foreground"> Showing {page * limit + 1} to {Math.min((page + 1) * limit, chatList.pagination.total)} of {chatList.pagination.total} chats </div> <div className="flex gap-2"> <button onClick={() => setPage(Math.max(0, page - 1))} disabled={page === 0} className="px-4 py-2 border rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" > Previous </button> <button onClick={() => setPage(page + 1)} disabled={!chatList.pagination.hasMore} className="px-4 py-2 border rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed" > Next </button> </div> </div> )} </> )} </CardContent> </Card> {/* Chat Detail Modal */} {selectedChat && chatDetail && ( <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"> <div className="bg-white rounded-lg max-w-4xl w-full max-h-[90vh] overflow-hidden"> <div className="flex items-center justify-between p-6 border-b"> <h3 className="text-xl font-semibold">Chat Details</h3> <button onClick={() => setSelectedChat(null)} className="p-2 hover:bg-gray-100 rounded" > <X className="h-5 w-5" /> </button> </div> <div className="p-6 overflow-y-auto max-h-[calc(90vh-80px)]"> {/* Metadata */} <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6"> <div> <div className="text-sm text-muted-foreground">Provider</div> <div className="font-medium">{chatDetail.provider}</div> </div> <div> <div className="text-sm text-muted-foreground">Model</div> <div className="font-medium">{chatDetail.model}</div> </div> <div> <div className="text-sm text-muted-foreground">Tool</div> <div className="font-medium">{chatDetail.toolName || 'N/A'}</div> </div> <div> <div className="text-sm text-muted-foreground">Status</div> <div className="font-medium"> {chatDetail.status === 'success' ? ( <span className="text-green-600">Success</span> ) : ( <span className="text-red-600">Error</span> )} </div> </div> <div> <div className="text-sm text-muted-foreground">Timestamp</div> <div className="font-medium">{new Date(chatDetail.timestamp).toLocaleString()}</div> </div> <div> <div className="text-sm text-muted-foreground">Duration</div> <div className="font-medium">{formatDuration(chatDetail.durationMs)}</div> </div> <div> <div className="text-sm text-muted-foreground">Total Tokens</div> <div className="font-medium"> {chatDetail.totalTokens || 0} {chatDetail.inputTokens && chatDetail.outputTokens && ( <span className="text-xs text-muted-foreground ml-1"> ({chatDetail.inputTokens} in / {chatDetail.outputTokens} out) </span> )} </div> </div> <div> <div className="text-sm text-muted-foreground">Cost</div> <div className="font-medium">{formatCost(chatDetail.estimatedCost)}</div> </div> </div> {/* Error Message */} {chatDetail.errorMessage && ( <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6"> <div className="font-medium text-red-800 mb-1">Error</div> <div className="text-sm text-red-700">{chatDetail.errorMessage}</div> </div> )} {/* Request Data */} {chatDetail.requestData && ( <div className="mb-6"> <h4 className="font-medium mb-2">Request</h4> <div className="bg-gray-50 rounded-lg p-4 overflow-x-auto"> <pre className="text-sm whitespace-pre-wrap"> {typeof chatDetail.requestData === 'string' ? chatDetail.requestData : JSON.stringify(chatDetail.requestData, null, 2)} </pre> </div> </div> )} {/* Response Data */} {chatDetail.responseData && ( <div> <h4 className="font-medium mb-2">Response</h4> <div className="bg-gray-50 rounded-lg p-4 overflow-x-auto"> <pre className="text-sm whitespace-pre-wrap"> {typeof chatDetail.responseData === 'string' ? chatDetail.responseData : JSON.stringify(chatDetail.responseData, null, 2)} </pre> </div> </div> )} </div> </div> </div> )} </div> ); }

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/RealMikeChong/ultra-mcp'

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