import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, HostListener, OnDestroy, Output, signal, ViewChild, } from '@angular/core'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; import { AgentCardData } from '../../models/agent.model'; import { AgentStatusService } from '../../services/agent-status.service'; @Component({ selector: 'app-quick-jump-drawer', standalone: true, imports: [ReactiveFormsModule], templateUrl: './quick-jump-drawer.component.html', styleUrl: './quick-jump-drawer.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class QuickJumpDrawerComponent implements OnDestroy { /** Emits when the drawer should close (ESC, outside click, or item select). */ @Output() readonly drawerClose = new EventEmitter(); /** Whether the drawer is visible. */ readonly isOpen = signal(false); /** Search/filter input control. */ readonly searchControl = new FormControl('', { nonNullable: true }); /** Filtered agent list based on search. */ readonly filteredAgents = signal([]); /** Track which agent row is highlighted via keyboard navigation. */ readonly highlightedIndex = signal(-1); @ViewChild('searchInput') searchInput!: ElementRef; @ViewChild('drawerPanel') drawerPanel!: ElementRef; private readonly destroy$ = new Subject(); constructor(private readonly agentStatusService: AgentStatusService) { // Reactively filter agents as the search input changes this.searchControl.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe((query) => this.filterAgents(query)); // Initial load this.filterAgents(''); } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /** Open the drawer and focus the search input. */ open(): void { this.isOpen.set(true); this.searchControl.setValue('', { emitEvent: false }); this.highlightedIndex.set(-1); // Focus search input after animation frame (drawer needs to render first) requestAnimationFrame(() => { this.searchInput?.nativeElement?.focus(); }); } /** Close the drawer. */ close(): void { this.isOpen.set(false); this.searchControl.setValue('', { emitEvent: false }); this.highlightedIndex.set(-1); this.drawerClose.emit(); } /** Toggle the drawer open/close. */ toggle(): void { if (this.isOpen()) { this.close(); } else { this.open(); } } // --------------------------------------------------------------------------- // Keyboard Handling // --------------------------------------------------------------------------- @HostListener('document:keydown.escape') onEscapeKey(): void { if (this.isOpen()) { this.close(); } } /** Handle keyboard navigation within the drawer panel. */ onDrawerKeydown(event: KeyboardEvent): void { const agents = this.filteredAgents(); if (!agents.length) return; switch (event.key) { case 'ArrowDown': { event.preventDefault(); this.highlightedIndex.update((i) => i < agents.length - 1 ? i + 1 : 0 ); this.scrollIntoView(); break; } case 'ArrowUp': { event.preventDefault(); this.highlightedIndex.update((i) => i > 0 ? i - 1 : agents.length - 1 ); this.scrollIntoView(); break; } case 'Enter': { const idx = this.highlightedIndex(); if (idx >= 0 && idx < agents.length) { this.selectAgent(agents[idx]); } break; } } } // --------------------------------------------------------------------------- // Outside Click // --------------------------------------------------------------------------- /** Close when clicking on the backdrop (outside the panel). */ onBackdropClick(event: MouseEvent): void { if ( this.drawerPanel?.nativeElement && !this.drawerPanel.nativeElement.contains(event.target as Node) ) { this.close(); } } // --------------------------------------------------------------------------- // Agent Selection // --------------------------------------------------------------------------- /** Select an agent — navigates or focuses the agent card. */ selectAgent(agent: AgentCardData): void { // TODO: Wire up navigation to the selected agent's detail view // For now, emit close after selection console.log('[QuickJump] Selected agent:', agent.id); this.close(); } // --------------------------------------------------------------------------- // Status Helpers // --------------------------------------------------------------------------- /** Get the CSS class for a given agent status. */ getStatusClass(status: string): string { return `status-dot--${status}`; } /** Get a human-readable label for an agent status. */ getStatusLabel(status: string): string { const labels: Record = { active: 'Active', idle: 'Idle', thinking: 'Thinking', error: 'Error', offline: 'Offline', }; return labels[status] ?? status; } // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } // --------------------------------------------------------------------------- // Private // --------------------------------------------------------------------------- private filterAgents(query: string): void { const allAgents = this.agentStatusService.agents(); const lowerQuery = query.toLowerCase().trim(); if (!lowerQuery) { this.filteredAgents.set(allAgents); return; } const filtered = allAgents.filter( (agent) => agent.displayName.toLowerCase().includes(lowerQuery) || agent.id.toLowerCase().includes(lowerQuery) || agent.role.toLowerCase().includes(lowerQuery) ); this.filteredAgents.set(filtered); this.highlightedIndex.set(-1); } private scrollIntoView(): void { const idx = this.highlightedIndex(); const el = document.getElementById(`quick-jump-agent-${idx}`); el?.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); } }