Spaces:
Sleeping
Sleeping
| <template> | |
| <SaapCard | |
| :hoverable="!disabled" | |
| class="agent-status-card" | |
| :class="cardClasses" | |
| > | |
| <template #header> | |
| <div class="agent-status-card__header"> | |
| <div class="agent-status-card__info"> | |
| <div class="agent-status-card__avatar"> | |
| <div | |
| class="agent-status-card__avatar-circle" | |
| :style="{ backgroundColor: agent.color }" | |
| > | |
| <component | |
| v-if="avatarIcon" | |
| :is="avatarIcon" | |
| class="agent-status-card__avatar-icon" | |
| /> | |
| <span v-else class="agent-status-card__avatar-text"> | |
| {{ agent.name.charAt(0) }} | |
| </span> | |
| </div> | |
| <SaapBadge | |
| :variant="statusVariant" | |
| size="sm" | |
| dot | |
| class="agent-status-card__status-badge" | |
| > | |
| {{ formatStatus(agent.status) }} | |
| </SaapBadge> | |
| </div> | |
| <div class="agent-status-card__details"> | |
| <h3 class="agent-status-card__name">{{ agent.name }}</h3> | |
| <p class="agent-status-card__description">{{ agent.description }}</p> | |
| <div class="agent-status-card__meta"> | |
| <span class="agent-status-card__type">{{ formatAgentType(agent.type) }}</span> | |
| <span class="agent-status-card__model">{{ agent.modelName }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="agent-status-card__actions" v-if="!disabled"> | |
| <SaapButton | |
| v-if="agent.status === 'inactive'" | |
| variant="primary" | |
| size="sm" | |
| icon="play" | |
| :loading="isStarting" | |
| @click="startAgent" | |
| :aria-label="`Start ${agent.name}`" | |
| > | |
| Start | |
| </SaapButton> | |
| <SaapButton | |
| v-else-if="['active', 'processing', 'idle'].includes(agent.status)" | |
| variant="secondary" | |
| size="sm" | |
| icon="pause" | |
| :loading="isStopping" | |
| @click="stopAgent" | |
| :aria-label="`Stop ${agent.name}`" | |
| > | |
| Stop | |
| </SaapButton> | |
| <SaapButton | |
| v-else-if="agent.status === 'error'" | |
| variant="warning" | |
| size="sm" | |
| icon="refresh-cw" | |
| :loading="isRestarting" | |
| @click="restartAgent" | |
| :aria-label="`Restart ${agent.name}`" | |
| > | |
| Restart | |
| </SaapButton> | |
| <SaapButton | |
| variant="ghost" | |
| size="sm" | |
| icon="settings" | |
| @click="$emit('configure', agent)" | |
| :aria-label="`Configure ${agent.name}`" | |
| /> | |
| </div> | |
| </div> | |
| </template> | |
| <!-- Metrics Grid --> | |
| <div class="agent-status-card__metrics"> | |
| <div class="agent-status-card__metric"> | |
| <div class="agent-status-card__metric-icon"> | |
| <MessageSquareIcon /> | |
| </div> | |
| <div class="agent-status-card__metric-content"> | |
| <div class="agent-status-card__metric-value">{{ formatNumber(agent.messageCount) }}</div> | |
| <div class="agent-status-card__metric-label">Messages</div> | |
| </div> | |
| </div> | |
| <div class="agent-status-card__metric"> | |
| <div class="agent-status-card__metric-icon"> | |
| <ZapIcon /> | |
| </div> | |
| <div class="agent-status-card__metric-content"> | |
| <div class="agent-status-card__metric-value">{{ Math.round(agent.successRate) }}%</div> | |
| <div class="agent-status-card__metric-label">Success Rate</div> | |
| </div> | |
| </div> | |
| <div class="agent-status-card__metric"> | |
| <div class="agent-status-card__metric-icon"> | |
| <ClockIcon /> | |
| </div> | |
| <div class="agent-status-card__metric-content"> | |
| <div class="agent-status-card__metric-value">{{ formatDuration(agent.avgResponseTime) }}</div> | |
| <div class="agent-status-card__metric-label">Avg Response</div> | |
| </div> | |
| </div> | |
| <div class="agent-status-card__metric"> | |
| <div class="agent-status-card__metric-icon"> | |
| <ActivityIcon /> | |
| </div> | |
| <div class="agent-status-card__metric-content"> | |
| <div class="agent-status-card__metric-value">{{ formatMemory(agent.memoryUsage) }}</div> | |
| <div class="agent-status-card__metric-label">Memory</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Capabilities --> | |
| <div v-if="agent.capabilities.length" class="agent-status-card__capabilities"> | |
| <h4 class="agent-status-card__capabilities-title">Capabilities</h4> | |
| <div class="agent-status-card__capabilities-list"> | |
| <SaapBadge | |
| v-for="capability in agent.capabilities.slice(0, 3)" | |
| :key="capability" | |
| variant="neutral" | |
| size="sm" | |
| > | |
| {{ capability }} | |
| </SaapBadge> | |
| <SaapBadge | |
| v-if="agent.capabilities.length > 3" | |
| variant="neutral" | |
| size="sm" | |
| > | |
| +{{ agent.capabilities.length - 3 }} more | |
| </SaapBadge> | |
| </div> | |
| </div> | |
| <template #footer> | |
| <div class="agent-status-card__footer"> | |
| <div class="agent-status-card__last-activity"> | |
| <span class="agent-status-card__last-activity-label">Last activity:</span> | |
| <time | |
| class="agent-status-card__last-activity-time" | |
| :datetime="agent.lastActivity" | |
| > | |
| {{ formatRelativeTime(agent.lastActivity) }} | |
| </time> | |
| </div> | |
| <div class="agent-status-card__uptime" v-if="isOnline"> | |
| <div class="agent-status-card__online-indicator" /> | |
| Online | |
| </div> | |
| </div> | |
| </template> | |
| </SaapCard> | |
| </template> | |
| <script setup lang="ts"> | |
| import { computed, defineEmits, defineProps } from 'vue' | |
| import { | |
| MessageSquareIcon, | |
| ZapIcon, | |
| ClockIcon, | |
| ActivityIcon, | |
| BrainIcon, | |
| CodeIcon, | |
| HeartHandshakeIcon, | |
| ScaleIcon | |
| } from 'lucide-vue-next' | |
| import SaapCard from '@/components/base/SaapCard.vue' | |
| import SaapButton from '@/components/base/SaapButton.vue' | |
| import SaapBadge from '@/components/base/SaapBadge.vue' | |
| import { useAgentStatus } from '@/composables/useAgentStatus' | |
| import type { Agent } from '@/types' | |
| interface Props { | |
| agent: Agent | |
| disabled?: boolean | |
| } | |
| const props = defineProps<Props>() | |
| // Emits | |
| const emit = defineEmits<{ | |
| start: [agent: Agent] | |
| stop: [agent: Agent] | |
| restart: [agent: Agent] | |
| configure: [agent: Agent] | |
| }>() | |
| // Composables | |
| const { | |
| isStarting, | |
| isStopping, | |
| isRestarting, | |
| startAgent: start, | |
| stopAgent: stop, | |
| restartAgent: restart | |
| } = useAgentStatus(props.agent) | |
| // Computed | |
| const cardClasses = computed(() => [ | |
| `agent-status-card--${props.agent.status}`, | |
| { | |
| 'agent-status-card--disabled': props.disabled, | |
| 'agent-status-card--online': isOnline.value | |
| } | |
| ]) | |
| const statusVariant = computed(() => { | |
| const variantMap = { | |
| active: 'success', | |
| inactive: 'neutral', | |
| processing: 'warning', | |
| error: 'error', | |
| idle: 'info' | |
| } as const | |
| return variantMap[props.agent.status] || 'neutral' | |
| }) | |
| const isOnline = computed(() => props.agent.isOnline) | |
| const avatarIcon = computed(() => { | |
| const iconMap = { | |
| 'jane': BrainIcon, | |
| 'john': CodeIcon, | |
| 'lara': HeartHandshakeIcon, | |
| 'justus': ScaleIcon | |
| } as Record<string, any> | |
| const agentKey = props.agent.name.toLowerCase().split(' ')[0] | |
| return iconMap[agentKey] || null | |
| }) | |
| // Methods | |
| function formatStatus(status: string): string { | |
| const statusMap = { | |
| active: 'Active', | |
| inactive: 'Inactive', | |
| processing: 'Processing', | |
| error: 'Error', | |
| idle: 'Idle' | |
| } as Record<string, string> | |
| return statusMap[status] || status | |
| } | |
| function formatAgentType(type: string): string { | |
| const typeMap = { | |
| generalist: 'Generalist', | |
| specialist: 'Specialist', | |
| coordinator: 'Coordinator' | |
| } as Record<string, string> | |
| return typeMap[type] || type | |
| } | |
| function formatNumber(num: number): string { | |
| if (num >= 1000) { | |
| return `${(num / 1000).toFixed(1)}k` | |
| } | |
| return num.toString() | |
| } | |
| function formatDuration(ms: number): string { | |
| if (ms < 1000) { | |
| return `${ms}ms` | |
| } | |
| return `${(ms / 1000).toFixed(1)}s` | |
| } | |
| function formatMemory(bytes: number): string { | |
| if (bytes >= 1024 * 1024 * 1024) { | |
| return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB` | |
| } | |
| if (bytes >= 1024 * 1024) { | |
| return `${(bytes / (1024 * 1024)).toFixed(1)}MB` | |
| } | |
| return `${(bytes / 1024).toFixed(1)}KB` | |
| } | |
| function formatRelativeTime(timestamp: string): string { | |
| const date = new Date(timestamp) | |
| const now = new Date() | |
| const diff = now.getTime() - date.getTime() | |
| if (diff < 60 * 1000) { | |
| return 'Just now' | |
| } | |
| if (diff < 60 * 60 * 1000) { | |
| const minutes = Math.floor(diff / (60 * 1000)) | |
| return `${minutes}m ago` | |
| } | |
| if (diff < 24 * 60 * 60 * 1000) { | |
| const hours = Math.floor(diff / (60 * 60 * 1000)) | |
| return `${hours}h ago` | |
| } | |
| return date.toLocaleDateString() | |
| } | |
| async function startAgent() { | |
| await start() | |
| emit('start', props.agent) | |
| } | |
| async function stopAgent() { | |
| await stop() | |
| emit('stop', props.agent) | |
| } | |
| async function restartAgent() { | |
| await restart() | |
| emit('restart', props.agent) | |
| } | |
| </script> | |
| <style lang="scss" scoped> | |
| .agent-status-card { | |
| --agent-color: v-bind('agent.color'); | |
| &--active { | |
| border-left: 4px solid var(--saap-success); | |
| } | |
| &--processing { | |
| border-left: 4px solid var(--saap-warning); | |
| } | |
| &--error { | |
| border-left: 4px solid var(--saap-error); | |
| } | |
| &--disabled { | |
| opacity: 0.6; | |
| pointer-events: none; | |
| } | |
| } | |
| .agent-status-card__header { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: var(--saap-space-4); | |
| } | |
| .agent-status-card__info { | |
| flex: 1; | |
| display: flex; | |
| gap: var(--saap-space-4); | |
| min-width: 0; | |
| } | |
| .agent-status-card__avatar { | |
| position: relative; | |
| flex-shrink: 0; | |
| } | |
| .agent-status-card__avatar-circle { | |
| width: 3rem; | |
| height: 3rem; | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: var(--saap-font-semibold); | |
| font-size: var(--saap-text-lg); | |
| } | |
| .agent-status-card__avatar-icon { | |
| width: 1.5rem; | |
| height: 1.5rem; | |
| } | |
| .agent-status-card__status-badge { | |
| position: absolute; | |
| bottom: -0.25rem; | |
| right: -0.25rem; | |
| } | |
| .agent-status-card__details { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .agent-status-card__name { | |
| font-size: var(--saap-text-lg); | |
| font-weight: var(--saap-font-semibold); | |
| color: var(--saap-neutral-900); | |
| margin: 0 0 var(--saap-space-1) 0; | |
| } | |
| .agent-status-card__description { | |
| font-size: var(--saap-text-sm); | |
| color: var(--saap-neutral-600); | |
| margin: 0 0 var(--saap-space-2) 0; | |
| @include truncate-lines(2); | |
| } | |
| .agent-status-card__meta { | |
| display: flex; | |
| gap: var(--saap-space-3); | |
| font-size: var(--saap-text-xs); | |
| color: var(--saap-neutral-500); | |
| } | |
| .agent-status-card__type, | |
| .agent-status-card__model { | |
| background: var(--saap-neutral-100); | |
| padding: var(--saap-space-1) var(--saap-space-2); | |
| border-radius: var(--saap-border-radius-sm); | |
| font-weight: var(--saap-font-medium); | |
| } | |
| .agent-status-card__actions { | |
| display: flex; | |
| gap: var(--saap-space-2); | |
| flex-shrink: 0; | |
| } | |
| .agent-status-card__metrics { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); | |
| gap: var(--saap-space-4); | |
| margin: var(--saap-space-4) 0; | |
| } | |
| .agent-status-card__metric { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--saap-space-3); | |
| } | |
| .agent-status-card__metric-icon { | |
| width: 2rem; | |
| height: 2rem; | |
| border-radius: var(--saap-border-radius); | |
| background: var(--saap-neutral-100); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: var(--saap-neutral-600); | |
| flex-shrink: 0; | |
| svg { | |
| width: 1rem; | |
| height: 1rem; | |
| } | |
| } | |
| .agent-status-card__metric-content { | |
| min-width: 0; | |
| } | |
| .agent-status-card__metric-value { | |
| font-size: var(--saap-text-lg); | |
| font-weight: var(--saap-font-semibold); | |
| color: var(--saap-neutral-900); | |
| line-height: 1; | |
| } | |
| .agent-status-card__metric-label { | |
| font-size: var(--saap-text-xs); | |
| color: var(--saap-neutral-600); | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| margin-top: var(--saap-space-1); | |
| } | |
| .agent-status-card__capabilities { | |
| margin: var(--saap-space-4) 0; | |
| } | |
| .agent-status-card__capabilities-title { | |
| font-size: var(--saap-text-sm); | |
| font-weight: var(--saap-font-semibold); | |
| color: var(--saap-neutral-700); | |
| margin: 0 0 var(--saap-space-2) 0; | |
| } | |
| .agent-status-card__capabilities-list { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: var(--saap-space-2); | |
| } | |
| .agent-status-card__footer { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| font-size: var(--saap-text-xs); | |
| color: var(--saap-neutral-500); | |
| } | |
| .agent-status-card__last-activity { | |
| display: flex; | |
| gap: var(--saap-space-1); | |
| } | |
| .agent-status-card__last-activity-time { | |
| font-weight: var(--saap-font-medium); | |
| } | |
| .agent-status-card__uptime { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--saap-space-2); | |
| font-weight: var(--saap-font-medium); | |
| } | |
| .agent-status-card__online-indicator { | |
| width: 0.5rem; | |
| height: 0.5rem; | |
| border-radius: 50%; | |
| background: var(--saap-success); | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { | |
| opacity: 1; | |
| } | |
| 50% { | |
| opacity: 0.5; | |
| } | |
| } | |
| // Responsive adjustments | |
| @include respond-to('sm') { | |
| .agent-status-card__metrics { | |
| grid-template-columns: repeat(2, 1fr); | |
| } | |
| } | |
| @include respond-to('lg') { | |
| .agent-status-card__metrics { | |
| grid-template-columns: repeat(4, 1fr); | |
| } | |
| } | |
| </style> | |