All checks were successful
Dev Build / build-test (pull_request) Successful in 1m57s
215 lines
6.4 KiB
TypeScript
215 lines
6.4 KiB
TypeScript
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<void>();
|
|
|
|
/** 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<AgentCardData[]>([]);
|
|
|
|
/** Track which agent row is highlighted via keyboard navigation. */
|
|
readonly highlightedIndex = signal(-1);
|
|
|
|
@ViewChild('searchInput') searchInput!: ElementRef<HTMLInputElement>;
|
|
@ViewChild('drawerPanel') drawerPanel!: ElementRef<HTMLElement>;
|
|
|
|
private readonly destroy$ = new Subject<void>();
|
|
|
|
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<string, string> = {
|
|
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' });
|
|
}
|
|
} |