Skip to main content
Glama
Bichev
by Bichev
ChatInterface.tsx41.5 kB
import React, { useState, useRef, useEffect } from 'react'; import { Send, Bot, User, Loader2, RefreshCw, History, Mic, MicOff, Volume2, VolumeX } from 'lucide-react'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { ChatBubbleLeftRightIcon, CurrencyDollarIcon, SparklesIcon, CheckCircleIcon, ArrowTrendingUpIcon } from '@heroicons/react/24/outline'; import { aiService } from '../services/aiService'; import { chatSessionService, ChatMessage } from '../services/chatSessionService'; import ChatSessionHistory from '../components/ChatSessionHistory'; import { voiceService } from '../services/voiceService'; const API_BASE_URL = import.meta.env.VITE_API_URL || ''; interface PopularPairsResponse { data: string[]; } interface SpotPriceResponse { data: { amount: string; base: string; currency: string; }; } const ChatInterface: React.FC = () => { const [messages, setMessages] = useState<ChatMessage[]>([]); const [input, setInput] = useState(''); const [isLoading, setIsLoading] = useState(false); const [showHistory, setShowHistory] = useState(false); const [remainingRequests, setRemainingRequests] = useState<number | null>(null); const [isListening, setIsListening] = useState(false); const [isRecording, setIsRecording] = useState(false); const [speechSupported, setSpeechSupported] = useState(false); const [useOpenAI, setUseOpenAI] = useState(false); // Toggle between Web Speech API and OpenAI Whisper const [playingMessageId, setPlayingMessageId] = useState<string | null>(null); const messagesEndRef = useRef<HTMLDivElement>(null); const recognitionRef = useRef<any>(null); // Fetch popular pairs for dashboard widgets const { data: popularPairs } = useQuery({ queryKey: ['popular-pairs'], queryFn: async (): Promise<PopularPairsResponse> => { const response = await axios.get(`${API_BASE_URL}/api/v1/popular-pairs`); return response.data; }, }); // Fetch prices for top 3 popular pairs for compact display const topPairs = popularPairs?.data.slice(0, 3) || []; const priceQueries = useQuery({ queryKey: ['chat-prices', topPairs], queryFn: async () => { const pricePromises = topPairs.map(async (pair) => { try { const response = await axios.get<SpotPriceResponse>(`${API_BASE_URL}/api/v1/prices/${pair}`); return { pair, ...response.data.data }; } catch (error) { return { pair, amount: '0', base: pair.split('-')[0], currency: pair.split('-')[1], error: true }; } }); return Promise.all(pricePromises); }, enabled: topPairs.length > 0, }); const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }; // Initialize speech recognition and check OpenAI availability useEffect(() => { // Check if speech recognition is supported const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; if (SpeechRecognition) { setSpeechSupported(true); const recognition = new SpeechRecognition(); recognition.continuous = false; recognition.interimResults = true; recognition.lang = 'en-US'; recognition.onstart = () => { setIsListening(true); }; recognition.onresult = (event: any) => { const transcript = Array.from(event.results) .map((result: any) => result[0]) .map((result: any) => result.transcript) .join(''); setInput(transcript); }; recognition.onerror = (event: any) => { console.error('Speech recognition error:', event.error); setIsListening(false); if (event.error === 'not-allowed') { alert('Microphone access was denied. Please enable microphone permissions in your browser settings.'); } }; recognition.onend = () => { setIsListening(false); }; recognitionRef.current = recognition; } // Check if OpenAI voice service is available if (voiceService.isAvailable()) { setUseOpenAI(true); // Prefer OpenAI if available } }, []); // Load current session on component mount useEffect(() => { const currentSession = chatSessionService.getCurrentSession(); if (currentSession) { setMessages(currentSession.messages); } // Subscribe to session updates const unsubscribe = chatSessionService.subscribe(() => { const updatedSession = chatSessionService.getCurrentSession(); if (updatedSession) { setMessages([...updatedSession.messages]); } }); return unsubscribe; }, []); useEffect(() => { scrollToBottom(); }, [messages]); // These functions are now replaced by the AI service // Keeping them commented for reference or fallback /* const parseUserIntent = (message: string): ToolCall[] => { // Basic pattern matching - replaced by AI service }; const executeTool = async (toolCall: ToolCall): Promise<ToolCall> => { // Tool execution - now handled by AI service }; const formatResponse = (toolCalls: ToolCall[]): string => { // Response formatting - now handled by AI service }; */ const handleSend = async () => { if (!input.trim() || isLoading) return; const userMessage: ChatMessage = { id: Date.now().toString(), type: 'user', content: input, timestamp: new Date() }; // Add user message to persistent session chatSessionService.addMessage(userMessage); const userInput = input; setInput(''); setIsLoading(true); try { // Use AI service to process the message const aiResponse = await aiService.processMessage(userInput); // Update remaining requests setRemainingRequests(aiResponse.remainingRequests ?? null); const assistantMessage: ChatMessage = { id: (Date.now() + 1).toString(), type: 'assistant', content: aiResponse.message, timestamp: new Date(), toolCalls: aiResponse.toolCalls }; // Add assistant message to persistent session chatSessionService.addMessage(assistantMessage); } catch (error) { const errorMessage: ChatMessage = { id: (Date.now() + 1).toString(), type: 'assistant', content: `❌ Sorry, I encountered an error: ${error instanceof Error ? error.message : 'Unknown error'}`, timestamp: new Date() }; // Add error message to persistent session chatSessionService.addMessage(errorMessage); } finally { setIsLoading(false); } }; const resetConversation = () => { chatSessionService.clearCurrentSession(); }; const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const toggleVoiceInput = async () => { // Use OpenAI Whisper if available, otherwise fall back to Web Speech API if (useOpenAI && voiceService.isAvailable()) { await toggleOpenAIVoiceInput(); } else { toggleWebSpeechInput(); } }; const toggleWebSpeechInput = () => { if (!speechSupported) { alert('Speech recognition is not supported in your browser. Please use Chrome, Edge, or Safari.'); return; } if (isListening) { recognitionRef.current?.stop(); } else { try { recognitionRef.current?.start(); } catch (error) { console.error('Error starting speech recognition:', error); } } }; const toggleOpenAIVoiceInput = async () => { if (isRecording) { // Stop recording and transcribe try { setIsRecording(false); setIsListening(true); // Show processing state const audioBlob = await voiceService.stopRecording(); const transcript = await voiceService.transcribeAudio(audioBlob); setInput(transcript); setIsListening(false); } catch (error) { console.error('Error transcribing audio:', error); alert(error instanceof Error ? error.message : 'Failed to transcribe audio'); setIsListening(false); setIsRecording(false); } } else { // Start recording try { await voiceService.startRecording(); setIsRecording(true); setIsListening(true); } catch (error) { console.error('Error starting recording:', error); alert(error instanceof Error ? error.message : 'Failed to start recording'); setIsRecording(false); setIsListening(false); } } }; const speakMessage = async (messageId: string, text: string) => { if (!voiceService.isAvailable()) { alert('Text-to-speech requires OpenAI API key. Please add VITE_OPENAI_API_KEY to your .env file'); return; } // Stop if already playing this message if (playingMessageId === messageId) { voiceService.stopAudio(); setPlayingMessageId(null); return; } try { // Stop any currently playing audio voiceService.stopAudio(); setPlayingMessageId(messageId); const audioBlob = await voiceService.textToSpeech(text, { voice: 'alloy' }); await voiceService.playAudio(audioBlob); setPlayingMessageId(null); } catch (error) { console.error('Error playing message:', error); alert(error instanceof Error ? error.message : 'Failed to play audio'); setPlayingMessageId(null); } }; return ( <div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50"> {/* Header Section with Dashboard Widgets */} <div className="bg-white/80 backdrop-blur-sm border-b border-gray-200/50 sticky top-0 z-10"> <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6"> <div className="flex items-center space-x-4 mb-6"> <div className="p-3 bg-gradient-to-br from-cyan-500 to-blue-600 rounded-xl shadow-lg"> <ChatBubbleLeftRightIcon className="h-8 w-8 text-white" /> </div> <div> <h1 className="text-3xl font-bold bg-gradient-to-r from-gray-900 to-gray-600 bg-clip-text text-transparent"> Coinbase MCP Chat </h1> <p className="text-gray-600 mt-1">AI-powered cryptocurrency assistant with real-time data</p> </div> </div> {/* Dashboard Widgets Row */} <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4"> {/* Market Status */} <div className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg border border-gray-200/50 overflow-hidden"> <div className="bg-gradient-to-r from-green-500 to-emerald-600 p-4"> <div className="flex items-center space-x-2"> <div className="p-1.5 bg-white/20 rounded-lg"> <CheckCircleIcon className="h-4 w-4 text-white" /> </div> <div> <h3 className="text-sm font-semibold text-white">Market Status</h3> <p className="text-green-100 text-xs">Real-time feed</p> </div> </div> </div> <div className="p-3"> <p className="text-lg font-bold text-green-600">Active</p> <div className="flex items-center space-x-1 mt-1"> <div className="w-1.5 h-1.5 bg-green-400 rounded-full animate-pulse"></div> <span className="text-xs text-gray-500">Live updates</span> </div> </div> </div> {/* MCP Status */} <div className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg border border-gray-200/50 overflow-hidden"> <div className="bg-gradient-to-r from-purple-500 to-pink-600 p-4"> <div className="flex items-center space-x-2"> <div className="p-1.5 bg-white/20 rounded-lg"> <SparklesIcon className="h-4 w-4 text-white" /> </div> <div> <h3 className="text-sm font-semibold text-white">MCP Status</h3> <p className="text-purple-100 text-xs">8 tools ready</p> </div> </div> </div> <div className="p-3"> <p className="text-lg font-bold text-purple-600">Ready</p> <div className="flex items-center space-x-1 mt-1"> <div className="w-1.5 h-1.5 bg-purple-400 rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div> <span className="text-xs text-gray-500">Server running</span> </div> </div> </div> {/* Top Crypto Prices - Compact */} {priceQueries.data?.slice(0, 2).map((price, index) => ( <div key={price.pair} className="bg-white/80 backdrop-blur-sm rounded-xl shadow-lg border border-gray-200/50 overflow-hidden"> <div className={`p-4 bg-gradient-to-r ${ index === 0 ? 'from-orange-400 to-red-500' : 'from-blue-400 to-indigo-500' }`}> <div className="flex items-center space-x-2"> <div className="p-1.5 bg-white/20 rounded-lg"> <CurrencyDollarIcon className="h-4 w-4 text-white" /> </div> <div> <h3 className="text-sm font-semibold text-white">{price.base}</h3> <p className="text-white/80 text-xs">{price.pair}</p> </div> </div> </div> <div className="p-3"> {price.error ? ( <p className="text-sm text-red-500">Error</p> ) : ( <> <p className="text-lg font-bold text-gray-900"> ${parseFloat(price.amount).toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 0 })} </p> <div className="flex items-center space-x-1 mt-1"> <ArrowTrendingUpIcon className="w-3 h-3 text-green-500" /> <span className="text-xs text-gray-500">Live price</span> </div> </> )} </div> </div> ))} </div> {/* Chat Controls */} <div className="flex items-center justify-between"> <div className="flex items-center space-x-2"> <div className="w-2 h-2 bg-green-400 rounded-full animate-pulse"></div> <span className="text-sm text-gray-600">Ask me about crypto prices, buy virtual Bitcoin, or check your demo wallet 🍺₿</span> </div> <div className="flex items-center space-x-2"> {/* Rate Limit Indicator */} {remainingRequests !== null && ( <div className={`flex items-center space-x-2 px-3 py-2 text-sm rounded-lg border ${ remainingRequests > 1 ? 'text-green-700 bg-green-50 border-green-200' : remainingRequests === 1 ? 'text-yellow-700 bg-yellow-50 border-yellow-200' : 'text-red-700 bg-red-50 border-red-200' }`}> <div className={`w-2 h-2 rounded-full ${ remainingRequests > 1 ? 'bg-green-400' : remainingRequests === 1 ? 'bg-yellow-400' : 'bg-red-400' }`}></div> <span className="font-medium"> {remainingRequests} request{remainingRequests !== 1 ? 's' : ''} left </span> </div> )} <button onClick={() => setShowHistory(true)} className="flex items-center space-x-2 px-3 py-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-white/70 rounded-lg transition-all duration-200 border border-gray-200/50" title="Chat History" > <History className="w-4 h-4" /> <span>History</span> </button> <button onClick={resetConversation} className="flex items-center space-x-2 px-3 py-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-white/70 rounded-lg transition-all duration-200 border border-gray-200/50" title="Reset conversation" > <RefreshCw className="w-4 h-4" /> <span>Reset</span> </button> </div> </div> </div> </div> {/* Chat Container */} <div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-6 flex flex-col h-[calc(100vh-280px)]"> {/* Messages */} <div className="flex-1 overflow-y-auto space-y-4 mb-6"> {messages.length === 0 && ( <div className="text-center py-12"> <div className="w-16 h-16 bg-gradient-to-br from-cyan-100 to-blue-200 rounded-2xl flex items-center justify-center mx-auto mb-4"> <ChatBubbleLeftRightIcon className="h-8 w-8 text-cyan-600" /> </div> <h3 className="text-lg font-medium text-gray-900 mb-4">Hello! I'm your specialized Coinbase MCP assistant.</h3> <p className="text-gray-600 text-sm max-w-2xl mx-auto leading-relaxed mb-6"> I'm focused exclusively on cryptocurrency, blockchain, and MCP technology. I can help you with crypto prices, market analysis, and trading insights using real-time Coinbase data. </p> {/* Educational Notice */} <div className="bg-gradient-to-r from-amber-50 to-orange-50 border border-amber-200 rounded-lg p-4 mb-6 max-w-2xl mx-auto"> <div className="flex items-center space-x-2 mb-2"> <span className="text-lg">🎓</span> <span className="text-sm font-semibold text-amber-800">Educational Use</span> </div> <p className="text-xs text-amber-700 leading-relaxed"> This demo is rate-limited to 3 requests per minute for educational purposes. I only discuss cryptocurrency and MCP-related topics to keep our conversations focused and valuable. </p> </div> <div className="max-w-3xl mx-auto mb-8"> <div className="bg-white/70 backdrop-blur-sm rounded-xl p-6 border border-gray-200/50"> <h4 className="text-sm font-semibold text-gray-800 mb-4">Try asking me things like:</h4> <div className="grid grid-cols-1 md:grid-cols-2 gap-3 text-left"> <div className="space-y-2"> <div className="flex items-center space-x-2"> <div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div> <span className="text-sm text-gray-700">"What's the current Bitcoin price?"</span> </div> <div className="flex items-center space-x-2"> <div className="w-1.5 h-1.5 bg-orange-500 rounded-full"></div> <span className="text-sm text-gray-700">"🍺 Buy me a beer worth of Bitcoin"</span> </div> <div className="flex items-center space-x-2"> <div className="w-1.5 h-1.5 bg-green-500 rounded-full"></div> <span className="text-sm text-gray-700">"Show me popular trading pairs"</span> </div> <div className="flex items-center space-x-2"> <div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div> <span className="text-sm text-gray-700">"What's in my virtual wallet?"</span> </div> </div> <div className="space-y-2"> <div className="flex items-center space-x-2"> <div className="w-1.5 h-1.5 bg-cyan-500 rounded-full"></div> <span className="text-sm text-gray-700">"Analyze Bitcoin volatility over 30 days"</span> </div> <div className="flex items-center space-x-2"> <div className="w-1.5 h-1.5 bg-pink-500 rounded-full"></div> <span className="text-sm text-gray-700">"Buy $10 worth of Ethereum"</span> </div> <div className="flex items-center space-x-2"> <div className="w-1.5 h-1.5 bg-indigo-500 rounded-full"></div> <span className="text-sm text-gray-700">"Show my transaction history"</span> </div> <div className="flex items-center space-x-2"> <div className="w-1.5 h-1.5 bg-teal-500 rounded-full"></div> <span className="text-sm text-gray-700">"Get Ethereum market stats"</span> </div> </div> </div> </div> </div> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 max-w-2xl mx-auto mb-8"> <div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-xl p-4 border border-blue-200/50"> <div className="flex items-center space-x-2 mb-2"> <span className="text-lg">💡</span> <span className="text-sm font-semibold text-blue-900">AI Mode</span> </div> <p className="text-xs text-blue-700 leading-relaxed"> For advanced conversational AI, add your OpenAI API key to <code className="bg-blue-100 px-1 rounded text-xs">.env</code> </p> </div> <div className="bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl p-4 border border-green-200/50"> <div className="flex items-center space-x-2 mb-2"> <span className="text-lg">🔧</span> <span className="text-sm font-semibold text-green-900">Basic Mode</span> </div> <p className="text-xs text-green-700 leading-relaxed"> I can still help with crypto data using pattern matching and MCP tools! </p> </div> </div> <div className="flex flex-wrap justify-center gap-2"> <button onClick={() => setInput("What's the current Bitcoin price?")} className="px-4 py-2 bg-white/70 hover:bg-white border border-gray-200 rounded-lg text-sm text-gray-700 hover:text-gray-900 transition-all duration-200 shadow-sm hover:shadow-md" > Bitcoin price </button> <button onClick={() => setInput("Buy me a beer worth of Bitcoin")} className="px-4 py-2 bg-white/70 hover:bg-white border border-orange-200 rounded-lg text-sm text-orange-700 hover:text-orange-900 transition-all duration-200 shadow-sm hover:shadow-md" > 🍺 Beer → BTC </button> <button onClick={() => setInput("Show me popular trading pairs")} className="px-4 py-2 bg-white/70 hover:bg-white border border-gray-200 rounded-lg text-sm text-gray-700 hover:text-gray-900 transition-all duration-200 shadow-sm hover:shadow-md" > Popular pairs </button> <button onClick={() => setInput("Show my wallet balance")} className="px-4 py-2 bg-white/70 hover:bg-white border border-purple-200 rounded-lg text-sm text-purple-700 hover:text-purple-900 transition-all duration-200 shadow-sm hover:shadow-md" > 👛 My wallet </button> <button onClick={() => setInput("Get Ethereum market stats")} className="px-4 py-2 bg-white/70 hover:bg-white border border-gray-200 rounded-lg text-sm text-gray-700 hover:text-gray-900 transition-all duration-200 shadow-sm hover:shadow-md" > ETH stats </button> </div> </div> )} {messages.map((message) => ( <div key={message.id} className={`flex ${message.type === 'user' ? 'justify-end' : 'justify-start'}`} > <div className={`flex max-w-3xl ${message.type === 'user' ? 'flex-row-reverse' : 'flex-row'} space-x-3`}> <div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center shadow-lg ${ message.type === 'user' ? 'bg-gradient-to-r from-cyan-500 to-blue-600 text-white ml-3' : 'bg-gradient-to-r from-gray-100 to-gray-200 text-gray-600 mr-3' }`}> {message.type === 'user' ? <User className="w-5 h-5" /> : <Bot className="w-5 h-5" />} </div> <div className={`rounded-2xl p-4 shadow-lg backdrop-blur-sm ${ message.type === 'user' ? 'bg-gradient-to-r from-cyan-500 to-blue-600 text-white' : 'bg-white/80 text-gray-900 border border-gray-200/50' }`}> <div className="flex items-start justify-between space-x-3"> <div className="whitespace-pre-wrap text-sm leading-relaxed flex-1">{message.content}</div> {/* Text-to-Speech button for assistant messages */} {message.type === 'assistant' && voiceService.isAvailable() && ( <button onClick={() => speakMessage(message.id, message.content)} className={`flex-shrink-0 p-2 rounded-lg transition-all duration-200 ${ playingMessageId === message.id ? 'bg-green-100 text-green-600' : 'hover:bg-gray-100 text-gray-400 hover:text-gray-600' }`} title={playingMessageId === message.id ? 'Stop speaking' : 'Read aloud'} > {playingMessageId === message.id ? ( <VolumeX className="w-4 h-4" /> ) : ( <Volume2 className="w-4 h-4" /> )} </button> )} </div> {message.toolCalls && message.toolCalls.length > 0 && ( <div className="mt-3 pt-3 border-t border-white/20"> <div className="text-xs opacity-80 mb-2">🔧 Tool calls executed:</div> {message.toolCalls.map((toolCall, index) => { const isWalletTool = ['calculate_beer_cost', 'simulate_btc_purchase', 'get_virtual_wallet', 'get_transaction_history', 'buy_virtual_beer'].includes(toolCall.tool); const isTransaction = toolCall.tool === 'simulate_btc_purchase'; const isBeerPurchase = toolCall.tool === 'buy_virtual_beer'; return ( <div key={index} className="mb-2"> <div className={`text-xs rounded-lg p-2 backdrop-blur-sm ${ isWalletTool ? 'bg-purple-100/20 border border-purple-300/30' : 'bg-white/10' }`}> <div className="flex items-center justify-between"> <span className="font-mono flex items-center space-x-1"> {isWalletTool && <span>🍺₿</span>} <span>{toolCall.tool}</span> </span> {toolCall.error && <span className="text-red-300">❌ {toolCall.error}</span>} {toolCall.result && !isTransaction && <span className="text-green-300">✅ Success</span>} </div> {/* Show transaction details */} {isTransaction && toolCall.result?.data && ( <div className="mt-2 p-3 bg-green-900/30 rounded-lg border border-green-400/30"> <div className="text-xs font-bold text-green-300 mb-1"> ✅ Transaction Successful! </div> <div className="space-y-1 text-xs"> <div className="flex justify-between"> <span className="text-green-200">Type:</span> <span className="font-mono text-white">{toolCall.result.data.type.toUpperCase()}</span> </div> <div className="flex justify-between"> <span className="text-green-200">From:</span> <span className="font-mono text-white"> {toolCall.result.data.fromAmount.toFixed(2)} {toolCall.result.data.fromCurrency} </span> </div> <div className="flex justify-between"> <span className="text-green-200">To:</span> <span className="font-mono text-white"> {toolCall.result.data.toAmount.toFixed(8)} {toolCall.result.data.toCurrency} </span> </div> <div className="flex justify-between"> <span className="text-green-200">Price:</span> <span className="font-mono text-white"> ${toolCall.result.data.price.toLocaleString()} </span> </div> <div className="flex justify-between"> <span className="text-green-200">ID:</span> <span className="font-mono text-xs text-white opacity-70"> {toolCall.result.data.id} </span> </div> </div> </div> )} {/* Show beer calculation details */} {toolCall.tool === 'calculate_beer_cost' && toolCall.result?.data && ( <div className="mt-2 p-2 bg-orange-900/30 rounded-lg border border-orange-400/30 text-xs"> <div className="text-orange-200"> 🍺 ${toolCall.result.data.usdAmount} = {toolCall.result.data.cryptoAmount.toFixed(8)} {toolCall.result.data.cryptoCurrency} </div> </div> )} {/* Show wallet balance */} {toolCall.tool === 'get_virtual_wallet' && toolCall.result?.data?.wallet && ( <div className="mt-2 p-2 bg-purple-900/30 rounded-lg border border-purple-400/30 text-xs"> <div className="space-y-1"> {Object.entries(toolCall.result.data.wallet.balances) .filter(([_, balance]: [string, any]) => balance > 0) .map(([currency, balance]: [string, any]) => ( <div key={currency} className="flex justify-between text-purple-100"> <span>{currency === 'USD' ? '💵' : '🪙'} {currency}:</span> <span className="font-mono"> {currency === 'USD' ? `$${balance.toFixed(2)}` : balance.toFixed(8)} </span> </div> ))} {toolCall.result.data.wallet.inventory?.beers > 0 && ( <div className="flex justify-between text-purple-100 pt-2 border-t border-purple-400/30 mt-2"> <span>🍺 Beers:</span> <span className="font-mono">{toolCall.result.data.wallet.inventory.beers}</span> </div> )} </div> </div> )} {/* Show beer purchase */} {isBeerPurchase && toolCall.result && ( <div className={`mt-2 p-3 rounded-lg border ${ toolCall.result.success === false ? 'bg-yellow-900/30 border-yellow-400/30' : 'bg-orange-900/30 border-orange-400/30' }`}> {toolCall.result.success === false && toolCall.result.needsMoreCrypto ? ( // Not enough crypto - show suggestion <div className="text-xs text-yellow-100 space-y-1"> <div className="font-bold text-yellow-300">⚠️ Need More Crypto!</div> <div className="text-yellow-200"> 💡 Suggested: Buy ${toolCall.result.suggestedAmount} worth of crypto first </div> </div> ) : toolCall.result.data && ( // Success - show beer purchase <div className="text-xs space-y-1"> <div className="font-bold text-orange-300 mb-1"> 🍺 Beer Purchased with Crypto! </div> <div className="flex justify-between text-orange-100"> <span>Paid:</span> <span className="font-mono"> {toolCall.result.data.fromAmount?.toFixed(8)} {toolCall.result.data.fromCurrency} </span> </div> <div className="flex justify-between text-orange-100"> <span>Received:</span> <span className="font-mono"> {toolCall.result.data.toAmount} 🍺 </span> </div> <div className="text-orange-200 text-center mt-2 pt-2 border-t border-orange-400/30"> 🎉 Cheers! Enjoy your virtual beer! </div> </div> )} </div> )} </div> </div> ); })} </div> )} <div className="text-xs opacity-70 mt-2"> {message.timestamp.toLocaleTimeString()} </div> </div> </div> </div> ))} {isLoading && ( <div className="flex justify-start"> <div className="flex space-x-3"> <div className="flex-shrink-0 w-10 h-10 rounded-xl bg-gradient-to-r from-gray-100 to-gray-200 text-gray-600 flex items-center justify-center shadow-lg"> <Bot className="w-5 h-5" /> </div> <div className="bg-white/80 backdrop-blur-sm rounded-2xl p-4 border border-gray-200/50 shadow-lg"> <div className="flex items-center space-x-3"> <Loader2 className="w-4 h-4 animate-spin text-blue-600" /> <span className="text-sm text-gray-600">Analyzing and fetching data...</span> </div> </div> </div> </div> )} <div ref={messagesEndRef} /> </div> {/* Input */} <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl border border-gray-200/50 p-4"> <div className="flex space-x-3"> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} onKeyPress={handleKeyPress} placeholder="Ask me about cryptocurrency prices, market analysis, or trading insights..." className="flex-1 border-0 bg-transparent focus:outline-none text-gray-900 placeholder-gray-500 text-sm" disabled={isLoading || isListening} /> {/* Voice Input Button */} {speechSupported && ( <button onClick={toggleVoiceInput} disabled={isLoading} className={`p-3 rounded-xl transition-all duration-200 transform hover:scale-105 shadow-lg ${ isListening ? 'bg-gradient-to-r from-red-500 to-pink-600 text-white animate-pulse' : 'bg-gradient-to-r from-purple-500 to-indigo-600 text-white hover:from-purple-600 hover:to-indigo-700' } disabled:opacity-50 disabled:cursor-not-allowed`} title={isListening ? 'Stop recording' : 'Start voice input'} > {isListening ? <MicOff className="w-4 h-4" /> : <Mic className="w-4 h-4" />} </button> )} {/* Send Button */} <button onClick={handleSend} disabled={!input.trim() || isLoading} className="bg-gradient-to-r from-cyan-500 to-blue-600 text-white p-3 rounded-xl hover:from-cyan-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 transform hover:scale-105 shadow-lg" > <Send className="w-4 h-4" /> </button> </div> {/* Voice Input Status */} {isListening && ( <div className="mt-3 flex items-center justify-between"> <div className="flex items-center space-x-2 text-sm text-purple-600"> <div className="flex space-x-1"> <div className="w-1 h-4 bg-purple-600 rounded-full animate-pulse" style={{ animationDelay: '0s' }}></div> <div className="w-1 h-4 bg-purple-600 rounded-full animate-pulse" style={{ animationDelay: '0.2s' }}></div> <div className="w-1 h-4 bg-purple-600 rounded-full animate-pulse" style={{ animationDelay: '0.4s' }}></div> </div> <span className="font-medium"> {isRecording ? 'Recording... Click mic to stop' : 'Processing audio...'} </span> </div> {useOpenAI && voiceService.isAvailable() && ( <span className="text-xs text-purple-500 bg-purple-50 px-2 py-1 rounded-full"> 🌐 Multi-language (OpenAI Whisper) </span> )} </div> )} </div> </div> {/* Chat Session History Modal */} {showHistory && ( <ChatSessionHistory onClose={() => setShowHistory(false)} /> )} </div> ); }; export default ChatInterface;

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/Bichev/coinbase-chat-mcp'

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