diff --git a/report-app/angular.json b/report-app/angular.json index 35086cb..4358c5e 100644 --- a/report-app/angular.json +++ b/report-app/angular.json @@ -16,6 +16,7 @@ "browser": "src/main.ts", "tsConfig": "tsconfig.app.json", "externalDependencies": [ + "firebase-admin", "@firebase/app", "@firebase/firestore", "tiktoken", diff --git a/report-app/src/app/app.scss b/report-app/src/app/app.scss index e9ef106..2cb476c 100644 --- a/report-app/src/app/app.scss +++ b/report-app/src/app/app.scss @@ -54,10 +54,10 @@ .report-body { display: flex; flex-grow: 1; - overflow: hidden; max-width: var(--main-container-width); margin: auto; - padding: 0 1rem 2rem; + // Add some padding on the bottom so there's some space for dropdowns to open. + padding: 0 1rem 10rem; } .reports-link.active { diff --git a/report-app/src/app/pages/report-list/report-list.html b/report-app/src/app/pages/report-list/report-list.html index fffac16..b9d89c4 100644 --- a/report-app/src/app/pages/report-list/report-list.html +++ b/report-app/src/app/pages/report-list/report-list.html @@ -28,6 +28,12 @@ } } + @if (allLabels().length > 0) { + + }
@if (isCompareMode()) { diff --git a/report-app/src/app/pages/report-list/report-list.ts b/report-app/src/app/pages/report-list/report-list.ts index 7f2b75b..d71ef56 100644 --- a/report-app/src/app/pages/report-list/report-list.ts +++ b/report-app/src/app/pages/report-list/report-list.ts @@ -11,10 +11,19 @@ import {MessageSpinner} from '../../shared/message-spinner'; import {Score} from '../../shared/score/score'; import {ProviderLabel} from '../../shared/provider-label'; import {bucketToScoreVariable} from '../../shared/scoring'; +import {MultiSelect} from '../../shared/multi-select/multi-select'; @Component({ selector: 'app-report-list', - imports: [RouterLink, DatePipe, StackedBarChart, MessageSpinner, Score, ProviderLabel], + imports: [ + RouterLink, + DatePipe, + StackedBarChart, + MessageSpinner, + Score, + ProviderLabel, + MultiSelect, + ], templateUrl: './report-list.html', styleUrls: ['./report-list.scss'], }) @@ -30,6 +39,7 @@ export class ReportListComponent { protected selectedFramework = signal(null); protected selectedModel = signal(null); protected selectedRunner = signal(null); + protected selectedLabels = signal([]); protected allFrameworks = computed(() => { const frameworks = new Map(); @@ -67,17 +77,40 @@ export class ReportListComponent { })); }); + protected allLabels = computed(() => { + const labels = new Set(); + + for (const group of this.allGroups()) { + for (const label of group.labels) { + const trimmed = label.trim(); + + if (trimmed) { + labels.add(trimmed); + } + } + } + + return Array.from(labels) + .sort() + .map(label => ({ + label, + value: label, + })); + }); + protected reportGroups = computed(() => { const framework = this.selectedFramework(); const model = this.selectedModel(); const runner = this.selectedRunner(); + const labels = this.selectedLabels(); const groups = this.allGroups(); return groups.filter(group => { const frameworkMatch = !framework || group.framework.fullStackFramework.id === framework; const modelMatch = !model || group.model === model; const runnerMatch = !runner || group.runner?.id === runner; - return frameworkMatch && modelMatch && runnerMatch; + const labelsMatch = labels.length === 0 || group.labels.some(l => labels.includes(l.trim())); + return frameworkMatch && modelMatch && runnerMatch && labelsMatch; }); }); diff --git a/report-app/src/app/pages/report-viewer/failed-checks-filter.ts b/report-app/src/app/pages/report-viewer/failed-checks-filter.ts deleted file mode 100644 index f8b1fba..0000000 --- a/report-app/src/app/pages/report-viewer/failed-checks-filter.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {Component, output, input} from '@angular/core'; - -@Component({ - selector: 'failed-checks-filter', - template: ` - @for (check of allFailedChecks(); track check.name) { - - } - `, - styles: ` - label { - display: block; - padding: 8px 12px; - cursor: pointer; - } - - label:hover { - background-color: #eff6ff; - border-radius: var(--border-radius); - } - `, -}) -export class FailedChecksFilter { - allFailedChecks = input.required<{name: string; count: number}[]>(); - selectedChecks = input.required>(); - toggleCheck = output(); -} diff --git a/report-app/src/app/pages/report-viewer/report-viewer.html b/report-app/src/app/pages/report-viewer/report-viewer.html index 6e7e895..d7c7603 100644 --- a/report-app/src/app/pages/report-viewer/report-viewer.html +++ b/report-app/src/app/pages/report-viewer/report-viewer.html @@ -240,16 +240,11 @@

Logs

Generated applications

@if (allFailedChecks().length > 0) { -
- Filter by failed checks ({{ selectedChecks().size }} selected) - -
+ }
@for (result of filteredResults(); track result) { diff --git a/report-app/src/app/pages/report-viewer/report-viewer.scss b/report-app/src/app/pages/report-viewer/report-viewer.scss index 44b05fb..fa6eb4e 100644 --- a/report-app/src/app/pages/report-viewer/report-viewer.scss +++ b/report-app/src/app/pages/report-viewer/report-viewer.scss @@ -147,51 +147,8 @@ lighthouse-category + lighthouse-category { margin-bottom: 2em; } -.filter-dropdown { - position: relative; - display: inline-block; - - margin-bottom: 1.5rem; - gap: 0.5rem; - background-color: var(--card-bg-color); - padding: 0; - border-radius: var(--control-border-radius); - border: 1px solid var(--border-color); - transition: background-color var(--transition-speed) ease; - - &:hover { - background-color: var(--button-active-bg-color); - } - - & summary { - cursor: pointer; - padding: 0.5rem 1.25rem; - border-radius: 4px; - background-color: transparent; - list-style: none; - font-weight: 500; - color: var(--text-secondary); - transition: color var(--transition-speed) ease; - } - - &[open] .dropdown-content { - display: block; - color: var(--text-primary); - } -} - -.dropdown-content { - display: none; - position: absolute; - background-color: var(--card-bg-color); - min-width: 380px; - box-shadow: var(--shadow); - z-index: 1; - border: 1px solid var(--border-color); - border-radius: var(--border-radius); - padding: 10px; - max-height: 300px; - overflow: auto; +.check-filter { + margin-bottom: 1rem; } .no-failed-checks { diff --git a/report-app/src/app/pages/report-viewer/report-viewer.ts b/report-app/src/app/pages/report-viewer/report-viewer.ts index 3175df1..08e7109 100644 --- a/report-app/src/app/pages/report-viewer/report-viewer.ts +++ b/report-app/src/app/pages/report-viewer/report-viewer.ts @@ -32,7 +32,6 @@ import { StackedBarChartData, } from '../../shared/visualization/stacked-bar-chart/stacked-bar-chart'; import {formatFile} from './formatter'; -import {FailedChecksFilter} from './failed-checks-filter'; import {MessageSpinner} from '../../shared/message-spinner'; import {createPromptDebuggingZip} from '../../shared/debugging-zip'; import {Score} from '../../shared/score/score'; @@ -42,6 +41,7 @@ import {ExpansionPanelHeader} from '../../shared/expansion-panel/expansion-panel import {ProviderLabel} from '../../shared/provider-label'; import {AiAssistant} from '../../shared/ai-assistant/ai-assistant'; import {LighthouseCategory} from './lighthouse-category'; +import {MultiSelect} from '../../shared/multi-select/multi-select'; const localReportRegex = /-l\d+$/; @@ -51,7 +51,6 @@ const localReportRegex = /-l\d+$/; CodeViewer, DatePipe, DecimalPipe, - FailedChecksFilter, MessageSpinner, Score, ExpansionPanel, @@ -60,12 +59,10 @@ const localReportRegex = /-l\d+$/; NgxJsonViewerModule, AiAssistant, LighthouseCategory, + MultiSelect, ], templateUrl: './report-viewer.html', styleUrls: ['./report-viewer.scss'], - host: { - '(document:click)': 'closeDropdownIfOpen($event)', - }, }) export class ReportViewer { private clipboard = inject(Clipboard); @@ -107,7 +104,7 @@ export class ReportViewer { return this.reportsFetcher.reportGroups().find(group => group.id === id); }); - protected selectedChecks = signal>(new Set()); + protected selectedChecks = signal([]); protected allFailedChecks = computed(() => { if (!this.selectedReport.hasValue()) { @@ -136,11 +133,11 @@ export class ReportViewer { } const failedChecksArray = Array.from(failedChecksMap.entries()).map(([name, count]) => ({ - name, - count, + label: `${name} (${count})`, + value: name, })); - return failedChecksArray.sort((a, b) => a.name.localeCompare(b.name)); + return failedChecksArray.sort((a, b) => a.label.localeCompare(b.label)); }); protected filteredResults = computed(() => { @@ -151,7 +148,7 @@ export class ReportViewer { return []; } - if (checks.size === 0) { + if (checks.length === 0) { return report.results; } @@ -164,7 +161,7 @@ export class ReportViewer { if (this.isSkippedAssessment(assessment)) { continue; } - if (assessment.successPercentage < 1 && checks.has(assessment.name)) { + if (assessment.successPercentage < 1 && checks.includes(assessment.name)) { return true; } } @@ -353,27 +350,6 @@ export class ReportViewer { return value.state === IndividualAssessmentState.SKIPPED; } - protected dropdownRef = viewChild('dropdown'); - - protected closeDropdownIfOpen(event: MouseEvent): void { - const detailsElement = this.dropdownRef()?.nativeElement; - if (detailsElement?.hasAttribute('open') && !detailsElement.contains(event.target)) { - detailsElement.removeAttribute('open'); - } - } - - protected toggleCheckFilter(check: string): void { - this.selectedChecks.update(currentChecks => { - const checks = new Set(currentChecks); - if (checks.has(check)) { - checks.delete(check); - } else { - checks.add(check); - } - return checks; - }); - } - protected async format(file: LlmResponseFile): Promise { const result = await formatFile(file, this.selectedReport.value()!.details.summary.framework); if (typeof result === 'string') { diff --git a/report-app/src/app/shared/multi-select/multi-select.html b/report-app/src/app/shared/multi-select/multi-select.html new file mode 100644 index 0000000..b98f9d0 --- /dev/null +++ b/report-app/src/app/shared/multi-select/multi-select.html @@ -0,0 +1,19 @@ + diff --git a/report-app/src/app/shared/multi-select/multi-select.scss b/report-app/src/app/shared/multi-select/multi-select.scss new file mode 100644 index 0000000..e0deba0 --- /dev/null +++ b/report-app/src/app/shared/multi-select/multi-select.scss @@ -0,0 +1,61 @@ +:host { + display: inline-flex; +} + +button { + border-radius: var(--control-border-radius); + border: 1px solid var(--border-color); + background-color: var(--card-bg-color); + color: var(--text-secondary); + font-size: 0.9rem; + font-weight: 500; + padding: 0 36px 0 16px; + height: var(--control-height); + display: inline-block; + transition: background-color var(--transition-speed) ease; + position: relative; + cursor: pointer; + background-image: url('data:image/svg+xml;charset=US-ASCII,'); + background-repeat: no-repeat; + background-position: right 1rem center; + background-size: 0.65rem auto; + + &:not(.open):focus, &:not(.open):hover { + border-color: var(--accent-blue); + } +} + +.dropdown { + display: none; + position: absolute; + top: 100%; + left: 0; + background-color: var(--card-bg-color); + min-width: 380px; + box-shadow: var(--shadow); + z-index: 1; + border: 1px solid var(--border-color); + border-radius: var(--border-radius); + padding: 10px; + max-height: 300px; + overflow: auto; + text-align: left; + + .open & { + display: block; + color: var(--text-primary); + } +} + +label { + display: flex; + padding: 8px 12px; + cursor: pointer; + align-items: center; + gap: 0.5rem; +} + +label:hover { + background-color: #eff6ff; + border-radius: var(--border-radius); +} diff --git a/report-app/src/app/shared/multi-select/multi-select.ts b/report-app/src/app/shared/multi-select/multi-select.ts new file mode 100644 index 0000000..0e06efb --- /dev/null +++ b/report-app/src/app/shared/multi-select/multi-select.ts @@ -0,0 +1,39 @@ +import {Component, ElementRef, inject, input, model, signal} from '@angular/core'; + +interface MultiSelectOption { + label: string; + value: unknown; +} + +@Component({ + selector: 'multi-select', + templateUrl: 'multi-select.html', + styleUrl: 'multi-select.scss', + host: { + '(document:click)': 'outsideClick($event)', + }, +}) +export class MultiSelect { + private elementRef = inject>(ElementRef); + + options = input.required(); + label = input.required(); + selected = model([]); + + protected isOpen = signal(false); + + protected optionClicked(option: MultiSelectOption) { + this.selected.update(selected => { + if (selected.includes(option.value)) { + return selected.filter(current => current !== option.value); + } + return [...selected, option.value]; + }); + } + + protected outsideClick(event: MouseEvent) { + if (this.isOpen() && !this.elementRef.nativeElement.contains(event.target as HTMLElement)) { + this.isOpen.set(false); + } + } +} diff --git a/report-app/src/app/shared/scoring.ts b/report-app/src/app/shared/scoring.ts index e87a73d..f609b5e 100644 --- a/report-app/src/app/shared/scoring.ts +++ b/report-app/src/app/shared/scoring.ts @@ -55,8 +55,6 @@ export function getHardcodedColor( CACHED_COLORS[colorMode][varName] = value; } - console.log(CACHED_COLORS); - return CACHED_COLORS[colorMode][varName]; }