import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, Input, OnDestroy, Output, signal, ViewChild, } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; import { AgentCardData, AgentStatus } from '../../models/agent.model'; // ============================================================================ // Agent Session Drawer — Per CUB-26 // Desktop: 480px side drawer slides from right with modal overlay. // Mobile: Bottom sheet slides up from bottom. // Shows: Agent name, status badge, session key, live log tail, // recent messages, and action buttons. // ============================================================================ export interface SessionLogLine { timestamp: Date; level: 'info' | 'warn' | 'error' | 'debug'; message: string; } export interface SessionMessage { id: string; sender: 'agent' | 'user'; content: string; timestamp: Date; } @Component({ selector: 'app-agent-session-drawer', standalone: true, imports: [CommonModule, MatButtonModule, MatIconModule, MatChipsModule], templateUrl: './agent-session-drawer.component.html', styleUrl: './agent-session-drawer.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AgentSessionDrawerComponent implements OnDestroy { // --------------------------------------------------------------------------- // Inputs // --------------------------------------------------------------------------- /** The agent whose session details are displayed. */ @Input() set agent(value: AgentCardData | null) { this._agent = value; if (value) { this.isOpen.set(true); this.loadSessionData(value); } } get agent(): AgentCardData | null { return this._agent; } private _agent: AgentCardData | null = null; /** Whether this is mobile viewport (bottom sheet mode). */ @Input() isMobile = false; // --------------------------------------------------------------------------- // Outputs // --------------------------------------------------------------------------- /** Emitted when the user clicks "Open Full Session". Payload is the session key. */ @Output() readonly openSession = new EventEmitter(); /** Emitted when the user clicks "Pin to Dashboard". Payload is the session key. */ @Output() readonly pinToDashboard = new EventEmitter(); /** Emitted when the drawer closes. */ @Output() readonly drawerClose = new EventEmitter(); // --------------------------------------------------------------------------- // Signals // --------------------------------------------------------------------------- readonly isOpen = signal(false); readonly logLines = signal([]); readonly recentMessages = signal([]); // --------------------------------------------------------------------------- // View Children // --------------------------------------------------------------------------- @ViewChild('drawerPanel') drawerPanel!: ElementRef; // --------------------------------------------------------------------------- // Status Helpers // --------------------------------------------------------------------------- getStatusClass(status: string): string { return `status-dot--${status}`; } getStatusLabel(status: AgentStatus): string { const labels: Record = { active: 'Active', idle: 'Idle', thinking: 'Thinking…', error: 'Error', offline: 'Offline', }; return labels[status] ?? status; } getStatusChipColor(status: AgentStatus): string { const map: Record = { active: 'status-chip--active', idle: 'status-chip--idle', thinking: 'status-chip--thinking', error: 'status-chip--error', offline: 'status-chip--offline', }; return map[status] ?? ''; } getLogLevelClass(level: SessionLogLine['level']): string { return `log-line--${level}`; } /** Format a date to a short time string. */ formatTime(date: Date): string { return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } /** Format a date to a relative time string. */ formatRelativeTime(date: Date): string { 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`; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /** Open the drawer for a specific agent. */ open(agentData: AgentCardData): void { this._agent = agentData; this.isOpen.set(true); this.loadSessionData(agentData); } /** Close the drawer. */ close(): void { this.isOpen.set(false); this.drawerClose.emit(); } // --------------------------------------------------------------------------- // Keyboard Handling // --------------------------------------------------------------------------- @HostListener('document:keydown.escape') onEscapeKey(): void { if (this.isOpen()) { this.close(); } } /** Handle keyboard navigation within the drawer. */ onDrawerKeydown(event: KeyboardEvent): void { if (event.key === 'Escape') { this.close(); return; } // Tab through actions — browser default Tab behavior is fine, // we just trap focus within the drawer } // --------------------------------------------------------------------------- // Outside Click // --------------------------------------------------------------------------- onBackdropClick(): void { this.close(); } // --------------------------------------------------------------------------- // Actions // --------------------------------------------------------------------------- onOpenSession(): void { if (this._agent) { this.openSession.emit(this._agent.sessionKey); } this.close(); } onPinToDashboard(): void { if (this._agent) { this.pinToDashboard.emit(this._agent.sessionKey); } // Don't close — user may want to keep viewing } // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- ngOnDestroy(): void { // Clean up any subscriptions when needed } // --------------------------------------------------------------------------- // Private // --------------------------------------------------------------------------- /** Load mock session data for the agent (TODO: wire to real data service). */ private loadSessionData(agentData: AgentCardData): void { // TODO: Replace with real session data service when available. // For now, generate placeholder log lines and messages. const now = new Date(); const logLines: SessionLogLine[] = []; for (let i = 19; i >= 0; i--) { const ts = new Date(now.getTime() - i * 5000); const levels: SessionLogLine['level'][] = ['info', 'info', 'info', 'debug', 'warn']; const messages = [ `Processing task queue for ${agentData.displayName}`, `SignalR heartbeat OK`, `Session state: active`, `Checking for pending commands…`, `Updating task progress: ${Math.floor(Math.random() * 100)}%`, ]; logLines.push({ timestamp: ts, level: levels[i % levels.length], message: messages[i % messages.length], }); } this.logLines.set(logLines); const recentMessages: SessionMessage[] = [ { id: '1', sender: 'user', content: `Hey ${agentData.displayName}, how's the current task going?`, timestamp: new Date(now.getTime() - 120000), }, { id: '2', sender: 'agent', content: agentData.currentTask ?? 'Working on it — progress is steady.', timestamp: new Date(now.getTime() - 115000), }, { id: '3', sender: 'user', content: 'Great, let me know if you hit any blockers.', timestamp: new Date(now.getTime() - 110000), }, ]; this.recentMessages.set(recentMessages); } }