-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathobservability-drawer.ts
More file actions
123 lines (110 loc) · 4.39 KB
/
Copy pathobservability-drawer.ts
File metadata and controls
123 lines (110 loc) · 4.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
import {
Component,
ElementRef,
HostListener,
Injector,
afterNextRender,
computed,
effect,
inject,
viewChild,
} from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { CdkTrapFocus } from '@angular/cdk/a11y';
import { MetricComponent } from '../ui/metric/metric';
import { SectionHeadComponent } from '../ui/section-head/section-head';
import { ObservabilityService, type TimelineRow } from '../../core/observability/observability.service';
import { ObservabilityDrawerService } from '../../core/observability/observability-drawer.service';
import { TokenAccountantService } from '../../core/observability/token-accountant.service';
import { AgentEventStore } from '../../core/streaming/agent-event.store';
import { formatTokens, formatUsd } from '../../core/observability/pricing';
interface RenderedRow extends TimelineRow {
readonly leftPct: number;
readonly widthPct: number;
}
@Component({
selector: 'app-observability-drawer',
imports: [MatIconModule, MatButtonModule, CdkTrapFocus, MetricComponent, SectionHeadComponent],
templateUrl: './observability-drawer.html',
styleUrl: './observability-drawer.scss',
})
export class ObservabilityDrawerComponent {
protected readonly drawer = inject(ObservabilityDrawerService);
protected readonly observability = inject(ObservabilityService);
protected readonly accountant = inject(TokenAccountantService);
private readonly store = inject(AgentEventStore);
protected readonly isOpen = this.drawer.isOpen;
protected readonly currentTurn = this.accountant.currentTurn;
protected readonly selectedRow = this.observability.selectedRow;
// Low-level stream telemetry, relocated here out of the primary flow.
protected readonly streamStats = this.store.stats;
// Aside stays mounted for slide transition — manual focus management instead of cdkTrapFocus auto-capture.
// Read ElementRef explicitly: #closeBtn sits on a mat-icon-button (a component),
// so a bare viewChild would return the MatIconButton instance (no .nativeElement).
private readonly closeBtn = viewChild<unknown, ElementRef<HTMLButtonElement>>('closeBtn', {
read: ElementRef,
});
private previouslyFocused: HTMLElement | null = null;
constructor() {
const injector = inject(Injector);
effect(() => {
const open = this.isOpen();
if (open) {
const active = document.activeElement;
this.previouslyFocused = active instanceof HTMLElement ? active : null;
afterNextRender(
() => this.closeBtn()?.nativeElement.focus({ preventScroll: true }),
{ injector },
);
} else if (this.previouslyFocused) {
const target = this.previouslyFocused;
this.previouslyFocused = null;
target.focus({ preventScroll: true });
}
});
}
protected readonly bounds = this.observability.bounds;
protected readonly rows = computed<readonly RenderedRow[]>(() => {
const b = this.bounds();
const span = Math.max(1, b.durationMs);
return this.observability.timeline().map((row) => ({
...row,
leftPct: ((row.startedAt - b.startedAt) / span) * 100,
widthPct: Math.max(0.5, (row.durationMs / span) * 100),
}));
});
protected readonly hasData = computed(() => this.rows().length > 0);
protected readonly totalCost = computed(() => this.currentTurn().costUsd);
protected readonly totalTokens = computed(() => this.currentTurn().totals.totalTokens);
protected readonly totalLatency = computed(() => this.currentTurn().totalLatencyMs);
protected readonly roundCount = computed(() => this.currentTurn().rounds.length);
protected close(): void {
this.drawer.close();
}
protected selectRow(id: string): void {
if (this.observability.selectedRowId() === id) {
this.observability.clearSelection();
} else {
this.observability.selectRow(id);
}
}
protected clearSelection(): void {
this.observability.clearSelection();
}
@HostListener('document:keydown.escape')
protected onEscape(): void {
if (this.isOpen()) {
if (this.observability.selectedRowId()) {
this.observability.clearSelection();
} else {
this.close();
}
}
}
protected fmtUsd = formatUsd;
protected fmtTokens = formatTokens;
protected fmtLatency(ms: number): string {
return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(2)}s`;
}
}