CUB-20: Develop agent card component with dynamic status/progress
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m4s
All checks were successful
Dev Build / build-test (pull_request) Successful in 2m4s
This commit is contained in:
@@ -2,7 +2,12 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnDestroy,
|
||||
Signal,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
@@ -11,11 +16,17 @@ 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({
|
||||
@@ -33,7 +44,7 @@ import { AgentStatus } from '../../../models/agent.model';
|
||||
styleUrl: './agent-card.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AgentCardComponent {
|
||||
export class AgentCardComponent implements OnDestroy {
|
||||
// --- Six required inputs per spec ---
|
||||
|
||||
/** Agent status — drives badge color and left‑border accent */
|
||||
@@ -68,7 +79,18 @@ export class AgentCardComponent {
|
||||
/** Error message (shown only when status is 'error') */
|
||||
@Input() errorMessage = '';
|
||||
|
||||
// --- Computed values ---
|
||||
/** 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 left‑border accent */
|
||||
readonly statusBorderColor = computed(() => {
|
||||
@@ -82,46 +104,68 @@ export class AgentCardComponent {
|
||||
return map[this.status] ?? 'var(--status-offline)';
|
||||
});
|
||||
|
||||
/** Human‑readable 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;
|
||||
});
|
||||
/** 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 */
|
||||
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';
|
||||
/** 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);
|
||||
});
|
||||
|
||||
/** Relative time string for lastActivity */
|
||||
readonly lastActivityLabel = computed(() => {
|
||||
if (!this.lastActivity) return '';
|
||||
/** 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 = this.lastActivity.getTime();
|
||||
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`;
|
||||
});
|
||||
}
|
||||
|
||||
/** Quick‑jump route derived from sessionKey */
|
||||
readonly jumpRoute = computed(() => `/sessions/${this.sessionKey}`);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user