Skip to main content
Glama
Dinesh-Satram

Health & Fitness Coach MCP

chat-window.tsx12 kB
"use client" import { useState, useRef, useEffect } from "react" import { Send, Bot, User, Zap, Database, FileText, MessageSquare, Dumbbell, Apple } from "lucide-react" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { cn } from "@/lib/utils" interface Message { id: string content: string sender: "user" | "coach" timestamp: Date toolUsed?: string intentType?: string } export function ChatWindow() { const [messages, setMessages] = useState<Message[]>([ { id: "1", content: "Hi there! I'm your personal fitness coach. I can help you log workouts, track nutrition, create plans, and answer any fitness questions. What would you like to work on today? 💪", sender: "coach", timestamp: new Date(Date.now() - 300000), toolUsed: "context-viewer" }, ]) const [newMessage, setNewMessage] = useState("") const [isTyping, setIsTyping] = useState(false) const messagesEndRef = useRef<HTMLDivElement>(null) const scrollToBottom = () => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }) } useEffect(() => { scrollToBottom() }, [messages]) const handleSendMessage = async () => { if (!newMessage.trim()) return const userMessage: Message = { id: Date.now().toString(), content: newMessage, sender: "user", timestamp: new Date(), } setMessages((prev) => [...prev, userMessage]) const currentMessage = newMessage setNewMessage("") setIsTyping(true) try { // Get current user context for better responses const contextResponse = await fetch('/api/context?userId=default-user') let userContext = null if (contextResponse.ok) { userContext = await contextResponse.json() } // Call enhanced chat API const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: currentMessage, context: userContext, userId: 'default-user', }), }) if (response.ok) { const data = await response.json() const coachMessage: Message = { id: (Date.now() + 1).toString(), content: data.response || "I'm here to help you achieve your fitness goals! 💪", sender: "coach", timestamp: new Date(), toolUsed: data.toolUsed, intentType: data.intent } setMessages((prev) => [...prev, coachMessage]) // Handle structured data responses if (data.structuredData?.refreshContext) { // Trigger context refresh to update UI panels window.dispatchEvent(new CustomEvent('refreshContext')) console.log('🔄 Context refresh triggered by chat action') } // Show additional feedback for specific actions if (data.structuredData?.type === 'workout_logged') { console.log('💪 Workout logged via chat - UI should update automatically') } else if (data.structuredData?.type === 'nutrition_logged') { console.log('🍎 Nutrition logged via chat - UI should update automatically') } else if (data.structuredData?.type === 'plan_generated') { console.log('📋 Plan generated via chat - UI should update automatically') } } else { throw new Error('Failed to get response') } } catch (error) { console.error('Error in chat:', error) const errorMessage: Message = { id: (Date.now() + 1).toString(), content: "I'm sorry, I'm having trouble right now. Please try again in a moment! 😊", sender: "coach", timestamp: new Date(), } setMessages((prev) => [...prev, errorMessage]) } finally { setIsTyping(false) } } // Get tool icon based on tool used const getToolIcon = (toolUsed?: string) => { switch (toolUsed) { case 'workout-logger': return <Dumbbell className="w-3 h-3" /> case 'nutrition-logger': return <Apple className="w-3 h-3" /> case 'plan-generator': return <FileText className="w-3 h-3" /> case 'context-viewer': return <Database className="w-3 h-3" /> default: return <MessageSquare className="w-3 h-3" /> } } // Get tool display name const getToolName = (toolUsed?: string) => { switch (toolUsed) { case 'workout-logger': return 'Workout Logger' case 'nutrition-logger': return 'Nutrition Tracker' case 'plan-generator': return 'Plan Generator' case 'context-viewer': return 'Context Analysis' default: return 'General Chat' } } // Get intent badge color const getIntentBadgeColor = (intentType?: string) => { switch (intentType) { case 'log_activity': return 'bg-orange-100 text-orange-700 border-orange-200' case 'log_nutrition': return 'bg-green-100 text-green-700 border-green-200' case 'generate_plan': return 'bg-blue-100 text-blue-700 border-blue-200' default: return 'bg-gray-100 text-gray-600 border-gray-200' } } return ( <div className="h-full bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg border border-white/20 flex flex-col overflow-hidden"> {/* Chat Header */} <div className="p-6 border-b border-slate-200/50 bg-gradient-to-r from-slate-50 to-blue-50"> <div className="flex items-center gap-3"> <div className="w-10 h-10 bg-gradient-to-br from-blue-400 to-indigo-500 rounded-full flex items-center justify-center"> <Bot className="w-6 h-6 text-white" /> </div> <div> <h3 className="font-semibold text-slate-800">AI Fitness Coach</h3> <p className="text-sm text-slate-600">Smart MCP-powered assistant</p> </div> </div> </div> {/* Messages */} <div className="flex-1 overflow-y-auto p-6 space-y-4"> {messages.map((message) => ( <div key={message.id} className={cn( "flex gap-3 animate-in slide-in-from-bottom-2 duration-300", message.sender === "user" ? "justify-end" : "justify-start", )} > {message.sender === "coach" && ( <div className="w-8 h-8 bg-gradient-to-br from-blue-400 to-indigo-500 rounded-full flex items-center justify-center flex-shrink-0"> <Bot className="w-4 h-4 text-white" /> </div> )} <div className="flex flex-col max-w-xs lg:max-w-md"> <div className={cn( "px-4 py-3 rounded-2xl shadow-sm", message.sender === "user" ? "bg-gradient-to-br from-lime-400 to-green-500 text-white rounded-br-md" : "bg-gradient-to-br from-blue-50 to-indigo-50 text-slate-800 border border-blue-200/50 rounded-bl-md", )} > <p className="text-sm leading-relaxed whitespace-pre-wrap">{message.content}</p> <p className={cn("text-xs mt-2 opacity-70", message.sender === "user" ? "text-white" : "text-slate-500")}> {message.timestamp.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })} </p> </div> {/* Tool Usage Indicator - Only for coach messages */} {message.sender === "coach" && message.toolUsed && ( <div className="flex items-center gap-2 mt-2 ml-2"> <div className={cn( "flex items-center gap-1.5 px-2 py-1 rounded-full text-xs border", getIntentBadgeColor(message.intentType) )}> {getToolIcon(message.toolUsed)} <span className="font-medium">{getToolName(message.toolUsed)}</span> </div> {message.intentType && message.intentType !== 'general_chat' && ( <div className="flex items-center gap-1"> <Zap className="w-3 h-3 text-amber-500" /> <span className="text-xs text-slate-500 font-medium"> {message.intentType === 'log_activity' ? 'Logged Activity' : message.intentType === 'log_nutrition' ? 'Logged Nutrition' : message.intentType === 'generate_plan' ? 'Generated Plan' : 'Chat'} </span> </div> )} </div> )} </div> {message.sender === "user" && ( <div className="w-8 h-8 bg-gradient-to-br from-lime-400 to-green-500 rounded-full flex items-center justify-center flex-shrink-0"> <User className="w-4 h-4 text-white" /> </div> )} </div> ))} {/* Typing Indicator */} {isTyping && ( <div className="flex gap-3 animate-in slide-in-from-bottom-2 duration-300"> <div className="w-8 h-8 bg-gradient-to-br from-blue-400 to-indigo-500 rounded-full flex items-center justify-center"> <Bot className="w-4 h-4 text-white" /> </div> <div className="bg-gradient-to-br from-blue-50 to-indigo-50 border border-blue-200/50 px-4 py-3 rounded-2xl rounded-bl-md"> <div className="flex gap-1"> <div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce"></div> <div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce" style={{ animationDelay: "0.1s" }} ></div> <div className="w-2 h-2 bg-blue-400 rounded-full animate-bounce" style={{ animationDelay: "0.2s" }} ></div> </div> </div> </div> )} <div ref={messagesEndRef} /> </div> {/* Message Input */} <div className="p-6 border-t border-slate-200/50 bg-gradient-to-r from-slate-50 to-blue-50"> <div className="flex gap-3"> <Input value={newMessage} onChange={(e) => setNewMessage(e.target.value)} placeholder="Try: 'I did 20 push-ups' or 'Create a meal plan' or 'I ate breakfast'" className="flex-1 rounded-2xl border-slate-200 bg-white/80 backdrop-blur-sm" onKeyPress={(e) => e.key === "Enter" && handleSendMessage()} /> <Button onClick={handleSendMessage} className="rounded-2xl bg-gradient-to-r from-lime-400 to-green-500 hover:from-lime-500 hover:to-green-600 shadow-lg" size="icon" > <Send className="w-4 h-4" /> </Button> </div> {/* Quick Action Hints */} <div className="mt-3 flex flex-wrap gap-2"> <button onClick={() => setNewMessage("I did 15 minutes of push-ups")} className="text-xs px-3 py-1 bg-orange-100 text-orange-700 rounded-full hover:bg-orange-200 transition-colors" > Log Workout </button> <button onClick={() => setNewMessage("I ate breakfast with oatmeal")} className="text-xs px-3 py-1 bg-green-100 text-green-700 rounded-full hover:bg-green-200 transition-colors" > Log Meal </button> <button onClick={() => setNewMessage("Create a workout plan for me")} className="text-xs px-3 py-1 bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors" > Generate Plan </button> </div> </div> </div> ) }

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/Dinesh-Satram/fitness_coach_MCP'

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