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