| <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> |
| |
| |
| <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> |
| |
| |
| <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>() |
| |
| |
| const emit = defineEmits<{ |
| start: [agent: Agent] |
| stop: [agent: Agent] |
| restart: [agent: Agent] |
| configure: [agent: Agent] |
| }>() |
| |
| |
| const { |
| isStarting, |
| isStopping, |
| isRestarting, |
| startAgent: start, |
| stopAgent: stop, |
| restartAgent: restart |
| } = useAgentStatus(props.agent) |
| |
| |
| 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 |
| }) |
| |
| |
| 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> |
| |