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