Skip to main content
Glama

MCP Agent - AI Expense Tracker

by dev-muhammad
Sidebar.tsx•10 kB
'use client' import { Button } from '@/components/ui/button' import { ModeSelector } from '@/components/chat/Sidebar/ModeSelector' import { EntitySelector } from '@/components/chat/Sidebar/EntitySelector' import useChatActions from '@/hooks/useChatActions' import { useStore } from '@/store' import { motion, AnimatePresence } from 'framer-motion' import { useState, useEffect } from 'react' import Icon from '@/components/ui/icon' import { getProviderIcon } from '@/lib/modelProvider' import Sessions from './Sessions' import { isValidUrl } from '@/lib/utils' import { toast } from 'sonner' import { useQueryState } from 'nuqs' import { truncateText } from '@/lib/utils' import { Skeleton } from '@/components/ui/skeleton' const ENDPOINT_PLACEHOLDER = 'NO ENDPOINT ADDED' const SidebarHeader = () => ( <div className="flex items-center gap-2"> <Icon type="agno" size="xs" /> <span className="text-xs font-medium uppercase text-white">Agent UI</span> </div> ) const NewChatButton = ({ disabled, onClick }: { disabled: boolean onClick: () => void }) => ( <Button onClick={onClick} disabled={disabled} size="lg" className="h-9 w-full rounded-xl bg-primary text-xs font-medium text-background hover:bg-primary/80" > <Icon type="plus-icon" size="xs" className="text-background" /> <span className="uppercase">New Chat</span> </Button> ) const ModelDisplay = ({ model }: { model: string }) => ( <div className="flex h-9 w-full items-center gap-3 rounded-xl border border-primary/15 bg-accent p-3 text-xs font-medium uppercase text-muted"> {(() => { const icon = getProviderIcon(model) return icon ? <Icon type={icon} className="shrink-0" size="xs" /> : null })()} {model} </div> ) const Endpoint = () => { const { selectedEndpoint, isEndpointActive, setSelectedEndpoint, setAgents, setSessionsData, setMessages } = useStore() const { initialize } = useChatActions() const [isEditing, setIsEditing] = useState(false) const [endpointValue, setEndpointValue] = useState('') const [isMounted, setIsMounted] = useState(false) const [isHovering, setIsHovering] = useState(false) const [isRotating, setIsRotating] = useState(false) const [, setAgentId] = useQueryState('agent') const [, setSessionId] = useQueryState('session') useEffect(() => { setEndpointValue(selectedEndpoint) setIsMounted(true) }, [selectedEndpoint]) const getStatusColor = (isActive: boolean) => isActive ? 'bg-positive' : 'bg-destructive' const handleSave = async () => { if (!isValidUrl(endpointValue)) { toast.error('Please enter a valid URL') return } const cleanEndpoint = endpointValue.replace(/\/$/, '').trim() setSelectedEndpoint(cleanEndpoint) setAgentId(null) setSessionId(null) setIsEditing(false) setIsHovering(false) setAgents([]) setSessionsData([]) setMessages([]) } const handleCancel = () => { setEndpointValue(selectedEndpoint) setIsEditing(false) setIsHovering(false) } const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') { handleSave() } else if (e.key === 'Escape') { handleCancel() } } const handleRefresh = async () => { setIsRotating(true) await initialize() setTimeout(() => setIsRotating(false), 500) } return ( <div className="flex flex-col items-start gap-2"> <div className="text-xs font-medium uppercase text-primary">AgentOS</div> {isEditing ? ( <div className="flex w-full items-center gap-1"> <input type="text" value={endpointValue} onChange={(e) => setEndpointValue(e.target.value)} onKeyDown={handleKeyDown} className="flex h-9 w-full items-center text-ellipsis rounded-xl border border-primary/15 bg-accent p-3 text-xs font-medium text-muted" autoFocus /> <Button variant="ghost" size="icon" onClick={handleSave} className="hover:cursor-pointer hover:bg-transparent" > <Icon type="save" size="xs" /> </Button> </div> ) : ( <div className="flex w-full items-center gap-1"> <motion.div className="relative flex h-9 w-full cursor-pointer items-center justify-between rounded-xl border border-primary/15 bg-accent p-3 uppercase" onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} onClick={() => setIsEditing(true)} transition={{ type: 'spring', stiffness: 400, damping: 10 }} > <AnimatePresence mode="wait"> {isHovering ? ( <motion.div key="endpoint-display-hover" className="absolute inset-0 flex items-center justify-center" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} > <p className="flex items-center gap-2 whitespace-nowrap text-xs font-medium text-primary"> <Icon type="edit" size="xxs" /> EDIT AGENTOS </p> </motion.div> ) : ( <motion.div key="endpoint-display" className="absolute inset-0 flex items-center justify-between px-3" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} > <p className="text-xs font-medium text-muted"> {isMounted ? truncateText(selectedEndpoint, 21) || ENDPOINT_PLACEHOLDER : 'http://localhost:7777'} </p> <div className={`size-2 shrink-0 rounded-full ${getStatusColor(isEndpointActive)}`} /> </motion.div> )} </AnimatePresence> </motion.div> <Button variant="ghost" size="icon" onClick={handleRefresh} className="hover:cursor-pointer hover:bg-transparent" > <motion.div key={isRotating ? 'rotating' : 'idle'} animate={{ rotate: isRotating ? 360 : 0 }} transition={{ duration: 0.5, ease: 'easeInOut' }} > <Icon type="refresh" size="xs" /> </motion.div> </Button> </div> )} </div> ) } const Sidebar = () => { const [isCollapsed, setIsCollapsed] = useState(false) const { clearChat, focusChatInput, initialize } = useChatActions() const { messages, selectedEndpoint, isEndpointActive, selectedModel, hydrated, isEndpointLoading, mode } = useStore() const [isMounted, setIsMounted] = useState(false) const [agentId] = useQueryState('agent') const [teamId] = useQueryState('team') useEffect(() => { setIsMounted(true) if (hydrated) initialize() }, [selectedEndpoint, initialize, hydrated, mode]) const handleNewChat = () => { clearChat() focusChatInput() } return ( <motion.aside className="relative flex h-screen shrink-0 grow-0 flex-col overflow-hidden px-2 py-3 font-dmmono" initial={{ width: '16rem' }} animate={{ width: isCollapsed ? '2.5rem' : '16rem' }} transition={{ type: 'spring', stiffness: 300, damping: 30 }} > <motion.button onClick={() => setIsCollapsed(!isCollapsed)} className="absolute right-2 top-2 z-10 p-1" aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'} type="button" whileTap={{ scale: 0.95 }} > <Icon type="sheet" size="xs" className={`transform ${isCollapsed ? 'rotate-180' : 'rotate-0'}`} /> </motion.button> <motion.div className="w-60 space-y-5" initial={{ opacity: 0, x: -20 }} animate={{ opacity: isCollapsed ? 0 : 1, x: isCollapsed ? -20 : 0 }} transition={{ duration: 0.3, ease: 'easeInOut' }} style={{ pointerEvents: isCollapsed ? 'none' : 'auto' }} > <SidebarHeader /> <NewChatButton disabled={messages.length === 0} onClick={handleNewChat} /> {isMounted && ( <> <Endpoint /> {isEndpointActive && ( <> <motion.div className="flex w-full flex-col items-start gap-2" initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.5, ease: 'easeInOut' }} > <div className="text-xs font-medium uppercase text-primary"> Mode </div> {isEndpointLoading ? ( <div className="flex w-full flex-col gap-2"> {Array.from({ length: 3 }).map((_, index) => ( <Skeleton key={index} className="h-9 w-full rounded-xl" /> ))} </div> ) : ( <> <ModeSelector /> <EntitySelector /> {selectedModel && (agentId || teamId) && ( <ModelDisplay model={selectedModel} /> )} </> )} </motion.div> <Sessions /> </> )} </> )} </motion.div> </motion.aside> ) } export default Sidebar

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/dev-muhammad/MCPAgent'

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