Files
Control-Center/frontend/src/app/command-hub/components/agent-card/agent-card.component.ts
rex-bot a946670157
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m4s
CUB-20: Develop agent card component with dynamic status/progress
2026-04-28 08:18:27 -04:00

171 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 QuickJump Button into a card with leftborder 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 leftborder accent */
@Input({ required: true }) status!: AgentStatus;
/** Current task description, e.g. "Reviewing PR #42" */
@Input() task = '';
/** Task progress percentage 0100 */
@Input() progress = 0;
/** Full session key for quickjump 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<typeof setInterval> | 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 leftborder accent */
readonly statusBorderColor = computed(() => {
const map: Record<AgentStatus, string> = {
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)';
});
/** Humanreadable 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);
});
/** Quickjump 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;
}
}
}