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;