import { ChangeDetectionStrategy, Component, Input, OnDestroy, Signal, computed, effect, inject, signal, } 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'; import { STATUS_COLORS, STATUS_LABELS, CHANNEL_ICONS, } from '../../../design/tokens'; // ============================================================================ // 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. // Enhanced with data-status attribute, elapsed time, and design tokens. // ============================================================================ @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 implements OnDestroy { // --- 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 = ''; /** Elapsed time string, e.g. "04m 12s" */ @Input() taskElapsed = ''; // --- Internal state --- /** Timer for refreshing relative-time label */ private _timer: ReturnType | null = null; /** Internal signal to trigger relative-time recomputation */ private readonly _tick = signal(0); // --- Computed values using design tokens --- /** 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 (from design tokens) */ readonly statusLabel = computed(() => STATUS_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 (from design tokens) */ readonly channelIcon = computed(() => CHANNEL_ICONS[this.channel] ?? 'chat'); /** Relative time string for lastActivity, refreshed every 30s */ readonly lastActivityLabel = computed(() => { // Read tick to create dependency that forces recomputation this._tick(); return this._relativeTime(this.lastActivity); }); /** Quick‑jump route derived from sessionKey */ readonly jumpRoute = computed(() => `/sessions/${this.sessionKey}`); /** Whether progress bar should show */ readonly showProgress = computed(() => this.progress > 0 && this.status !== 'error'); /** Whether error state is active */ readonly isError = computed(() => this.status === 'error'); /** Whether card is in an active-like state (active or thinking) */ readonly isActiveLike = computed(() => this.status === 'active' || this.status === 'thinking'); constructor() { // Start the relative-time refresh timer this._startTimer(); } ngOnDestroy(): void { this._stopTimer(); } // --- Private helpers --- private _relativeTime(date: Date | null | undefined): string { if (!date) return ''; const now = Date.now(); const then = date.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`; } private _startTimer(): void { this._stopTimer(); this._timer = setInterval(() => { // Increment tick to force lastActivityLabel recomputation this._tick.update(v => v + 1); }, 30_000); } private _stopTimer(): void { if (this._timer) { clearInterval(this._timer); this._timer = null; } } }