Hwandji's picture
feat: initial HuggingFace Space deployment
4343907
raw
history blame
21.1 kB
<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>