import type { MetaFunction } from "@remix-run/node"; import { useState, useEffect, useTransition } from "react"; import { Activity, RefreshCw, Trash2, List, FileText, X, ChevronRight, ChevronDown, Inbox, Wrench, Bot, User, Settings, Zap, Users, Target, Cpu, MessageCircle, Brain, CheckCircle, ClipboardCheck, BarChart3, MessageSquare, Sparkles, Copy, Check, Lightbulb, Loader2, ArrowLeftRight } from "lucide-react"; import RequestDetailContent from "../components/RequestDetailContent"; import { ConversationThread } from "../components/ConversationThread"; export const meta: MetaFunction = () => { return [ { title: "Claude Code Monitor" }, { name: "description", content: "Claude Code Monitor - Real-time API request visualization" }, ]; }; interface Request { id: number; conversationId?: string; turnNumber?: number; isRoot?: boolean; timestamp: string; method: string; endpoint: string; headers: Record; originalModel?: string; routedModel?: string; body?: { model?: string; messages?: Array<{ role: string; content: any; }>; system?: Array<{ text: string; type: string; cache_control?: { type: string }; }>; tools?: Array<{ name: string; description: string; input_schema?: { type: string; properties?: Record; required?: string[]; }; }>; max_tokens?: number; temperature?: number; stream?: boolean; }; response?: { statusCode: number; headers: Record; body?: { usage?: { input_tokens?: number; output_tokens?: number; cache_creation_input_tokens?: number; cache_read_input_tokens?: number; service_tier?: string; }; [key: string]: any; }; bodyText?: string; responseTime: number; streamingChunks?: string[]; isStreaming: boolean; completedAt: string; }; promptGrade?: { score: number; criteria: Record; feedback: string; improvedPrompt: string; gradingTimestamp: string; }; } interface ConversationSummary { id: string; requestCount: number; startTime: string; lastActivity: string; duration: number; firstMessage: string; lastMessage: string; projectName: string; } interface Conversation { sessionId: string; projectPath: string; projectName: string; messages: Array<{ parentUuid: string | null; isSidechain: boolean; userType: string; cwd: string; sessionId: string; version: string; type: 'user' | 'assistant'; message: any; uuid: string; timestamp: string; }>; startTime: string; endTime: string; messageCount: number; } export default function Index() { const [requests, setRequests] = useState([]); const [conversations, setConversations] = useState([]); const [selectedRequest, setSelectedRequest] = useState(null); const [selectedConversation, setSelectedConversation] = useState(null); const [filter, setFilter] = useState("all"); const [viewMode, setViewMode] = useState<"requests" | "conversations">("requests"); const [isModalOpen, setIsModalOpen] = useState(false); const [isConversationModalOpen, setIsConversationModalOpen] = useState(false); const [modelFilter, setModelFilter] = useState("all"); const [isFetching, setIsFetching] = useState(false); const [isPending, startTransition] = useTransition(); const [requestsCurrentPage, setRequestsCurrentPage] = useState(1); const [hasMoreRequests, setHasMoreRequests] = useState(true); const [conversationsCurrentPage, setConversationsCurrentPage] = useState(1); const [hasMoreConversations, setHasMoreConversations] = useState(true); const itemsPerPage = 50; const loadRequests = async (filter?: string, loadMore = false) => { setIsFetching(true); const pageToFetch = loadMore ? requestsCurrentPage + 1 : 1; try { const currentModelFilter = filter || modelFilter; const url = new URL('/api/requests', window.location.origin); url.searchParams.append("page", pageToFetch.toString()); url.searchParams.append("limit", itemsPerPage.toString()); if (currentModelFilter !== "all") { url.searchParams.append("model", currentModelFilter); } const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); const requests = data.requests || []; const mappedRequests = requests.map((req: any, index: number) => ({ ...req, id: req.requestId ? `${req.requestId}_${index}` : `request_${index}` })); startTransition(() => { if (loadMore) { setRequests(prev => [...prev, ...mappedRequests]); } else { setRequests(mappedRequests); } setRequestsCurrentPage(pageToFetch); setHasMoreRequests(mappedRequests.length === itemsPerPage); }); } catch (error) { console.error('Failed to load requests:', error); startTransition(() => { setRequests([]); }); } finally { setIsFetching(false); } }; const loadConversations = async (modelFilter: string = "all", loadMore = false) => { setIsFetching(true); const pageToFetch = loadMore ? conversationsCurrentPage + 1 : 1; try { const url = new URL('/api/conversations', window.location.origin); url.searchParams.append("page", pageToFetch.toString()); url.searchParams.append("limit", itemsPerPage.toString()); if (modelFilter !== "all") { url.searchParams.append("model", modelFilter); } const response = await fetch(url.toString()); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); startTransition(() => { if (loadMore) { setConversations(prev => [...prev, ...data.conversations]); } else { setConversations(data.conversations); } setConversationsCurrentPage(pageToFetch); setHasMoreConversations(data.conversations.length === itemsPerPage); }); } catch (error) { console.error('Failed to load conversations:', error); startTransition(() => { setConversations([]); }); } finally { setIsFetching(false); } }; const loadConversationDetails = async (conversationId: string, projectName: string) => { try { const response = await fetch(`/api/conversations/${conversationId}?project=${encodeURIComponent(projectName)}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const conversation = await response.json(); setSelectedConversation(conversation); setIsConversationModalOpen(true); } catch (error) { console.error('Failed to load conversation details:', error); } }; const clearRequests = async () => { try { const response = await fetch('/api/requests', { method: 'DELETE' }); if (response.ok) { setRequests([]); setConversations([]); setRequestsCurrentPage(1); setHasMoreRequests(true); setConversationsCurrentPage(1); setHasMoreConversations(true); } } catch (error) { console.error('Failed to clear requests:', error); setRequests([]); } }; const filterRequests = (filter: string) => { if (filter === 'all') return requests; return requests.filter(req => { switch (filter) { case 'messages': return req.endpoint.includes('/messages'); case 'completions': return req.endpoint.includes('/completions'); case 'models': return req.endpoint.includes('/models'); default: return true; } }); }; const getMethodColor = (method: string) => { const colors = { 'GET': 'bg-green-50 text-green-700 border border-green-200', 'POST': 'bg-blue-50 text-blue-700 border border-blue-200', 'PUT': 'bg-yellow-50 text-yellow-700 border border-yellow-200', 'DELETE': 'bg-red-50 text-red-700 border border-red-200' }; return colors[method as keyof typeof colors] || 'bg-gray-50 text-gray-700 border border-gray-200'; }; const getRequestSummary = (request: Request) => { const parts = []; // Add token usage if available if (request.response?.body?.usage) { const usage = request.response.body.usage; const inputTokens = usage.input_tokens || 0; const outputTokens = usage.output_tokens || 0; const totalTokens = inputTokens + outputTokens; if (totalTokens > 0) { parts.push(`🪙 ${totalTokens.toLocaleString()} tokens`); if (usage.cache_read_input_tokens) { parts.push(`💾 ${usage.cache_read_input_tokens.toLocaleString()} cached`); } } } // Add response time if available if (request.response?.responseTime) { const seconds = (request.response.responseTime / 1000).toFixed(1); parts.push(`⏱️ ${seconds}s`); } // Add model if available (use routed model if different from original) const model = request.routedModel || request.body?.model; if (model) { const modelShort = model.includes('opus') ? 'Opus' : model.includes('sonnet') ? 'Sonnet' : model.includes('haiku') ? 'Haiku' : model.includes('gpt-4o') ? 'gpt-4o' : model.includes('o3') ? 'o3' : model.includes('o3-mini') ? 'o3-mini' : 'Model'; parts.push(`🤖 ${modelShort}`); // Show routing info if model was routed if (request.routedModel && request.originalModel && request.routedModel !== request.originalModel) { parts.push(`→ routed`); } } return parts.length > 0 ? parts.join(' • ') : '📡 API request'; }; const showRequestDetails = (requestId: number) => { const request = requests.find(r => r.id === requestId); if (request) { setSelectedRequest(request); setIsModalOpen(true); } }; const closeModal = () => { setIsModalOpen(false); setSelectedRequest(null); }; const getToolStats = () => { let toolDefinitions = 0; let toolCalls = 0; requests.forEach(req => { if (req.body) { // Count tool definitions in system prompts if (req.body.system) { req.body.system.forEach(sys => { if (sys.text && sys.text.includes('')) { const functionMatches = [...sys.text.matchAll(/([\s\S]*?)<\/function>/g)]; toolDefinitions += functionMatches.length; } }); } // Count actual tool calls in messages if (req.body.messages) { req.body.messages.forEach(msg => { if (msg.content && Array.isArray(msg.content)) { msg.content.forEach((contentPart: any) => { if (contentPart.type === 'tool_use') { toolCalls++; } if (contentPart.type === 'text' && contentPart.text && contentPart.text.includes('')) { const functionMatches = [...contentPart.text.matchAll(/([\s\S]*?)<\/function>/g)]; toolDefinitions += functionMatches.length; } }); } }); } } }); return `${toolCalls} calls / ${toolDefinitions} tools`; }; const getPromptGradeStats = () => { let totalGrades = 0; let gradeCount = 0; requests.forEach(req => { if (req.promptGrade && req.promptGrade.score) { totalGrades += req.promptGrade.score; gradeCount++; } }); if (gradeCount > 0) { const avgGrade = (totalGrades / gradeCount).toFixed(1); return `${avgGrade}/5`; } return '-/5'; }; const formatDuration = (milliseconds: number) => { if (milliseconds < 60000) { return `${Math.round(milliseconds / 1000)}s`; } else if (milliseconds < 3600000) { return `${Math.round(milliseconds / 60000)}m`; } else { return `${Math.round(milliseconds / 3600000)}h`; } }; const formatConversationSummary = (conversation: ConversationSummary) => { const duration = formatDuration(conversation.duration); return `${conversation.requestCount} requests • ${duration} duration`; }; const canGradeRequest = (request: Request) => { return request.body && request.body.messages && request.body.messages.some(msg => msg.role === 'user') && request.endpoint.includes('/messages'); }; const gradeRequest = async (requestId: number) => { const request = requests.find(r => r.id === requestId); if (!request || !canGradeRequest(request)) return; try { const response = await fetch('/api/grade-prompt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: request.body!.messages, systemMessages: request.body!.system || [], requestId: request.timestamp }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const promptGrade = await response.json(); // Update the request with the new grading const updatedRequests = requests.map(r => r.id === requestId ? { ...r, promptGrade } : r ); setRequests(updatedRequests); } catch (error) { console.error('Failed to grade prompt:', error); } }; const handleModelFilterChange = (newFilter: string) => { setModelFilter(newFilter); if (viewMode === 'requests') { loadRequests(newFilter); } else { loadConversations(newFilter); } }; useEffect(() => { if (viewMode === 'requests') { loadRequests(modelFilter); } else { loadConversations(modelFilter); } }, [viewMode, modelFilter]); // Handle escape key to close modals useEffect(() => { const handleEscapeKey = (event: KeyboardEvent) => { if (event.key === 'Escape') { if (isModalOpen) { closeModal(); } else if (isConversationModalOpen) { setIsConversationModalOpen(false); setSelectedConversation(null); } } }; window.addEventListener('keydown', handleEscapeKey); return () => { window.removeEventListener('keydown', handleEscapeKey); }; }, [isModalOpen, isConversationModalOpen]); const filteredRequests = filterRequests(filter); return (
{/* Header */}

Claude Code Monitor

{/* View mode toggle */}
{/* Filter buttons - only show for requests view */} {viewMode === "requests" && (
)} {/* Main Content */}
{/* Stats Grid */}

{viewMode === "requests" ? "Total Requests" : "Total Conversations"}

{viewMode === "requests" ? requests.length : conversations.length}

{/* Main Content */} {viewMode === "requests" ? ( /* Request History */

Request History

{(isFetching && requestsCurrentPage === 1) || isPending ? (

Loading requests...

) : filteredRequests.length === 0 ? (

No requests found

Make sure you have set ANTHROPIC_BASE_URL to point at the proxy

) : ( <> {filteredRequests.map(request => (
showRequestDetails(request.id)}>
{/* Model and Status */}

{request.routedModel || request.body?.model ? ( // Use routedModel if available, otherwise fall back to body.model (() => { const model = request.routedModel || request.body?.model || ''; if (model.includes('opus')) return Opus; if (model.includes('sonnet')) return Sonnet; if (model.includes('haiku')) return Haiku; if (model.includes('gpt-4o')) return GPT-4o; if (model.includes('gpt')) return GPT; return {model.split('-')[0]}; })() ) : API}

{request.routedModel && request.routedModel !== request.originalModel && ( routed )} {request.response?.statusCode && ( = 200 && request.response.statusCode < 300 ? 'bg-green-100 text-green-700' : request.response.statusCode >= 300 && request.response.statusCode < 400 ? 'bg-yellow-100 text-yellow-700' : 'bg-red-100 text-red-700' }`}> {request.response.statusCode} )} {request.conversationId && ( Turn {request.turnNumber} )}
{/* Endpoint */}
{request.routedModel && request.routedModel.startsWith('gpt-') ? '/v1/chat/completions' : request.endpoint}
{/* Metrics Row */}
{request.response?.body?.usage && ( <> {((request.response.body.usage.input_tokens || 0) + (request.response.body.usage.output_tokens || 0)).toLocaleString()} tokens {request.response.body.usage.cache_read_input_tokens && ( {request.response.body.usage.cache_read_input_tokens.toLocaleString()} cached )} )} {request.response?.responseTime && ( {(request.response.responseTime / 1000).toFixed(2)}s )}
{new Date(request.timestamp).toLocaleDateString()}
{new Date(request.timestamp).toLocaleTimeString()}
))} {hasMoreRequests && (
)} )}
) : ( /* Conversations View */

Conversations

{(isFetching && conversationsCurrentPage === 1) || isPending ? (

Loading conversations...

) : conversations.length === 0 ? (

No conversations found

Start a conversation to see it appear here

) : ( <> {conversations.map(conversation => (
loadConversationDetails(conversation.id, conversation.projectName)}>
#{conversation.id.slice(-8)} {conversation.requestCount} turns {formatDuration(conversation.duration)} {conversation.projectName && ( {conversation.projectName} )}
First Message
{conversation.firstMessage || "No content"}
{conversation.lastMessage && conversation.lastMessage !== conversation.firstMessage && (
Latest Message
{conversation.lastMessage}
)}
{new Date(conversation.startTime).toLocaleDateString()}
{new Date(conversation.startTime).toLocaleTimeString()}
))} {hasMoreConversations && (
)} )}
)}
{/* Request Detail Modal */} {isModalOpen && selectedRequest && (

Request Details

gradeRequest(selectedRequest.id)} />
)} {/* Conversation Detail Modal */} {isConversationModalOpen && selectedConversation && (

Conversation {selectedConversation.sessionId.slice(-8)}

{selectedConversation.messageCount} messages
{/* Conversation Overview */}
{selectedConversation.messageCount}
Messages
{new Date(selectedConversation.startTime).toLocaleDateString()}
Started
{new Date(selectedConversation.endTime).toLocaleDateString()}
Last Activity
{/* Conversation Thread */}
)}
); }