Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="saap-dashboard"> | |
| <!-- Header --> | |
| <header class="dashboard-header"> | |
| <div class="header-content"> | |
| <div class="header-left"> | |
| <h1 class="dashboard-title">SAAP Dashboard</h1> | |
| <p class="dashboard-subtitle">satware AI Autonomous Agent Platform</p> | |
| </div> | |
| <div class="header-right"> | |
| <div class="status-indicators"> | |
| <div class="status-indicator"> | |
| <div class="status-dot" :class="connectionStatus"></div> | |
| <span class="status-text">{{ connectionText }}</span> | |
| </div> | |
| <div class="stats-summary"> | |
| <span class="stat-item">{{ agents.length }} Agents</span> | |
| <span class="stat-item">{{ activeAgents }} Active</span> | |
| </div> | |
| </div> | |
| <!-- 🚀 NEW: Multi-Agent Communication Button --> | |
| <button class="multi-agent-btn" @click="showMultiAgentModal = true"> | |
| <UsersIcon class="btn-icon" /> | |
| Multi-Agent Chat | |
| </button> | |
| <button class="add-agent-btn" @click="showAddModal = true"> | |
| <PlusIcon class="btn-icon" /> | |
| Add Agent | |
| </button> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main class="dashboard-main"> | |
| <div class="dashboard-grid"> | |
| <!-- Agent Cards Section --> | |
| <section class="agents-section"> | |
| <div class="section-header"> | |
| <h2 class="section-title">Active Agents</h2> | |
| <div class="section-controls"> | |
| <!-- View Toggle --> | |
| <div class="view-toggle"> | |
| <button | |
| class="view-btn" | |
| :class="{ active: viewMode === 'grid' }" | |
| @click="viewMode = 'grid'" | |
| title="Grid View" | |
| > | |
| <GridIcon class="btn-icon" /> | |
| </button> | |
| <button | |
| class="view-btn" | |
| :class="{ active: viewMode === 'table' }" | |
| @click="viewMode = 'table'" | |
| title="Table View" | |
| > | |
| <ListIcon class="btn-icon" /> | |
| </button> | |
| </div> | |
| <!-- Agent Filters --> | |
| <div class="agent-filters"> | |
| <button | |
| v-for="filter in filters" | |
| :key="filter.key" | |
| class="filter-btn" | |
| :class="{ active: activeFilter === filter.key }" | |
| @click="activeFilter = filter.key" | |
| > | |
| {{ filter.label }} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div v-if="filteredAgents.length === 0" class="empty-state"> | |
| <div class="empty-icon">🤖</div> | |
| <h3 class="empty-title">No Agents Found</h3> | |
| <p class="empty-description"> | |
| {{ activeFilter === 'all' ? 'Start by adding your first agent.' : `No ${activeFilter} agents available.` }} | |
| </p> | |
| <button class="empty-action-btn" @click="showAddModal = true"> | |
| Add Your First Agent | |
| </button> | |
| </div> | |
| <!-- Grid View --> | |
| <div v-else-if="viewMode === 'grid'" class="agents-grid"> | |
| <div | |
| v-for="agent in filteredAgents" | |
| :key="getAgentId(agent)" | |
| class="agent-card" | |
| :style="{ '--agent-color': getAgentColor(agent) }" | |
| > | |
| <div class="agent-header"> | |
| <div class="agent-avatar"> | |
| <component :is="getAgentIcon(agent.type)" class="agent-icon" /> | |
| </div> | |
| <div class="agent-info"> | |
| <h3 class="agent-name">{{ agent.name || 'Unknown Agent' }}</h3> | |
| <p class="agent-description">{{ agent.description || 'No description available' }}</p> | |
| </div> | |
| <div class="agent-status"> | |
| <div class="status-badge" :class="getStatusClass(agent.status)"> | |
| {{ agent.status || 'unknown' }} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="agent-stats"> | |
| <div class="stat-item"> | |
| <span class="stat-label">Messages</span> | |
| <span class="stat-value">{{ agent.message_count || 0 }}</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="stat-label">Response Time</span> | |
| <span class="stat-value">{{ formatResponseTime(agent.avg_response_time) }}</span> | |
| </div> | |
| <div class="stat-item"> | |
| <span class="stat-label">Uptime</span> | |
| <span class="stat-value">{{ formatUptime(agent.uptime) }}</span> | |
| </div> | |
| </div> | |
| <div class="agent-capabilities"> | |
| <span | |
| v-for="capability in getAgentCapabilities(agent)?.slice(0, 3)" | |
| :key="capability" | |
| class="capability-tag" | |
| > | |
| {{ capability }} | |
| </span> | |
| <span v-if="getAgentCapabilities(agent)?.length > 3" class="capability-more"> | |
| +{{ getAgentCapabilities(agent).length - 3 }} more | |
| </span> | |
| </div> | |
| <div class="agent-actions"> | |
| <button | |
| class="action-btn primary h-10 min-h-10 flex items-center justify-center" | |
| @click="openChatModal(agent)" | |
| > | |
| <MessageCircleIcon class="btn-icon" /> | |
| Chat | |
| </button> | |
| <button | |
| class="action-btn secondary h-10 min-h-10 flex items-center justify-center" | |
| @click="viewAgentDetails(agent)" | |
| > | |
| <SettingsIcon class="btn-icon" /> | |
| Settings | |
| </button> | |
| <button | |
| class="action-btn h-10 min-h-10 flex items-center justify-center" | |
| :class="agent.status === 'active' ? 'danger' : 'success'" | |
| @click="toggleAgent(agent)" | |
| > | |
| <component | |
| :is="agent.status === 'active' ? PauseIcon : PlayIcon" | |
| class="btn-icon" | |
| /> | |
| {{ agent.status === 'active' ? 'Stop' : 'Start' }} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Table View --> | |
| <div v-else class="agents-table-container"> | |
| <table class="agents-table"> | |
| <thead> | |
| <tr> | |
| <th class="table-th">Agent</th> | |
| <th class="table-th">Type</th> | |
| <th class="table-th">Status</th> | |
| <th class="table-th">Messages</th> | |
| <th class="table-th">Response Time</th> | |
| <th class="table-th">Uptime</th> | |
| <th class="table-th">Capabilities</th> | |
| <th class="table-th">Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr | |
| v-for="agent in filteredAgents" | |
| :key="getAgentId(agent)" | |
| class="table-row" | |
| > | |
| <!-- Agent Info --> | |
| <td class="table-td"> | |
| <div class="table-agent-info"> | |
| <div | |
| class="table-agent-avatar" | |
| :style="{ backgroundColor: agent.color || '#6B7280' }" | |
| > | |
| <component :is="getAgentIcon(agent.type)" class="table-agent-icon" /> | |
| </div> | |
| <div class="table-agent-details"> | |
| <div class="table-agent-name">{{ agent.name || 'Unknown Agent' }}</div> | |
| <div class="table-agent-description">{{ agent.description || 'No description' }}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <!-- Type --> | |
| <td class="table-td"> | |
| <span class="table-type-badge" :class="`type-${agent.type}`"> | |
| {{ agent.type || 'unknown' }} | |
| </span> | |
| </td> | |
| <!-- Status --> | |
| <td class="table-td"> | |
| <div class="status-badge" :class="getStatusClass(agent.status)"> | |
| {{ agent.status || 'unknown' }} | |
| </div> | |
| </td> | |
| <!-- Messages --> | |
| <td class="table-td">{{ agent.message_count || 0 }}</td> | |
| <!-- Response Time --> | |
| <td class="table-td">{{ formatResponseTime(agent.avg_response_time) }}</td> | |
| <!-- Uptime --> | |
| <td class="table-td">{{ formatUptime(agent.uptime) }}</td> | |
| <!-- Capabilities --> | |
| <td class="table-td"> | |
| <div class="table-capabilities"> | |
| <span | |
| v-for="capability in getAgentCapabilities(agent)?.slice(0, 2)" | |
| :key="capability" | |
| class="table-capability-tag" | |
| > | |
| {{ capability }} | |
| </span> | |
| <span v-if="getAgentCapabilities(agent)?.length > 2" class="capability-more"> | |
| +{{ getAgentCapabilities(agent).length - 2 }} | |
| </span> | |
| </div> | |
| </td> | |
| <!-- Actions --> | |
| <td class="table-td"> | |
| <div class="table-actions"> | |
| <button | |
| class="table-action-btn primary h-10 w-10 min-h-10 min-w-10 flex items-center justify-center" | |
| @click="openChatModal(agent)" | |
| title="Chat" | |
| > | |
| <MessageCircleIcon class="table-btn-icon" /> | |
| </button> | |
| <button | |
| class="table-action-btn secondary h-10 w-10 min-h-10 min-w-10 flex items-center justify-center" | |
| @click="viewAgentDetails(agent)" | |
| title="Settings" | |
| > | |
| <SettingsIcon class="table-btn-icon" /> | |
| </button> | |
| <button | |
| class="table-action-btn h-10 w-10 min-h-10 min-w-10 flex items-center justify-center" | |
| :class="agent.status === 'active' ? 'danger' : 'success'" | |
| @click="toggleAgent(agent)" | |
| :title="agent.status === 'active' ? 'Stop' : 'Start'" | |
| > | |
| <component | |
| :is="agent.status === 'active' ? PauseIcon : PlayIcon" | |
| class="table-btn-icon" | |
| /> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| <!-- Real-time Communication Panel --> | |
| <aside class="communication-panel"> | |
| <div class="panel-header"> | |
| <h3 class="panel-title">Live Communication</h3> | |
| <div class="panel-controls"> | |
| <button class="control-btn" @click="clearMessages"> | |
| <TrashIcon class="btn-icon" /> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="messages-container" ref="messagesContainer"> | |
| <div v-if="messages.length === 0" class="messages-empty"> | |
| <MessageSquareIcon class="empty-icon" /> | |
| <p class="empty-text">No messages yet</p> | |
| <p class="empty-subtext">Agent communications will appear here</p> | |
| </div> | |
| <div v-else class="messages-list"> | |
| <div | |
| v-for="message in messages" | |
| :key="message.id" | |
| class="message-item" | |
| :class="getMessageClass(message)" | |
| > | |
| <div class="message-icon-wrapper"> | |
| <component :is="getMessageIcon(message.type)" class="message-icon" /> | |
| </div> | |
| <div class="message-body"> | |
| <div class="message-header"> | |
| <span class="message-agent">{{ formatMessageAgent(message) }}</span> | |
| <span class="message-time">{{ formatTime(message.timestamp) }}</span> | |
| </div> | |
| <div class="message-content">{{ formatMessageContent(message) }}</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="quick-actions"> | |
| <button | |
| v-for="action in quickActions" | |
| :key="action.id" | |
| class="quick-action-btn" | |
| @click="sendQuickMessage(action)" | |
| > | |
| {{ action.label }} | |
| </button> | |
| </div> | |
| </aside> | |
| </div> | |
| </main> | |
| <!-- Modals --> | |
| <AddAgentModal | |
| v-if="showAddModal" | |
| @close="showAddModal = false" | |
| @agent-created="handleAgentCreated" | |
| /> | |
| <ChatModal | |
| v-if="showChatModal && selectedAgent" | |
| :agent="selectedAgent" | |
| @close="showChatModal = false" | |
| /> | |
| <!-- Agent Settings Modal --> | |
| <AgentSettingsModal | |
| v-if="showSettingsModal && selectedAgentForSettings" | |
| :agent="selectedAgentForSettings" | |
| @close="closeSettingsModal" | |
| @agent-updated="handleAgentUpdated" | |
| /> | |
| <!-- 🚀 NEW: Multi-Agent Communication Modal --> | |
| <MultiAgentChatModal | |
| v-if="showMultiAgentModal" | |
| :isVisible="showMultiAgentModal" | |
| @close="showMultiAgentModal = false" | |
| /> | |
| </div> | |
| </template> | |
| <script> | |
| import { ref, reactive, computed, onMounted, onUnmounted, nextTick } from 'vue' | |
| import { | |
| PlusIcon, MessageCircleIcon, SettingsIcon, | |
| PauseIcon, PlayIcon, TrashIcon, MessageSquareIcon, | |
| UsersIcon, CodeIcon, HeartIcon, ScaleIcon, | |
| TrendingUpIcon, ServerIcon, BrainIcon, GridIcon, ListIcon | |
| } from 'lucide-vue-next' | |
| import AddAgentModal from './modals/AddAgentModal.vue' | |
| import ChatModal from './modals/ChatModal.vue' | |
| import AgentSettingsModal from './modals/AgentSettingsModal.vue' | |
| import MultiAgentChatModal from './modals/MultiAgentChatModal.vue' // 🚀 NEW: Multi-Agent Modal | |
| import { saapApi } from '../services/saapApi.js' | |
| export default { | |
| name: 'SaapDashboard', | |
| components: { | |
| AddAgentModal, | |
| ChatModal, | |
| AgentSettingsModal, | |
| MultiAgentChatModal, // 🚀 NEW: Register Multi-Agent Modal | |
| PlusIcon, | |
| MessageCircleIcon, | |
| SettingsIcon, | |
| PauseIcon, | |
| PlayIcon, | |
| TrashIcon, | |
| MessageSquareIcon, | |
| UsersIcon, | |
| CodeIcon, | |
| HeartIcon, | |
| ScaleIcon, | |
| TrendingUpIcon, | |
| ServerIcon, | |
| BrainIcon, | |
| GridIcon, | |
| ListIcon | |
| }, | |
| setup() { | |
| const showAddModal = ref(false) | |
| const showChatModal = ref(false) | |
| const showSettingsModal = ref(false) | |
| const showMultiAgentModal = ref(false) // 🚀 NEW: Multi-Agent Modal state | |
| const selectedAgent = ref(null) | |
| const selectedAgentForSettings = ref(null) | |
| const activeFilter = ref('all') | |
| const viewMode = ref('grid') | |
| const agents = ref([]) | |
| const messages = ref([]) | |
| const socket = ref(null) | |
| const connectionStatus = ref('disconnected') | |
| const messagesContainer = ref(null) | |
| const filters = [ | |
| { key: 'all', label: 'All' }, | |
| { key: 'active', label: 'Active' }, | |
| { key: 'inactive', label: 'Inactive' }, | |
| { key: 'coordinator', label: 'Coordinators' }, | |
| { key: 'developer', label: 'Developers' }, | |
| { key: 'specialist', label: 'Specialists' } | |
| ] | |
| const quickActions = [ | |
| { id: 'status', label: 'System Status' }, | |
| { id: 'performance', label: 'Performance Report' }, | |
| { id: 'agents', label: 'List Agents' }, | |
| { id: 'help', label: 'Help' } | |
| ] | |
| const connectionText = computed(() => { | |
| return connectionStatus.value === 'connected' ? 'Connected' : | |
| connectionStatus.value === 'connecting' ? 'Connecting...' : 'Disconnected' | |
| }) | |
| const activeAgents = computed(() => { | |
| return agents.value.filter(agent => agent.status === 'active').length | |
| }) | |
| const filteredAgents = computed(() => { | |
| if (activeFilter.value === 'all') return agents.value | |
| if (activeFilter.value === 'active' || activeFilter.value === 'inactive') { | |
| return agents.value.filter(agent => agent.status === activeFilter.value) | |
| } | |
| return agents.value.filter(agent => agent.type === activeFilter.value) | |
| }) | |
| // Helper function to get agent ID - handles both id and agent_id | |
| const getAgentId = (agent) => { | |
| return agent.id || agent.agent_id || 'unknown' | |
| } | |
| // Helper function to get agent capabilities | |
| const getAgentCapabilities = (agent) => { | |
| if (agent.capabilities && Array.isArray(agent.capabilities)) { | |
| return agent.capabilities | |
| } | |
| if (agent.capabilities && typeof agent.capabilities === 'string') { | |
| return agent.capabilities.split(',').map(cap => cap.trim()) | |
| } | |
| return [] | |
| } | |
| const getStatusClass = (status) => { | |
| return { | |
| 'active': 'status-active', | |
| 'inactive': 'status-inactive', | |
| 'starting': 'status-starting', | |
| 'stopping': 'status-stopping', | |
| 'error': 'status-error' | |
| }[status] || 'status-unknown' | |
| } | |
| // Fallback colors for agents if not provided by backend | |
| const agentColorMap = { | |
| 'jane_alesi': '#8B5CF6', // Purple | |
| 'john_alesi': '#14B8A6', // Teal | |
| 'lara_alesi': '#EC4899', // Pink | |
| 'theo_alesi': '#F59E0B', // Orange | |
| 'justus_alesi': '#10B981', // Green | |
| 'leon_alesi': '#6366F1', // Indigo | |
| 'luna_alesi': '#8B5CF6' // Purple | |
| } | |
| const getAgentColor = (agent) => { | |
| // Priority: agent.color > agent.appearance?.color > agentColorMap[agent.id] > default | |
| return agent.color || | |
| agent.appearance?.color || | |
| agentColorMap[agent.id] || | |
| agentColorMap[getAgentId(agent)] || | |
| '#6B7280' | |
| } | |
| const getAgentIcon = (type) => { | |
| const iconMap = { | |
| coordinator: UsersIcon, | |
| developer: CodeIcon, | |
| specialist: HeartIcon, | |
| analyst: ScaleIcon, | |
| finance: TrendingUpIcon, | |
| system: ServerIcon, | |
| support: HeartIcon | |
| } | |
| return iconMap[type] || BrainIcon | |
| } | |
| const formatResponseTime = (time) => { | |
| if (!time) return 'N/A' | |
| return time < 1000 ? `${Math.round(time)}ms` : `${(time / 1000).toFixed(1)}s` | |
| } | |
| const formatUptime = (uptime) => { | |
| if (!uptime) return 'N/A' | |
| const hours = Math.floor(uptime / 3600) | |
| const minutes = Math.floor((uptime % 3600) / 60) | |
| return `${hours}h ${minutes}m` | |
| } | |
| const formatTime = (timestamp) => { | |
| return new Date(timestamp).toLocaleTimeString('de-DE', { | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| second: '2-digit' | |
| }) | |
| } | |
| const getMessageClass = (message) => { | |
| const type = message.type || 'info' | |
| return `message-type-${type}` | |
| } | |
| const getMessageIcon = (type) => { | |
| const iconMap = { | |
| 'agent': MessageCircleIcon, | |
| 'system': ServerIcon, | |
| 'info': MessageSquareIcon, | |
| 'error': MessageSquareIcon, | |
| 'success': MessageSquareIcon | |
| } | |
| return iconMap[type] || MessageSquareIcon | |
| } | |
| const formatMessageAgent = (message) => { | |
| return message.from || message.agent_name || message.agent_id || 'System' | |
| } | |
| const formatMessageContent = (message) => { | |
| const content = message.content || message.message || '' | |
| // Filter out technical Echo messages | |
| if (content.startsWith('Echo:') || content.startsWith('{"action"')) { | |
| return '...' | |
| } | |
| // Truncate very long messages | |
| if (content.length > 200) { | |
| return content.substring(0, 200) + '...' | |
| } | |
| return content | |
| } | |
| // Load agents with proper error handling | |
| const loadAgents = async () => { | |
| try { | |
| console.log('🔄 Loading agents from backend...') | |
| const response = await saapApi.getAgents() | |
| console.log('✅ Backend response:', response) | |
| // Handle both possible response formats | |
| if (response.agents && Array.isArray(response.agents)) { | |
| agents.value = response.agents | |
| console.log(`✅ Loaded ${response.agents.length} agents`) | |
| } else if (Array.isArray(response)) { | |
| agents.value = response | |
| console.log(`✅ Loaded ${response.length} agents (direct array)`) | |
| } else { | |
| console.error('❌ Unexpected response format:', response) | |
| agents.value = [] | |
| } | |
| } catch (error) { | |
| console.error('❌ Failed to load agents:', error) | |
| agents.value = [] | |
| } | |
| } | |
| // Handle agent creation response | |
| const handleAgentCreated = async (agentOrResponse) => { | |
| console.log('🎉 Agent created:', agentOrResponse) | |
| // Handle different response formats | |
| if (agentOrResponse.agent) { | |
| agents.value.push(agentOrResponse.agent) | |
| } else if (agentOrResponse.id || agentOrResponse.agent_id) { | |
| agents.value.push(agentOrResponse) | |
| } | |
| showAddModal.value = false | |
| // Refresh agents list to get latest data | |
| await loadAgents() | |
| } | |
| // Handle agent updated response | |
| const handleAgentUpdated = async (updatedAgent) => { | |
| console.log('🎉 Agent updated:', updatedAgent) | |
| // Update the agent in the list | |
| const agentId = getAgentId(updatedAgent) | |
| const index = agents.value.findIndex(agent => getAgentId(agent) === agentId) | |
| if (index !== -1) { | |
| agents.value[index] = updatedAgent | |
| console.log(`✅ Updated agent ${agentId} in list`) | |
| } | |
| // Close settings modal | |
| closeSettingsModal() | |
| // Optionally refresh the full list to ensure consistency | |
| setTimeout(() => loadAgents(), 500) | |
| } | |
| const openChatModal = (agent) => { | |
| selectedAgent.value = agent | |
| showChatModal.value = true | |
| } | |
| // Implement viewAgentDetails function to open settings modal | |
| const viewAgentDetails = (agent) => { | |
| console.log('🔧 Opening settings for agent:', agent) | |
| selectedAgentForSettings.value = agent | |
| showSettingsModal.value = true | |
| } | |
| // Close settings modal | |
| const closeSettingsModal = () => { | |
| showSettingsModal.value = false | |
| selectedAgentForSettings.value = null | |
| } | |
| // Toggle agent with proper ID handling | |
| const toggleAgent = async (agent) => { | |
| const agentId = getAgentId(agent) | |
| const action = agent.status === 'active' ? 'stop' : 'start' | |
| console.log(`🔄 ${action} agent:`, agentId, agent) | |
| try { | |
| if (agentId === 'unknown') { | |
| console.error('❌ Cannot toggle agent: ID is unknown') | |
| return | |
| } | |
| // Use saapApi service instead of direct fetch | |
| if (action === 'start') { | |
| await saapApi.startAgent(agentId) | |
| } else { | |
| await saapApi.stopAgent(agentId) | |
| } | |
| console.log(`✅ Agent ${action} request sent`) | |
| // Refresh agent list | |
| await loadAgents() | |
| } catch (error) { | |
| console.error(`❌ Failed to ${action} agent:`, error) | |
| } | |
| } | |
| const clearMessages = () => { | |
| messages.value = [] | |
| } | |
| const sendQuickMessage = (action) => { | |
| if (socket.value && socket.value.readyState === WebSocket.OPEN) { | |
| socket.value.send(JSON.stringify({ action: action.id, type: 'quick_action' })) | |
| } | |
| } | |
| const scrollToBottom = () => { | |
| nextTick(() => { | |
| if (messagesContainer.value) { | |
| messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight | |
| } | |
| }) | |
| } | |
| // Native WebSocket implementation instead of Socket.IO | |
| const initWebSocket = () => { | |
| console.log('🔌 Connecting to native WebSocket...') | |
| connectionStatus.value = 'connecting' | |
| socket.value = new WebSocket('ws://localhost:8000/ws') | |
| socket.value.onopen = () => { | |
| connectionStatus.value = 'connected' | |
| console.log('✅ Native WebSocket connected') | |
| } | |
| socket.value.onclose = () => { | |
| connectionStatus.value = 'disconnected' | |
| console.log('❌ Native WebSocket disconnected') | |
| // Auto-reconnect after 5 seconds | |
| setTimeout(() => { | |
| if (connectionStatus.value === 'disconnected') { | |
| console.log('🔄 Attempting to reconnect WebSocket...') | |
| initWebSocket() | |
| } | |
| }, 5000) | |
| } | |
| socket.value.onerror = (error) => { | |
| console.error('❌ WebSocket error:', error) | |
| connectionStatus.value = 'disconnected' | |
| } | |
| socket.value.onmessage = (event) => { | |
| try { | |
| const data = JSON.parse(event.data) | |
| console.log('📨 WebSocket message received:', data) | |
| // Handle different message types | |
| if (data.type === 'agent_message') { | |
| messages.value.push({ | |
| id: Date.now(), | |
| type: 'agent', | |
| ...data, | |
| timestamp: new Date() | |
| }) | |
| scrollToBottom() | |
| } else if (data.type === 'system_message') { | |
| messages.value.push({ | |
| id: Date.now(), | |
| type: 'system', | |
| content: data.message || data.content, | |
| timestamp: new Date() | |
| }) | |
| scrollToBottom() | |
| } else if (data.type === 'agent_status_update') { | |
| const agent = agents.value.find(a => | |
| getAgentId(a) === data.agent_id || | |
| getAgentId(a) === data.id | |
| ) | |
| if (agent) { | |
| Object.assign(agent, data) | |
| } | |
| } else { | |
| // Generic message | |
| messages.value.push({ | |
| id: Date.now(), | |
| type: 'info', | |
| content: typeof data === 'string' ? data : JSON.stringify(data), | |
| timestamp: new Date() | |
| }) | |
| scrollToBottom() | |
| } | |
| } catch (error) { | |
| console.error('❌ Failed to parse WebSocket message:', error) | |
| // Handle plain text messages | |
| messages.value.push({ | |
| id: Date.now(), | |
| type: 'info', | |
| content: event.data, | |
| timestamp: new Date() | |
| }) | |
| scrollToBottom() | |
| } | |
| } | |
| } | |
| onMounted(() => { | |
| console.log('🚀 SaapDashboard mounted - initializing...') | |
| loadAgents() | |
| initWebSocket() | |
| // Auto-refresh agents every 30 seconds | |
| const refreshInterval = setInterval(loadAgents, 30000) | |
| onUnmounted(() => { | |
| clearInterval(refreshInterval) | |
| if (socket.value) { | |
| socket.value.close() | |
| } | |
| }) | |
| }) | |
| return { | |
| showAddModal, | |
| showChatModal, | |
| showSettingsModal, | |
| showMultiAgentModal, // 🚀 NEW: Multi-Agent Modal state | |
| selectedAgent, | |
| selectedAgentForSettings, | |
| activeFilter, | |
| viewMode, | |
| agents, | |
| messages, | |
| connectionStatus, | |
| connectionText, | |
| activeAgents, | |
| filteredAgents, | |
| filters, | |
| quickActions, | |
| messagesContainer, | |
| getAgentId, | |
| getAgentCapabilities, | |
| getAgentColor, | |
| getStatusClass, | |
| getAgentIcon, | |
| getMessageClass, | |
| getMessageIcon, | |
| formatMessageAgent, | |
| formatMessageContent, | |
| formatResponseTime, | |
| formatUptime, | |
| formatTime, | |
| handleAgentCreated, | |
| handleAgentUpdated, | |
| openChatModal, | |
| viewAgentDetails, | |
| closeSettingsModal, | |
| toggleAgent, | |
| clearMessages, | |
| sendQuickMessage | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| .saap-dashboard { | |
| @apply min-h-screen bg-gray-50; | |
| } | |
| .dashboard-header { | |
| @apply bg-white border-b border-gray-200 sticky top-0 z-40; | |
| } | |
| .header-content { | |
| @apply max-w-7xl mx-auto px-6 py-4 flex items-center justify-between; | |
| } | |
| .header-left { | |
| @apply flex-1; | |
| } | |
| .dashboard-title { | |
| @apply text-2xl font-bold text-gray-900; | |
| } | |
| .dashboard-subtitle { | |
| @apply text-sm text-gray-600 mt-1; | |
| } | |
| .header-right { | |
| @apply flex items-center space-x-4; | |
| } | |
| .status-indicators { | |
| @apply flex items-center space-x-4; | |
| } | |
| .status-indicator { | |
| @apply flex items-center space-x-2; | |
| } | |
| .status-dot { | |
| @apply w-3 h-3 rounded-full; | |
| } | |
| .status-dot.connected { | |
| @apply bg-green-500; | |
| } | |
| .status-dot.connecting { | |
| @apply bg-yellow-500 animate-pulse; | |
| } | |
| .status-dot.disconnected { | |
| @apply bg-red-500; | |
| } | |
| .status-text { | |
| @apply text-sm font-medium text-gray-700; | |
| } | |
| .stats-summary { | |
| @apply flex items-center space-x-3 text-sm text-gray-600; | |
| } | |
| .stat-item { | |
| @apply font-medium; | |
| } | |
| /* 🚀 NEW: Multi-Agent Communication Button Styles */ | |
| .multi-agent-btn { | |
| @apply inline-flex items-center px-4 py-2 bg-gradient-to-r from-purple-600 to-indigo-600 text-white font-medium rounded-lg hover:from-purple-700 hover:to-indigo-700 transition-all duration-200 shadow-md; | |
| } | |
| .add-agent-btn { | |
| @apply inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200; | |
| } | |
| .btn-icon { | |
| @apply w-4 h-4 mr-2; | |
| } | |
| .dashboard-main { | |
| @apply max-w-7xl mx-auto px-6 py-6; | |
| } | |
| .dashboard-grid { | |
| @apply grid grid-cols-1 lg:grid-cols-4 gap-6; | |
| } | |
| .agents-section { | |
| @apply lg:col-span-3 space-y-6; | |
| } | |
| .section-header { | |
| @apply flex items-center justify-between flex-wrap gap-4; | |
| } | |
| .section-title { | |
| @apply text-lg font-semibold text-gray-900; | |
| } | |
| .section-controls { | |
| @apply flex items-center space-x-4; | |
| } | |
| .view-toggle { | |
| @apply flex bg-gray-100 rounded-lg p-1; | |
| } | |
| .view-btn { | |
| @apply px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200; | |
| @apply text-gray-600 hover:text-gray-900; | |
| } | |
| .view-btn.active { | |
| @apply bg-white text-gray-900 shadow-sm; | |
| } | |
| .agent-filters { | |
| @apply flex space-x-2; | |
| } | |
| .filter-btn { | |
| @apply px-3 py-1 text-sm font-medium rounded-lg transition-colors duration-200; | |
| @apply text-gray-600 hover:text-gray-900 hover:bg-gray-100; | |
| } | |
| .filter-btn.active { | |
| @apply bg-blue-100 text-blue-700; | |
| } | |
| .empty-state { | |
| @apply text-center py-12; | |
| } | |
| .empty-icon { | |
| @apply text-4xl mb-4; | |
| } | |
| .empty-title { | |
| @apply text-lg font-semibold text-gray-900 mb-2; | |
| } | |
| .empty-description { | |
| @apply text-gray-600 mb-6; | |
| } | |
| .empty-action-btn { | |
| @apply inline-flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors duration-200; | |
| } | |
| /* Grid View Styles */ | |
| .agents-grid { | |
| @apply grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4; | |
| } | |
| .agent-card { | |
| @apply bg-white rounded-lg border border-gray-200 hover:border-gray-300 transition-all duration-200 p-4; | |
| @apply flex flex-col; | |
| height: 360px; | |
| } | |
| .agent-header { | |
| @apply flex items-start space-x-3 mb-4; | |
| flex-shrink: 0; | |
| } | |
| .agent-avatar { | |
| @apply w-12 h-12 rounded-lg flex items-center justify-center; | |
| background: linear-gradient(135deg, var(--agent-color) 0%, color-mix(in srgb, var(--agent-color) 80%, black) 100%); | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); | |
| } | |
| .agent-icon { | |
| @apply w-6 h-6; | |
| color: white; | |
| filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.2)); | |
| } | |
| .agent-info { | |
| @apply flex-1 min-w-0; | |
| } | |
| .agent-name { | |
| @apply font-semibold text-gray-900 truncate; | |
| } | |
| .agent-description { | |
| @apply text-sm text-gray-600 line-clamp-2; | |
| } | |
| .agent-status { | |
| @apply flex-shrink-0; | |
| } | |
| .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-gray-800; | |
| } | |
| .status-starting { | |
| @apply bg-blue-100 text-blue-800; | |
| } | |
| .status-stopping { | |
| @apply bg-orange-100 text-orange-800; | |
| } | |
| .status-error { | |
| @apply bg-red-100 text-red-800; | |
| } | |
| .status-unknown { | |
| @apply bg-gray-100 text-gray-500; | |
| } | |
| .agent-stats { | |
| @apply grid grid-cols-3 gap-3 pt-3 border-t border-gray-100 mb-4; | |
| flex-shrink: 0; | |
| } | |
| .agent-stats .stat-item { | |
| @apply text-center; | |
| } | |
| .stat-label { | |
| @apply block text-xs text-gray-500; | |
| } | |
| .stat-value { | |
| @apply block text-sm font-semibold text-gray-900 mt-1; | |
| } | |
| .agent-capabilities { | |
| @apply flex flex-wrap gap-1 mb-4; | |
| flex: 1 1 auto; | |
| align-content: flex-start; | |
| } | |
| .capability-tag { | |
| @apply inline-flex items-center px-2 py-1 text-xs font-medium rounded; | |
| background-color: color-mix(in srgb, var(--agent-color) 20%, white); | |
| color: var(--agent-color); | |
| } | |
| .capability-more { | |
| @apply text-xs text-gray-500 px-2 py-1; | |
| } | |
| .agent-actions { | |
| @apply flex space-x-2 pt-3 border-t border-gray-100 items-stretch; | |
| flex-shrink: 0; | |
| margin-top: auto; | |
| } | |
| .action-btn { | |
| @apply flex-1 inline-flex items-center justify-center px-3 py-2 text-sm font-medium rounded-lg transition-colors duration-200; | |
| min-height: 2.5rem; | |
| height: 2.5rem; | |
| } | |
| .action-btn.primary { | |
| @apply bg-blue-600 text-white hover:bg-blue-700; | |
| } | |
| .action-btn.secondary { | |
| @apply bg-purple-50 text-purple-600 border border-purple-200 hover:bg-purple-100 hover:border-purple-300; | |
| } | |
| .action-btn.success { | |
| @apply bg-green-600 text-white hover:bg-green-700; | |
| } | |
| .action-btn.danger { | |
| @apply bg-red-600 text-white hover:bg-red-700; | |
| } | |
| /* Table View Styles */ | |
| .agents-table-container { | |
| @apply overflow-x-auto bg-white rounded-lg border border-gray-200; | |
| } | |
| .agents-table { | |
| @apply w-full table-auto; | |
| } | |
| .table-th { | |
| @apply px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider border-b border-gray-200 bg-gray-50; | |
| } | |
| .table-row { | |
| @apply border-b border-gray-200 hover:bg-gray-50 transition-colors duration-150; | |
| } | |
| .table-td { | |
| @apply px-4 py-4 text-sm text-gray-900; | |
| } | |
| .table-agent-info { | |
| @apply flex items-center space-x-3; | |
| } | |
| .table-agent-avatar { | |
| @apply w-10 h-10 rounded-lg flex items-center justify-center text-white flex-shrink-0; | |
| } | |
| .table-agent-icon { | |
| @apply w-5 h-5; | |
| } | |
| .table-agent-details { | |
| @apply min-w-0 flex-1; | |
| } | |
| .table-agent-name { | |
| @apply font-medium text-gray-900 truncate; | |
| } | |
| .table-agent-description { | |
| @apply text-xs text-gray-500 truncate; | |
| } | |
| .table-type-badge { | |
| @apply px-2 py-1 text-xs font-medium rounded-full; | |
| } | |
| .type-coordinator { | |
| @apply bg-purple-100 text-purple-800; | |
| } | |
| .type-developer { | |
| @apply bg-teal-100 text-teal-800; | |
| } | |
| .type-specialist { | |
| @apply bg-pink-100 text-pink-800; | |
| } | |
| .type-analyst { | |
| @apply bg-indigo-100 text-indigo-800; | |
| } | |
| .type-support { | |
| @apply bg-green-100 text-green-800; | |
| } | |
| .table-capabilities { | |
| @apply flex flex-wrap gap-1 max-w-32; | |
| } | |
| .table-capability-tag { | |
| @apply px-2 py-1 text-xs bg-gray-100 text-gray-700 rounded; | |
| } | |
| .table-actions { | |
| @apply flex items-center space-x-2; | |
| } | |
| .table-action-btn { | |
| @apply p-2 rounded-lg transition-colors duration-200; | |
| min-height: 2.5rem; | |
| min-width: 2.5rem; | |
| height: 2.5rem; | |
| width: 2.5rem; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .table-action-btn.primary { | |
| @apply bg-blue-100 text-blue-600 hover:bg-blue-200; | |
| } | |
| .table-action-btn.secondary { | |
| @apply bg-gray-100 text-gray-600 hover:bg-gray-200; | |
| } | |
| .table-action-btn.success { | |
| @apply bg-green-100 text-green-600 hover:bg-green-200; | |
| } | |
| .table-action-btn.danger { | |
| @apply bg-red-100 text-red-600 hover:bg-red-200; | |
| } | |
| .table-btn-icon { | |
| @apply w-4 h-4; | |
| } | |
| /* Communication Panel */ | |
| .communication-panel { | |
| @apply lg:col-span-1 bg-white rounded-lg border border-gray-200 h-fit; | |
| } | |
| .panel-header { | |
| @apply flex items-center justify-between p-4 border-b border-gray-200; | |
| } | |
| .panel-title { | |
| @apply font-semibold text-gray-900; | |
| } | |
| .panel-controls { | |
| @apply flex space-x-2; | |
| } | |
| .control-btn { | |
| @apply p-1 text-gray-400 hover:text-gray-600 transition-colors duration-200; | |
| } | |
| .messages-container { | |
| @apply h-80 overflow-y-auto; | |
| scrollbar-width: thin; | |
| scrollbar-color: rgba(156, 163, 175, 0.3) transparent; | |
| } | |
| .messages-container::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .messages-container::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| .messages-container::-webkit-scrollbar-thumb { | |
| background: rgba(156, 163, 175, 0.3); | |
| border-radius: 3px; | |
| } | |
| .messages-container::-webkit-scrollbar-thumb:hover { | |
| background: rgba(156, 163, 175, 0.5); | |
| } | |
| .messages-empty { | |
| @apply flex flex-col items-center justify-center h-full text-gray-500; | |
| } | |
| .messages-empty .empty-icon { | |
| @apply w-12 h-12 mb-3 text-gray-400; | |
| } | |
| .empty-text { | |
| @apply text-sm font-medium text-gray-600 mb-1; | |
| } | |
| .empty-subtext { | |
| @apply text-xs text-gray-500; | |
| } | |
| .messages-list { | |
| @apply p-3 space-y-2; | |
| } | |
| .message-item { | |
| @apply rounded-lg p-3 transition-all duration-200; | |
| @apply flex items-start space-x-3; | |
| @apply hover:shadow-sm; | |
| } | |
| .message-type-agent { | |
| @apply bg-blue-50 border-l-4 border-blue-400; | |
| } | |
| .message-type-system { | |
| @apply bg-purple-50 border-l-4 border-purple-400; | |
| } | |
| .message-type-info { | |
| @apply bg-gray-50 border-l-4 border-gray-300; | |
| } | |
| .message-type-error { | |
| @apply bg-red-50 border-l-4 border-red-400; | |
| } | |
| .message-type-success { | |
| @apply bg-green-50 border-l-4 border-green-400; | |
| } | |
| .message-icon-wrapper { | |
| @apply flex-shrink-0; | |
| @apply w-8 h-8 rounded-full; | |
| @apply flex items-center justify-center; | |
| } | |
| .message-type-agent .message-icon-wrapper { | |
| @apply bg-blue-100; | |
| } | |
| .message-type-system .message-icon-wrapper { | |
| @apply bg-purple-100; | |
| } | |
| .message-type-info .message-icon-wrapper { | |
| @apply bg-gray-100; | |
| } | |
| .message-type-error .message-icon-wrapper { | |
| @apply bg-red-100; | |
| } | |
| .message-type-success .message-icon-wrapper { | |
| @apply bg-green-100; | |
| } | |
| .message-icon { | |
| @apply w-4 h-4; | |
| } | |
| .message-type-agent .message-icon { | |
| @apply text-blue-600; | |
| } | |
| .message-type-system .message-icon { | |
| @apply text-purple-600; | |
| } | |
| .message-type-info .message-icon { | |
| @apply text-gray-600; | |
| } | |
| .message-type-error .message-icon { | |
| @apply text-red-600; | |
| } | |
| .message-type-success .message-icon { | |
| @apply text-green-600; | |
| } | |
| .message-body { | |
| @apply flex-1 min-w-0; | |
| } | |
| .message-header { | |
| @apply flex items-center justify-between mb-1; | |
| } | |
| .message-agent { | |
| @apply text-xs font-semibold text-gray-700 truncate; | |
| } | |
| .message-time { | |
| @apply text-xs text-gray-500 flex-shrink-0 ml-2; | |
| } | |
| .message-content { | |
| @apply text-sm text-gray-800 leading-relaxed; | |
| word-wrap: break-word; | |
| } | |
| .quick-actions { | |
| @apply p-3 border-t border-gray-200; | |
| @apply grid grid-cols-2 gap-2; | |
| } | |
| .quick-action-btn { | |
| @apply px-3 py-2 text-xs font-medium; | |
| @apply bg-white border border-gray-200 text-gray-700; | |
| @apply hover:bg-gray-50 hover:border-gray-300; | |
| @apply rounded-lg transition-all duration-200; | |
| @apply flex items-center justify-center; | |
| } | |
| .quick-action-btn:hover { | |
| @apply shadow-sm; | |
| } | |
| /* Responsive Design */ | |
| @media (max-width: 768px) { | |
| .dashboard-grid { | |
| @apply grid-cols-1; | |
| } | |
| .header-content { | |
| @apply flex-col space-y-4 items-start; | |
| } | |
| .header-right { | |
| @apply w-full justify-between; | |
| } | |
| .agents-grid { | |
| @apply grid-cols-1; | |
| } | |
| .section-header { | |
| @apply flex-col items-start space-y-4; | |
| } | |
| .section-controls { | |
| @apply w-full justify-between; | |
| } | |
| .agents-table-container { | |
| @apply text-sm; | |
| } | |
| .table-th, | |
| .table-td { | |
| @apply px-2 py-2; | |
| } | |
| .table-agent-details { | |
| @apply hidden; | |
| } | |
| /* Mobile: Stack buttons vertically */ | |
| .header-right { | |
| @apply flex-col space-y-2 space-x-0 w-full; | |
| } | |
| .multi-agent-btn, | |
| .add-agent-btn { | |
| @apply w-full justify-center; | |
| } | |
| } | |
| </style> | |