Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="dashboard"> | |
| <!-- Header Section --> | |
| <div class="dashboard-header"> | |
| <div class="header-content"> | |
| <div class="header-left"> | |
| <h1 class="dashboard-title">SAAP Multi-Agent Dashboard</h1> | |
| <p class="dashboard-subtitle">Manage and monitor your AI agent ecosystem</p> | |
| </div> | |
| <div class="header-right"> | |
| <div class="status-indicator"> | |
| <div class="status-dot" :class="connectionStatus === 'connected' ? 'status-connected' : 'status-disconnected'"></div> | |
| <span class="status-text">{{ connectionStatus === 'connected' ? 'Connected' : 'Disconnected' }}</span> | |
| </div> | |
| <div class="agents-counter"> | |
| <span class="counter-number">{{ systemStats.activeAgents }}</span> | |
| <span class="counter-label">AGENTS ACTIVE</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Action Buttons --> | |
| <div class="action-section"> | |
| <button class="action-button refresh-button" @click="refreshData"> | |
| <ArrowPathIcon class="button-icon" /> | |
| <span>Refresh</span> | |
| </button> | |
| <button class="action-button add-button" @click="showAddAgent = true"> | |
| <PlusIcon class="button-icon" /> | |
| <span>Add Agent</span> | |
| </button> | |
| </div> | |
| <!-- System Statistics --> | |
| <div class="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="stat-icon-wrapper stat-icon-blue"> | |
| <UserGroupIcon class="stat-icon" /> | |
| </div> | |
| <div class="stat-content"> | |
| <h3>{{ systemStats.totalAgents }}</h3> | |
| <p>Total Agents</p> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon-wrapper stat-icon-green"> | |
| <ChartBarIcon class="stat-icon" /> | |
| </div> | |
| <div class="stat-content"> | |
| <h3>{{ systemStats.activeAgents }}</h3> | |
| <p>Active Agents</p> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon-wrapper stat-icon-orange"> | |
| <ChatBubbleLeftIcon class="stat-icon" /> | |
| </div> | |
| <div class="stat-content"> | |
| <h3>{{ systemStats.totalMessages }}</h3> | |
| <p>Messages Today</p> | |
| </div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-icon-wrapper stat-icon-purple"> | |
| <ClockIcon class="stat-icon" /> | |
| </div> | |
| <div class="stat-content"> | |
| <h3>{{ systemStats.averageResponseTime }}s</h3> | |
| <p>Avg Response Time</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Agents Grid --> | |
| <div class="agents-section"> | |
| <div class="section-header"> | |
| <h2>Active Agents</h2> | |
| <div class="view-toggle"> | |
| <button | |
| class="toggle-button" | |
| :class="{ active: viewMode === 'grid' }" | |
| @click="viewMode = 'grid'" | |
| > | |
| <Squares2X2Icon class="toggle-icon" /> | |
| Grid | |
| </button> | |
| <button | |
| class="toggle-button" | |
| :class="{ active: viewMode === 'list' }" | |
| @click="viewMode = 'list'" | |
| > | |
| <Bars3Icon class="toggle-icon" /> | |
| List | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Grid View --> | |
| <div v-if="viewMode === 'grid'" class="agents-grid"> | |
| <AgentCard | |
| v-for="agent in agents" | |
| :key="agent.id" | |
| :agent="agent" | |
| @start="startAgent" | |
| @stop="stopAgent" | |
| @chat="openChat" | |
| @details="showAgentDetails" | |
| /> | |
| <!-- Empty State --> | |
| <div v-if="agents.length === 0" class="empty-state"> | |
| <div class="empty-state-content"> | |
| <CpuChipIcon class="empty-icon" /> | |
| <h3>No Agents Available</h3> | |
| <p>Start by adding your first AI agent to the platform.</p> | |
| <button class="empty-action-button" @click="showAddAgent = true"> | |
| <PlusIcon class="button-icon" /> | |
| Add First Agent | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- List View --> | |
| <div v-if="viewMode === 'list'" class="agents-list"> | |
| <div class="list-header"> | |
| <div class="list-column">Name</div> | |
| <div class="list-column">Type</div> | |
| <div class="list-column">Status</div> | |
| <div class="list-column">Messages</div> | |
| <div class="list-column">Response Time</div> | |
| <div class="list-column">Actions</div> | |
| </div> | |
| <div | |
| v-for="agent in agents" | |
| :key="agent.id" | |
| class="list-row" | |
| > | |
| <div class="list-cell"> | |
| <div class="agent-name-cell"> | |
| <div | |
| class="agent-avatar" | |
| :style="{ backgroundColor: agent.color || '#6B7280' }" | |
| > | |
| {{ agent.name?.charAt(0) || 'A' }} | |
| </div> | |
| <span>{{ agent.name }}</span> | |
| </div> | |
| </div> | |
| <div class="list-cell"> | |
| <span class="agent-type-badge">{{ agent.type }}</span> | |
| </div> | |
| <div class="list-cell"> | |
| <span | |
| class="status-badge" | |
| :class="`status-${agent.status}`" | |
| > | |
| {{ agent.status }} | |
| </span> | |
| </div> | |
| <div class="list-cell"> | |
| {{ agent.metrics?.messages_processed || 0 }} | |
| </div> | |
| <div class="list-cell"> | |
| {{ agent.metrics?.average_response_time?.toFixed(2) || 0 }}s | |
| </div> | |
| <div class="list-cell"> | |
| <div class="action-buttons"> | |
| <button | |
| v-if="agent.status !== 'active'" | |
| class="action-btn start-btn" | |
| @click="startAgent(agent)" | |
| > | |
| <PlayIcon class="action-icon" /> | |
| </button> | |
| <button | |
| v-if="agent.status === 'active'" | |
| class="action-btn stop-btn" | |
| @click="stopAgent(agent)" | |
| > | |
| <StopIcon class="action-icon" /> | |
| </button> | |
| <button | |
| class="action-btn chat-btn" | |
| @click="openChat(agent)" | |
| > | |
| <ChatBubbleLeftIcon class="action-icon" /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Add Agent Modal --> | |
| <AddAgentModal | |
| v-if="showAddAgent" | |
| @close="showAddAgent = false" | |
| @add="handleAddAgent" | |
| /> | |
| <!-- Chat Modal --> | |
| <ChatModal | |
| v-if="showChat && selectedAgent" | |
| :agent="selectedAgent" | |
| @close="showChat = false" | |
| /> | |
| <!-- Agent Details Modal --> | |
| <AgentDetailsModal | |
| v-if="showDetails && selectedAgent" | |
| :agent="selectedAgent" | |
| @close="showDetails = false" | |
| @update="handleAgentUpdate" | |
| /> | |
| </div> | |
| </template> | |
| <script> | |
| import { ref, reactive, onMounted, onUnmounted } from 'vue' | |
| import AgentCard from '../components/agent/AgentCard.vue' | |
| import AddAgentModal from '../components/modals/AddAgentModal.vue' | |
| import ChatModal from '../components/modals/ChatModal.vue' | |
| import AgentDetailsModal from '../components/modals/AgentDetailsModal.vue' | |
| // Icons from Heroicons | |
| import { | |
| ArrowPathIcon, | |
| PlusIcon, | |
| UserGroupIcon, | |
| ChartBarIcon, | |
| ChatBubbleLeftIcon, | |
| ClockIcon, | |
| Squares2X2Icon, | |
| Bars3Icon, | |
| CpuChipIcon, | |
| PlayIcon, | |
| StopIcon | |
| } from '@heroicons/vue/24/outline' | |
| export default { | |
| name: 'Dashboard', | |
| components: { | |
| AgentCard, | |
| AddAgentModal, | |
| ChatModal, | |
| AgentDetailsModal, | |
| ArrowPathIcon, | |
| PlusIcon, | |
| UserGroupIcon, | |
| ChartBarIcon, | |
| ChatBubbleLeftIcon, | |
| ClockIcon, | |
| Squares2X2Icon, | |
| Bars3Icon, | |
| CpuChipIcon, | |
| PlayIcon, | |
| StopIcon | |
| }, | |
| setup() { | |
| const agents = ref([]) | |
| const viewMode = ref('grid') | |
| const connectionStatus = ref('disconnected') | |
| const showAddAgent = ref(false) | |
| const showChat = ref(false) | |
| const showDetails = ref(false) | |
| const selectedAgent = ref(null) | |
| const loading = ref(false) | |
| const systemStats = reactive({ | |
| totalAgents: 0, | |
| activeAgents: 0, | |
| totalMessages: 0, | |
| averageResponseTime: 0 | |
| }) | |
| // WebSocket connection | |
| let ws = null | |
| const connectWebSocket = () => { | |
| try { | |
| ws = new WebSocket('ws://localhost:8000/ws') | |
| ws.onopen = () => { | |
| connectionStatus.value = 'connected' | |
| console.log('✅ WebSocket connected') | |
| } | |
| ws.onmessage = (event) => { | |
| const data = JSON.parse(event.data) | |
| handleWebSocketMessage(data) | |
| } | |
| ws.onclose = () => { | |
| connectionStatus.value = 'disconnected' | |
| console.log('❌ WebSocket disconnected') | |
| // Reconnect after 5 seconds | |
| setTimeout(connectWebSocket, 5000) | |
| } | |
| ws.onerror = (error) => { | |
| console.error('❌ WebSocket error:', error) | |
| connectionStatus.value = 'disconnected' | |
| } | |
| } catch (error) { | |
| console.error('❌ WebSocket connection failed:', error) | |
| connectionStatus.value = 'disconnected' | |
| } | |
| } | |
| const handleWebSocketMessage = (data) => { | |
| switch (data.type) { | |
| case 'agent_status_update': | |
| updateAgentInList(data.agent) | |
| break | |
| case 'system_stats': | |
| Object.assign(systemStats, data.stats) | |
| break | |
| case 'new_message': | |
| // Handle new message notifications | |
| break | |
| default: | |
| console.log('📨 Unknown WebSocket message:', data) | |
| } | |
| } | |
| const updateAgentInList = (updatedAgent) => { | |
| const index = agents.value.findIndex(a => a.id === updatedAgent.id) | |
| if (index !== -1) { | |
| agents.value[index] = { ...agents.value[index], ...updatedAgent } | |
| } | |
| } | |
| const fetchAgents = async () => { | |
| try { | |
| loading.value = true | |
| console.log('🔄 Fetching agents from API...') | |
| const response = await fetch('http://localhost:8000/api/v1/agents') | |
| console.log('📡 Response status:', response.status) | |
| if (response.ok) { | |
| const data = await response.json() | |
| console.log('📊 Agents data:', data) | |
| agents.value = data | |
| updateSystemStats() | |
| } else { | |
| console.error('❌ API Error:', response.status, response.statusText) | |
| throw new Error(`API Error: ${response.status}`) | |
| } | |
| } catch (error) { | |
| console.error('❌ Error fetching agents:', error) | |
| // Keep existing mock data as fallback | |
| if (agents.value.length === 0) { | |
| agents.value = [ | |
| { | |
| id: 'jane_alesi', | |
| name: 'Jane Alesi', | |
| type: 'Coordinator', | |
| status: 'active', | |
| color: '#8B5CF6', | |
| metrics: { | |
| messages_processed: 42, | |
| average_response_time: 1.2 | |
| } | |
| }, | |
| { | |
| id: 'john_alesi', | |
| name: 'John Alesi', | |
| type: 'Developer', | |
| status: 'inactive', | |
| color: '#14B8A6', | |
| metrics: { | |
| messages_processed: 23, | |
| average_response_time: 2.1 | |
| } | |
| }, | |
| { | |
| id: 'lara_alesi', | |
| name: 'Lara Alesi', | |
| type: 'Medical Expert', | |
| status: 'active', | |
| color: '#EC4899', | |
| metrics: { | |
| messages_processed: 15, | |
| average_response_time: 1.8 | |
| } | |
| } | |
| ] | |
| } | |
| updateSystemStats() | |
| } finally { | |
| loading.value = false | |
| } | |
| } | |
| const fetchSystemStatus = async () => { | |
| try { | |
| console.log('🔄 Fetching system status...') | |
| const response = await fetch('http://localhost:8000/api/v1/health') | |
| if (response.ok) { | |
| const data = await response.json() | |
| console.log('🏥 Health data:', data) | |
| // Update system stats based on health data | |
| Object.assign(systemStats, { | |
| totalAgents: agents.value.length, | |
| activeAgents: agents.value.filter(a => a.status === 'active').length, | |
| totalMessages: agents.value.length * 15, // Mock calculation | |
| averageResponseTime: 1.8 // Mock data | |
| }) | |
| } | |
| } catch (error) { | |
| console.error('❌ Error fetching system status:', error) | |
| updateSystemStats() | |
| } | |
| } | |
| const updateSystemStats = () => { | |
| systemStats.totalAgents = agents.value.length | |
| systemStats.activeAgents = agents.value.filter(a => a.status === 'active').length | |
| systemStats.totalMessages = agents.value.length * 15 // Mock | |
| systemStats.averageResponseTime = 1.8 // Mock | |
| } | |
| const refreshData = async () => { | |
| console.log('🔄 Refreshing dashboard data...') | |
| await Promise.all([ | |
| fetchAgents(), | |
| fetchSystemStatus() | |
| ]) | |
| } | |
| const startAgent = async (agent) => { | |
| try { | |
| console.log(`🚀 Starting agent: ${agent.name}`) | |
| const response = await fetch(`http://localhost:8000/api/v1/agents/${agent.id}/start`, { | |
| method: 'POST' | |
| }) | |
| if (response.ok) { | |
| const data = await response.json() | |
| console.log('✅ Agent started:', data) | |
| // Update agent status immediately | |
| agent.status = 'active' | |
| updateSystemStats() | |
| } else { | |
| console.error('❌ Failed to start agent:', response.status) | |
| } | |
| } catch (error) { | |
| console.error('❌ Error starting agent:', error) | |
| } | |
| } | |
| const stopAgent = async (agent) => { | |
| try { | |
| console.log(`🛑 Stopping agent: ${agent.name}`) | |
| const response = await fetch(`http://localhost:8000/api/v1/agents/${agent.id}/stop`, { | |
| method: 'POST' | |
| }) | |
| if (response.ok) { | |
| const data = await response.json() | |
| console.log('✅ Agent stopped:', data) | |
| // Update agent status immediately | |
| agent.status = 'inactive' | |
| updateSystemStats() | |
| } else { | |
| console.error('❌ Failed to stop agent:', response.status) | |
| } | |
| } catch (error) { | |
| console.error('❌ Error stopping agent:', error) | |
| } | |
| } | |
| const openChat = (agent) => { | |
| selectedAgent.value = agent | |
| showChat.value = true | |
| } | |
| const showAgentDetails = (agent) => { | |
| selectedAgent.value = agent | |
| showDetails.value = true | |
| } | |
| const handleAddAgent = (newAgent) => { | |
| // Add agent logic handled in modal | |
| showAddAgent.value = false | |
| refreshData() | |
| } | |
| const handleAgentUpdate = (updatedAgent) => { | |
| // Update agent logic | |
| showDetails.value = false | |
| refreshData() | |
| } | |
| onMounted(async () => { | |
| console.log('🚀 Dashboard mounted - initializing...') | |
| await refreshData() | |
| connectWebSocket() | |
| }) | |
| onUnmounted(() => { | |
| if (ws) { | |
| ws.close() | |
| } | |
| }) | |
| return { | |
| agents, | |
| viewMode, | |
| connectionStatus, | |
| showAddAgent, | |
| showChat, | |
| showDetails, | |
| selectedAgent, | |
| loading, | |
| systemStats, | |
| refreshData, | |
| startAgent, | |
| stopAgent, | |
| openChat, | |
| showAgentDetails, | |
| handleAddAgent, | |
| handleAgentUpdate | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| .dashboard { | |
| @apply min-h-screen bg-gray-50; | |
| } | |
| .dashboard-header { | |
| @apply bg-white border-b border-gray-200 px-6 py-4; | |
| } | |
| .header-content { | |
| @apply flex justify-between items-start max-w-7xl mx-auto; | |
| } | |
| .header-left { | |
| @apply space-y-1; | |
| } | |
| .dashboard-title { | |
| @apply text-2xl font-bold text-saap-gray-900; | |
| } | |
| .dashboard-subtitle { | |
| @apply text-sm text-saap-gray-600; | |
| } | |
| .header-right { | |
| @apply flex items-center space-x-6; | |
| } | |
| .status-indicator { | |
| @apply flex items-center space-x-2; | |
| } | |
| .status-dot { | |
| @apply w-2 h-2 rounded-full; | |
| } | |
| .status-connected { | |
| @apply bg-green-400; | |
| } | |
| .status-disconnected { | |
| @apply bg-red-400; | |
| } | |
| .status-text { | |
| @apply text-sm font-medium text-saap-gray-700; | |
| } | |
| .agents-counter { | |
| @apply text-right; | |
| } | |
| .counter-number { | |
| @apply block text-2xl font-bold text-saap-primary-600; | |
| } | |
| .counter-label { | |
| @apply text-xs font-medium text-saap-gray-500 uppercase tracking-wide; | |
| } | |
| .action-section { | |
| @apply px-6 py-4 bg-white border-b border-gray-200; | |
| } | |
| .action-section { | |
| @apply flex space-x-3 max-w-7xl mx-auto; | |
| } | |
| .action-button { | |
| @apply inline-flex items-center px-4 py-2 border border-gray-300 rounded-lg text-sm font-medium transition-all duration-200 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2; | |
| } | |
| .refresh-button { | |
| @apply text-saap-gray-700 hover:text-saap-gray-900 focus:ring-saap-gray-500; | |
| } | |
| .add-button { | |
| @apply bg-saap-primary-600 text-white border-saap-primary-600 hover:bg-saap-primary-700 focus:ring-saap-primary-500; | |
| } | |
| .button-icon { | |
| @apply w-4 h-4 mr-2; | |
| } | |
| .stats-grid { | |
| @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 px-6 py-6 max-w-7xl mx-auto; | |
| } | |
| .stat-card { | |
| @apply bg-white rounded-xl p-6 shadow-sm border border-gray-200 hover:shadow-md transition-shadow duration-200; | |
| } | |
| .stat-card { | |
| @apply flex items-center space-x-4; | |
| } | |
| .stat-icon-wrapper { | |
| @apply w-12 h-12 rounded-lg flex items-center justify-center; | |
| } | |
| .stat-icon-blue { | |
| @apply bg-saap-primary-100; | |
| } | |
| .stat-icon-green { | |
| @apply bg-saap-secondary-100; | |
| } | |
| .stat-icon-orange { | |
| @apply bg-saap-accent-100; | |
| } | |
| .stat-icon-purple { | |
| @apply bg-purple-100; | |
| } | |
| .stat-icon { | |
| @apply w-6 h-6; | |
| } | |
| .stat-icon-blue .stat-icon { | |
| @apply text-saap-primary-600; | |
| } | |
| .stat-icon-green .stat-icon { | |
| @apply text-saap-secondary-600; | |
| } | |
| .stat-icon-orange .stat-icon { | |
| @apply text-saap-accent-600; | |
| } | |
| .stat-icon-purple .stat-icon { | |
| @apply text-purple-600; | |
| } | |
| .stat-content h3 { | |
| @apply text-xl font-bold text-saap-gray-900 truncate; | |
| } | |
| .stat-content p { | |
| @apply text-sm text-saap-gray-600; | |
| } | |
| .agents-section { | |
| @apply px-6 py-6 max-w-7xl mx-auto; | |
| } | |
| .section-header { | |
| @apply flex justify-between items-center mb-6; | |
| } | |
| .section-header h2 { | |
| @apply text-xl font-semibold text-saap-gray-900; | |
| } | |
| .view-toggle { | |
| @apply flex bg-gray-100 rounded-lg p-1; | |
| } | |
| .toggle-button { | |
| @apply flex items-center px-3 py-1.5 text-sm font-medium rounded-md transition-all duration-200; | |
| } | |
| .toggle-button.active { | |
| @apply bg-white text-saap-primary-600 shadow-sm; | |
| } | |
| .toggle-button:not(.active) { | |
| @apply text-saap-gray-600 hover:text-saap-gray-900; | |
| } | |
| .toggle-icon { | |
| @apply w-4 h-4 mr-1.5; | |
| } | |
| .agents-grid { | |
| @apply grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6; | |
| } | |
| .empty-state { | |
| @apply col-span-full; | |
| } | |
| .empty-state-content { | |
| @apply text-center py-12 px-4; | |
| } | |
| .empty-icon { | |
| @apply w-16 h-16 mx-auto text-saap-gray-400 mb-4; | |
| } | |
| .empty-state-content h3 { | |
| @apply text-lg font-medium text-saap-gray-900 mb-2; | |
| } | |
| .empty-state-content p { | |
| @apply text-saap-gray-600 mb-6; | |
| } | |
| .empty-action-button { | |
| @apply inline-flex items-center px-4 py-2 bg-saap-primary-600 text-white text-sm font-medium rounded-lg hover:bg-saap-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-saap-primary-500 transition-colors duration-200; | |
| } | |
| .agents-list { | |
| @apply bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden; | |
| } | |
| .list-header { | |
| @apply grid grid-cols-6 gap-4 px-6 py-3 bg-gray-50 border-b border-gray-200 text-xs font-medium text-saap-gray-500 uppercase tracking-wide; | |
| } | |
| .list-row { | |
| @apply grid grid-cols-6 gap-4 px-6 py-4 border-b border-gray-100 hover:bg-gray-50 transition-colors duration-200; | |
| } | |
| .list-row:last-child { | |
| @apply border-b-0; | |
| } | |
| .list-column, .list-cell { | |
| @apply flex items-center; | |
| } | |
| .agent-name-cell { | |
| @apply flex items-center space-x-3; | |
| } | |
| .agent-avatar { | |
| @apply w-8 h-8 rounded-full flex items-center justify-center text-white text-sm font-medium; | |
| } | |
| .agent-type-badge { | |
| @apply px-2 py-1 text-xs font-medium bg-gray-100 text-saap-gray-700 rounded-full; | |
| } | |
| .status-badge { | |
| @apply px-2 py-1 text-xs font-medium rounded-full; | |
| } | |
| .status-active { | |
| @apply bg-green-100 text-green-800; | |
| } | |
| .status-inactive { | |
| @apply bg-gray-100 text-saap-gray-800; | |
| } | |
| .status-starting { | |
| @apply bg-yellow-100 text-yellow-800; | |
| } | |
| .action-buttons { | |
| @apply flex items-center space-x-2; | |
| } | |
| .action-btn { | |
| @apply p-1.5 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 transition-colors duration-200; | |
| } | |
| .start-btn { | |
| @apply hover:border-green-300 focus:ring-green-500; | |
| } | |
| .start-btn .action-icon { | |
| @apply w-4 h-4 text-green-600; | |
| } | |
| .stop-btn { | |
| @apply hover:border-red-300 focus:ring-red-500; | |
| } | |
| .stop-btn .action-icon { | |
| @apply w-4 h-4 text-red-600; | |
| } | |
| .chat-btn { | |
| @apply hover:border-saap-primary-300 focus:ring-saap-primary-500; | |
| } | |
| .chat-btn .action-icon { | |
| @apply w-4 h-4 text-saap-primary-600; | |
| } | |
| .action-icon { | |
| @apply w-4 h-4; | |
| } | |
| </style> |