Files
Control-Center/frontend/src/app/command-hub/components/agent-card/agent-card.component.ts

140 lines
4.5 KiB
TypeScript
Raw Normal View History

import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
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';
import { LongPressDirective } from '../../../directives/long-press.directive';
// ============================================================================
// AgentCard Component
// Per spec Section 7.3: Composes Agent Status Badge, Task Progress Bar,
// and QuickJump Button into a card with leftborder status accent.
// CUB-26: Emits cardClick and cardLongPress for drawer/modal integration.
// ============================================================================
@Component({
selector: 'app-agent-card',
standalone: true,
imports: [
CommonModule,
RouterModule,
MatIconModule,
MatButtonModule,
MatProgressBarModule,
MatTooltipModule,
LongPressDirective,
],
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 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 = '';
// --- CUB-26: Outputs for drawer/modal integration ---
/** Emitted when the card is clicked — opens the session drawer. */
@Output() readonly cardClick = new EventEmitter<string>();
/** Emitted when the card is long-pressed — bypasses drawer, opens session log directly. */
@Output() readonly cardLongPress = new EventEmitter<string>();
// --- Computed values ---
/** 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 */
readonly statusLabel = computed(() => {
const labels: Record<AgentStatus, string> = {
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<string, string> = {
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`;
});
/** Quickjump route derived from sessionKey */
readonly jumpRoute = computed(() => `/sessions/${this.sessionKey}`);
}