2026-04-27 14:36:14 +00:00
|
|
|
|
import {
|
|
|
|
|
|
ChangeDetectionStrategy,
|
|
|
|
|
|
Component,
|
2026-04-28 09:14:30 -04:00
|
|
|
|
EventEmitter,
|
2026-04-27 14:36:14 +00:00
|
|
|
|
Input,
|
2026-04-28 08:18:27 -04:00
|
|
|
|
OnDestroy,
|
2026-04-28 09:14:30 -04:00
|
|
|
|
Output,
|
2026-04-27 14:36:14 +00:00
|
|
|
|
computed,
|
2026-04-28 08:18:27 -04:00
|
|
|
|
effect,
|
|
|
|
|
|
inject,
|
|
|
|
|
|
signal,
|
2026-04-27 14:36:14 +00:00
|
|
|
|
} 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';
|
2026-04-28 09:14:30 -04:00
|
|
|
|
import { LongPressDirective } from '../../../directives/long-press.directive';
|
2026-04-28 08:18:27 -04:00
|
|
|
|
import {
|
|
|
|
|
|
STATUS_COLORS,
|
|
|
|
|
|
STATUS_LABELS,
|
|
|
|
|
|
CHANNEL_ICONS,
|
|
|
|
|
|
} from '../../../design/tokens';
|
2026-04-27 14:36:14 +00:00
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 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.
|
2026-04-28 09:14:30 -04:00
|
|
|
|
// CUB-26: Emits cardClick and cardLongPress for drawer/modal integration.
|
2026-04-28 08:18:27 -04:00
|
|
|
|
// Enhanced with data-status attribute, elapsed time, and design tokens.
|
2026-04-27 14:36:14 +00:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
|
|
selector: 'app-agent-card',
|
|
|
|
|
|
standalone: true,
|
|
|
|
|
|
imports: [
|
|
|
|
|
|
CommonModule,
|
|
|
|
|
|
RouterModule,
|
|
|
|
|
|
MatIconModule,
|
|
|
|
|
|
MatButtonModule,
|
|
|
|
|
|
MatProgressBarModule,
|
|
|
|
|
|
MatTooltipModule,
|
2026-04-28 09:14:30 -04:00
|
|
|
|
LongPressDirective,
|
2026-04-27 14:36:14 +00:00
|
|
|
|
],
|
|
|
|
|
|
templateUrl: './agent-card.component.html',
|
|
|
|
|
|
styleUrl: './agent-card.component.scss',
|
|
|
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
|
|
|
})
|
2026-04-28 08:18:27 -04:00
|
|
|
|
export class AgentCardComponent implements OnDestroy {
|
2026-04-27 14:36:14 +00:00
|
|
|
|
// --- 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 = '';
|
|
|
|
|
|
|
2026-04-28 08:18:27 -04:00
|
|
|
|
/** Elapsed time string, e.g. "04m 12s" */
|
|
|
|
|
|
@Input() taskElapsed = '';
|
|
|
|
|
|
|
2026-04-28 09:14:30 -04:00
|
|
|
|
// --- 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>();
|
|
|
|
|
|
|
2026-04-28 08:18:27 -04:00
|
|
|
|
// --- 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 ---
|
2026-04-27 14:36:14 +00:00
|
|
|
|
|
|
|
|
|
|
/** Map status → CSS custom property for the left‑border 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)';
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:18:27 -04:00
|
|
|
|
/** Human‑readable status label (from design tokens) */
|
|
|
|
|
|
readonly statusLabel = computed(() => STATUS_LABELS[this.status] ?? this.status);
|
2026-04-27 14:36:14 +00:00
|
|
|
|
|
|
|
|
|
|
/** CSS class suffix for the status badge dot */
|
|
|
|
|
|
readonly statusDotClass = computed(() => `status-dot--${this.status}`);
|
|
|
|
|
|
|
2026-04-28 08:18:27 -04:00
|
|
|
|
/** 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);
|
2026-04-27 14:36:14 +00:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-28 08:18:27 -04:00
|
|
|
|
/** 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 '';
|
2026-04-27 14:36:14 +00:00
|
|
|
|
const now = Date.now();
|
2026-04-28 08:18:27 -04:00
|
|
|
|
const then = date.getTime();
|
2026-04-27 14:36:14 +00:00
|
|
|
|
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`;
|
2026-04-28 08:18:27 -04:00
|
|
|
|
}
|
2026-04-27 14:36:14 +00:00
|
|
|
|
|
2026-04-28 08:18:27 -04:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-27 14:36:14 +00:00
|
|
|
|
}
|