import { ChangeDetectionStrategy, Component, Input, computed, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatTooltipModule } from '@angular/material/tooltip'; import { AgentStatus } from '../../../models/agent.model'; // ============================================================================ // AgentCard Component // Per spec Section 7.3: Composes Agent Status Badge, Task Progress Bar, // and Quick‑Jump Button into a card with left‑border status accent. // ============================================================================ @Component({ selector: 'app-agent-card', standalone: true, imports: [ CommonModule, RouterModule, MatIconModule, MatButtonModule, MatProgressBarModule, MatTooltipModule, ], templateUrl: './agent-card.component.html', styleUrl: './agent-card.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AgentCardComponent { // --- Six required inputs per spec --- /** Agent status — drives badge color and left‑border accent */ @Input({ required: true }) status!: AgentStatus; /** Current task description, e.g. "Reviewing PR #42" */ @Input() task = ''; /** Task progress percentage 0–100 */ @Input() progress = 0; /** Full session key for quick‑jump navigation */ @Input({ required: true }) sessionKey = ''; /** Communication channel, e.g. "telegram" */ @Input({ required: true }) channel = ''; /** Timestamp of last agent activity */ @Input({ required: true }) lastActivity!: Date; // --- Additional display inputs --- /** Short agent ID, e.g. "otto" */ @Input() agentId = ''; /** Display name, e.g. "Otto" */ @Input() displayName = ''; /** Role description, e.g. "Orchestrator Agent" */ @Input() role = ''; /** Error message (shown only when status is 'error') */ @Input() errorMessage = ''; // --- Computed values --- /** Map status → CSS custom property for the left‑border accent */ readonly statusBorderColor = computed(() => { const map: Record = { active: 'var(--status-active)', idle: 'var(--status-idle)', thinking: 'var(--status-thinking)', error: 'var(--status-error)', offline: 'var(--status-offline)', }; return map[this.status] ?? 'var(--status-offline)'; }); /** Human‑readable status label */ readonly statusLabel = computed(() => { const labels: Record = { active: 'Active', idle: 'Idle', thinking: 'Thinking…', error: 'Error', offline: 'Offline', }; return labels[this.status] ?? this.status; }); /** CSS class suffix for the status badge dot */ readonly statusDotClass = computed(() => `status-dot--${this.status}`); /** Material icon name for the channel */ readonly channelIcon = computed(() => { const icons: Record = { telegram: 'telegram', // falls back to font icon if no SVG registered slack: 'chat', discord: 'forum', whatsapp: 'chat', webchat: 'language', email: 'email', }; return icons[this.channel] ?? 'chat'; }); /** Relative time string for lastActivity */ readonly lastActivityLabel = computed(() => { if (!this.lastActivity) return ''; const now = Date.now(); const then = this.lastActivity.getTime(); const diffSec = Math.max(0, Math.floor((now - then) / 1000)); if (diffSec < 60) return 'just now'; if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`; if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`; return `${Math.floor(diffSec / 86400)}d ago`; }); /** Quick‑jump route derived from sessionKey */ readonly jumpRoute = computed(() => `/sessions/${this.sessionKey}`); }