Skip to main content
Glama
App.tsx13.1 kB
import { useState, useRef, useEffect, useCallback } from 'react' import LeftSidebar from './components/LeftSidebar' import MiddleTopPanel from './components/MiddleTopPanel' import MiddleBottomPanel from './components/MiddleBottomPanel' import RightChatPanel from './components/RightChatPanel' import Dashboard from './components/Dashboard' import TutorialProvider, { TutorialType } from './components/Tutorial/TutorialProvider' import { TUTORIAL_STORAGE_KEY, TUTORIAL_SERVER_VIEW_KEY } from './components/Tutorial/tutorialSteps.tsx' import { mockServers, mockChatMessages } from './components/Tutorial/mockData' import type { MCPServer, ChatMessage } from './types' function App() { const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState<boolean>(true) const [selectedServer, setSelectedServer] = useState<MCPServer | null>(null) const [selectedMessage, setSelectedMessage] = useState<ChatMessage | null>(null) const [pendingMessageId, setPendingMessageId] = useState<string | number | null>(null) // Data from backend const [mcpServers, setMcpServers] = useState<MCPServer[]>([]) const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]) const [loading, setLoading] = useState<boolean>(true) // Tutorial state const [runTutorial, setRunTutorial] = useState<boolean>(false) const [tutorialType, setTutorialType] = useState<TutorialType>('dashboard') const [isTutorialMode, setIsTutorialMode] = useState<boolean>(false) // Resizable states const [middleTopHeight, setMiddleTopHeight] = useState(50) // percentage const [rightPanelWidth, setRightPanelWidth] = useState(400) // pixels instead of percentage const isDraggingVertical = useRef(false) const isDraggingHorizontal = useRef(false) const selectedServerIdRef = useRef<string | number | null>(null) // Update ref when selectedServer changes useEffect(() => { selectedServerIdRef.current = selectedServer?.id ?? null }, [selectedServer]) // Memoize fetch functions to avoid recreating on every render const fetchServers = useCallback(async () => { try { const data = await window.electronAPI.getServers() setMcpServers(data) // Update selectedServer if it exists (to reflect tool safety changes in real-time) // Use ref to avoid adding selectedServer to dependencies if (selectedServerIdRef.current !== null) { const updatedServer = data.find((s: MCPServer) => s.id === selectedServerIdRef.current) if (updatedServer) { setSelectedServer(updatedServer) } } setLoading(false) } catch (error) { console.error('Error fetching servers:', error) setLoading(false) } }, []) const fetchMessages = useCallback(async (serverId: string | number) => { try { const data = await window.electronAPI.getServerMessages(Number(serverId)) setChatMessages(data) } catch (error) { console.error('Error fetching messages:', error) setChatMessages([]) } }, []) // Fetch servers on mount useEffect(() => { fetchServers() }, [fetchServers]) // Subscribe to WebSocket updates for real-time data useEffect(() => { const unsubscribe = window.electronAPI.onWebSocketUpdate((message: any) => { console.log('[App] WebSocket update received:', message.type) switch (message.type) { case 'server_update': // Refresh server list when servers change console.log('[App] Refreshing servers due to server_update') fetchServers() break case 'message_update': // Refresh messages if the current server matches console.log('[App] Message update for:', message.data.server_name) if (selectedServer && message.data.server_name === selectedServer.name) { console.log('[App] Refreshing messages for current server') fetchMessages(selectedServer.id) } // Also refresh servers to update indicators fetchServers() break case 'detection_result': // Refresh both servers and messages to update malicious scores console.log('[App] Detection result received, refreshing all data') fetchServers() if (selectedServer) { fetchMessages(selectedServer.id) } break case 'tool_safety_update': // Refresh servers to update tool safety indicators console.log('[App] Tool safety update received:', message.data.mcp_tag, message.data.tool_name) fetchServers() // If the current server matches, refresh it to update the tools list if (selectedServer && message.data.mcp_tag === selectedServer.name) { console.log('[App] Refreshing current server for tool safety update') // Fetch servers will update the selectedServer in the list fetchServers() } break case 'reload_all': // Full reload of all data console.log('[App] Full reload requested') fetchServers() if (selectedServer) { fetchMessages(selectedServer.id) } break case 'connection': console.log('[App] WebSocket connection established') // Fetch initial data on connection fetchServers() break default: console.log('[App] Unknown WebSocket message type:', message.type) } }) // Cleanup on unmount return () => { unsubscribe() } }, [selectedServer, fetchServers, fetchMessages]) // Fetch messages when server is selected useEffect(() => { if (selectedServer) { // 튜토리얼 모드에서는 가상 데이터 사용 if (isTutorialMode) { setChatMessages(mockChatMessages) setSelectedMessage(mockChatMessages[0]) } else { fetchMessages(selectedServer.id) } // Reset selected message when server changes (튜토리얼 모드가 아닐 때만) if (!isTutorialMode) { setSelectedMessage(null) } } else { setChatMessages([]) setSelectedMessage(null) } }, [selectedServer, isTutorialMode]) // Auto-select message when pendingMessageId is set and messages are loaded useEffect(() => { if (pendingMessageId !== null && chatMessages.length > 0) { const message = chatMessages.find(m => m.id === pendingMessageId) if (message) { setSelectedMessage(message) setPendingMessageId(null) } } }, [pendingMessageId, chatMessages]) const serverInfo = selectedServer ? { name: selectedServer.name, type: selectedServer.type, tools: selectedServer.tools } : null // Handle vertical resize (middle panels) const handleVerticalMouseDown = () => { isDraggingVertical.current = true } const handleVerticalMouseMove = (e: React.MouseEvent<HTMLDivElement>) => { if (!isDraggingVertical.current) return const container = e.currentTarget const rect = container.getBoundingClientRect() const newHeight = ((e.clientY - rect.top) / rect.height) * 100 if (newHeight > 20 && newHeight < 80) { setMiddleTopHeight(newHeight) } } const handleVerticalMouseUp = () => { isDraggingVertical.current = false } // Handle horizontal resize (right panel) const handleHorizontalMouseDown = () => { isDraggingHorizontal.current = true } const handleHorizontalMouseMove = (e: React.MouseEvent<HTMLDivElement>) => { if (!isDraggingHorizontal.current) return const windowWidth = window.innerWidth const newWidth = windowWidth - e.clientX // Min 300px, max 60% of window width const minWidth = 300 const maxWidth = windowWidth * 0.6 if (newWidth >= minWidth && newWidth <= maxWidth) { setRightPanelWidth(newWidth) } } const handleHorizontalMouseUp = () => { isDraggingHorizontal.current = false } if (loading) { return ( <div className="flex h-screen bg-gray-100 items-center justify-center"> <div className="text-center"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div> <p className="text-gray-600">Loading servers...</p> </div> </div> ) } const handleTutorialComplete = () => { setRunTutorial(false) // Dashboard 튜토리얼 완료 후 MCP Server 튜토리얼로 자동 연결 if (isTutorialMode && tutorialType === 'dashboard') { // 첫 번째 mock 서버 선택 const firstServer = mockServers[0] setSelectedServer(firstServer) setChatMessages(mockChatMessages) setSelectedMessage(mockChatMessages[0]) // DOM 업데이트 후 Server 튜토리얼 시작 setTimeout(() => { setTutorialType('server') setRunTutorial(true) }, 300) return } // Server 튜토리얼 완료 또는 일반 모드 종료 시 정리 if (isTutorialMode) { setIsTutorialMode(false) setSelectedServer(null) setChatMessages([]) setSelectedMessage(null) } } const handleStartTutorial = () => { // 먼저 실행 중인 튜토리얼 중지 setRunTutorial(false) // 튜토리얼 시작 시 가상 데이터 모드 활성화 setIsTutorialMode(true) localStorage.removeItem(TUTORIAL_STORAGE_KEY) localStorage.removeItem(TUTORIAL_SERVER_VIEW_KEY) setSelectedServer(null) setTutorialType('dashboard') // 모든 스크롤 리셋 window.scrollTo(0, 0) // 1단계: 상태 업데이트 후 DOM 안정화 대기 requestAnimationFrame(() => { // 2단계: Dashboard 스크롤 리셋 const dashboardContainer = document.getElementById('dashboard-container') if (dashboardContainer) { dashboardContainer.scrollTop = 0 } // 3단계: DOM이 완전히 렌더링된 후 튜토리얼 시작 requestAnimationFrame(() => { setRunTutorial(true) }) }) } // 튜토리얼 모드에서 사용할 서버 목록 const displayServers = isTutorialMode ? mockServers : mcpServers return ( <div className="flex h-screen bg-gray-100" onMouseMove={handleHorizontalMouseMove} onMouseUp={handleHorizontalMouseUp} > {/* Tutorial */} <TutorialProvider run={runTutorial} type={tutorialType} onComplete={handleTutorialComplete} /> {/* Left Sidebar */} <LeftSidebar isOpen={isLeftSidebarOpen} setIsOpen={setIsLeftSidebarOpen} servers={displayServers} selectedServer={selectedServer} setSelectedServer={setSelectedServer} onStartTutorial={handleStartTutorial} /> {/* Main Content Area */} {selectedServer === null ? ( /* Dashboard View */ <div className="flex-1 min-w-0"> <Dashboard setSelectedServer={setSelectedServer} servers={displayServers} setSelectedMessageId={setPendingMessageId} isTutorialMode={isTutorialMode} /> </div> ) : ( <> {/* Middle Section */} <div className="flex-1 flex flex-col min-w-0" style={{ width: `calc(100% - ${rightPanelWidth}px)` }} onMouseMove={handleVerticalMouseMove} onMouseUp={handleVerticalMouseUp} > {/* Middle Top Panel */} <div className="border-b border-gray-300 relative min-h-0" style={{ height: `${middleTopHeight}%` }} data-tutorial="server-info" > <MiddleTopPanel serverInfo={serverInfo} /> {/* Vertical Resize Handle */} <div className="absolute bottom-0 left-0 right-0 h-1 bg-gray-300 hover:bg-blue-400 cursor-ns-resize transition-colors" onMouseDown={handleVerticalMouseDown} /> </div> {/* Middle Bottom Panel */} <div className="min-h-0" style={{ height: `${100 - middleTopHeight}%` }} data-tutorial="message-detail" > <MiddleBottomPanel selectedMessage={selectedMessage} /> </div> </div> {/* Horizontal Resize Handle */} <div className="w-1 bg-gray-300 hover:bg-blue-400 cursor-ew-resize transition-colors shrink-0" onMouseDown={handleHorizontalMouseDown} /> {/* Right Chat Panel */} <div className="border-l border-gray-300 -shrink-0" style={{ width: `${rightPanelWidth}px`, minWidth: '300px', maxWidth: '60vw' }} data-tutorial="chat-panel" > <RightChatPanel messages={chatMessages} selectedMessage={selectedMessage} setSelectedMessage={setSelectedMessage} /> </div> </> )} </div> ) } export default App

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/seungwon9201/MCP-Dandan'

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