diff --git a/frontend/src/app/command-hub/components/agent-card/agent-card.component.html b/frontend/src/app/command-hub/components/agent-card/agent-card.component.html new file mode 100644 index 0000000..8140132 --- /dev/null +++ b/frontend/src/app/command-hub/components/agent-card/agent-card.component.html @@ -0,0 +1,82 @@ + + + + + +
+ + +
+
+ + {{ statusLabel() }} +
+ +
+ {{ displayName || agentId }} + {{ role }} +
+
+ + +
+

+ {{ status === 'error' ? errorMessage || task : task }} +

+
+ + +
+ + {{ progress }}% +
+ + + +
\ No newline at end of file diff --git a/frontend/src/app/command-hub/components/agent-card/agent-card.component.scss b/frontend/src/app/command-hub/components/agent-card/agent-card.component.scss new file mode 100644 index 0000000..2846e74 --- /dev/null +++ b/frontend/src/app/command-hub/components/agent-card/agent-card.component.scss @@ -0,0 +1,234 @@ +// ============================================================================ +// AgentCard — M3 tactical dark styling +// Per spec Section 7.3: left‑border accent, status‑aware coloring, +// responsive card layout with 320px min‑width. +// ============================================================================ + +.agent-card { + display: flex; + flex-direction: column; + min-width: var(--cc-card-min-width); + padding: var(--cc-card-padding); + background-color: var(--cc-surface-container); + border-radius: var(--cc-card-border-radius); + border-left: 4px solid var(--status-offline); // default; overridden by [style] + border-top: 1px solid var(--cc-outline); + border-right: 1px solid var(--cc-outline); + border-bottom: 1px solid var(--cc-outline); + gap: 16px; + transition: border-left-color 0.3s ease, box-shadow 0.2s ease; + cursor: default; + + &:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + + &:focus-within { + outline: 2px solid var(--status-active); + outline-offset: 2px; + } +} + +// ── Header ── +.agent-card__header { + display: flex; + align-items: center; + gap: 12px; +} + +.agent-card__badge { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 12px; + background-color: var(--status-active-bg); // overridden per status below + font-size: 12px; + font-weight: 500; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--cc-on-surface); + + // Per‑status background tints + .status-dot--active + & { + background-color: var(--status-active-bg); + } +} + +.agent-card__status-label { + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--cc-on-surface-variant); +} + +.agent-card__identity { + display: flex; + flex-direction: column; + gap: 2px; +} + +.agent-card__name { + font-size: 16px; + font-weight: 600; + color: var(--cc-on-surface); + line-height: 1.2; +} + +.agent-card__role { + font-size: 12px; + font-weight: 400; + color: var(--cc-on-surface-variant); +} + +// ── Body ── +.agent-card__body { + padding: 4px 0; +} + +.agent-card__task { + margin: 0; + font-size: 14px; + font-weight: 400; + color: var(--cc-on-surface); + line-height: 1.4; + + // Error messages get distinct styling + .agent-card--error & { + color: var(--status-error); + } +} + +// ── Progress Bar ── +.agent-card__progress { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 0; +} + +.agent-card__progress-label { + font-size: 12px; + font-weight: 500; + color: var(--cc-on-surface-variant); + white-space: nowrap; + min-width: 36px; +} + +// Override mat-progress-bar to match tactical dark theme +.agent-card__progress ::ng-deep .mat-mdc-progress-bar { + height: 4px; + border-radius: 2px; + + .mdc-linear-progress__bar-inner { + background-color: var(--status-active); + } + + .mdc-linear-progress__track { + background-color: var(--cc-outline); + } +} + +// ── Footer ── +.agent-card__footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-top: auto; // push footer to bottom +} + +.agent-card__meta { + display: flex; + align-items: center; + gap: 12px; +} + +.agent-card__channel { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 12px; + color: var(--cc-on-surface-variant); +} + +.agent-card__channel-icon, +.agent-card__channel .mat-icon { + font-size: 14px; + width: 14px; + height: 14px; +} + +.agent-card__last-activity { + font-size: 12px; + color: var(--cc-on-surface-variant); +} + +// ── Quick‑Jump Button ── +.agent-card__jump { + flex-shrink: 0; + + // Match M3 text button sizing + .mat-mdc-button { + min-width: 36px; + padding: 0 8px; + color: var(--status-active); + } + + .mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} + +// ── Status‑specific background tints for badge ── +// We use the global status-dot classes from styles.scss and pair them +// with contextual background-color overrides here. + +.agent-card[data-status="active"] .agent-card__badge, +.agent-card .status-dot--active ~ .agent-card__badge { + background-color: var(--status-active-bg); +} + +.agent-card[data-status="idle"] .agent-card__badge { + background-color: var(--status-idle-bg); +} + +.agent-card[data-status="thinking"] .agent-card__badge { + background-color: var(--status-thinking-bg); +} + +.agent-card[data-status="error"] .agent-card__badge { + background-color: var(--status-error-bg); +} + +// ── Responsive ── +@media (max-width: 599px) { + .agent-card { + min-width: unset; + padding: 16px; + } + + .agent-card__header { + flex-wrap: wrap; + gap: 8px; + } + + .agent-card__footer { + flex-wrap: wrap; + gap: 8px; + } + + .agent-card__meta { + gap: 8px; + } +} + +// ── Accessibility: reduced motion ── +@media (prefers-reduced-motion: reduce) { + .agent-card { + transition: none; + } +} \ No newline at end of file diff --git a/frontend/src/app/command-hub/components/agent-card/agent-card.component.ts b/frontend/src/app/command-hub/components/agent-card/agent-card.component.ts new file mode 100644 index 0000000..a378e62 --- /dev/null +++ b/frontend/src/app/command-hub/components/agent-card/agent-card.component.ts @@ -0,0 +1,127 @@ +import { + ChangeDetectionStrategy, + Component, + Input, + 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'; + +// ============================================================================ +// 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. +// ============================================================================ + +@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 { + // --- 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 = ''; + + // --- Computed values --- + + /** 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 */ + readonly statusLabel = computed(() => { + const labels: Record = { + 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 = { + 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`; + }); + + /** Quick‑jump route derived from sessionKey */ + readonly jumpRoute = computed(() => `/sessions/${this.sessionKey}`); +} \ No newline at end of file diff --git a/frontend/src/app/command-hub/components/index.ts b/frontend/src/app/command-hub/components/index.ts new file mode 100644 index 0000000..39f0d45 --- /dev/null +++ b/frontend/src/app/command-hub/components/index.ts @@ -0,0 +1 @@ +export * from './agent-card/agent-card.component'; \ No newline at end of file