diff --git a/report-app/src/app/app.html b/report-app/src/app/app.html index 9fb9bd4..3df85e1 100644 --- a/report-app/src/app/app.html +++ b/report-app/src/app/app.html @@ -13,6 +13,14 @@

Web Codegen Scorer

arrow_back + + trending_up + + diff --git a/report-app/src/app/app.routes.ts b/report-app/src/app/app.routes.ts index cb60796..e702041 100644 --- a/report-app/src/app/app.routes.ts +++ b/report-app/src/app/app.routes.ts @@ -2,6 +2,7 @@ import {Routes} from '@angular/router'; import {ReportViewer} from './pages/report-viewer/report-viewer'; import {ComparisonPage} from './pages/comparison/comparison'; import {ReportListComponent} from './pages/report-list/report-list'; +import {Trajectory} from './pages/trajectory/trajectory'; export const routes: Routes = [ { @@ -16,6 +17,10 @@ export const routes: Routes = [ path: 'comparison', component: ComparisonPage, }, + { + path: 'trajectory', + component: Trajectory, + }, { path: '', redirectTo: 'reports', 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 b9d89c4..2093eca 100644 --- a/report-app/src/app/pages/report-list/report-list.html +++ b/report-app/src/app/pages/report-list/report-list.html @@ -3,38 +3,7 @@ } @else {
-
- @if (allFrameworks().length > 1) { - - } - @if (allModels().length > 1) { - - } - @if (allRunners().length > 1) { - - } - @if (allLabels().length > 0) { - - } -
+
@if (isCompareMode()) { @@ -48,7 +17,7 @@
- @for (group of reportGroups(); track group.id) { + @for (group of filters.filteredGroups(); track group.id) {
diff --git a/report-app/src/app/pages/report-list/report-list.scss b/report-app/src/app/pages/report-list/report-list.scss index 38b2ef1..d8ccf52 100644 --- a/report-app/src/app/pages/report-list/report-list.scss +++ b/report-app/src/app/pages/report-list/report-list.scss @@ -93,65 +93,12 @@ h1, h2 { flex-shrink: 0; } -.report-item-container { - display: flex; - align-items: center; - gap: 1rem; -} - -.report-checkbox { - appearance: none; - -webkit-appearance: none; - margin: 0; - font: inherit; - color: currentColor; - width: 1.5rem; - height: 1.5rem; - border: 0.15rem solid var(--border-color); - border-radius: 0.35rem; - transform: translateY(-0.075em); - display: grid; - place-content: center; - cursor: pointer; - transition: all 0.1s ease-in-out; -} - -.report-checkbox::before { - content: ''; - width: 0.75rem; - height: 0.75rem; - transform: scale(0); - transition: 120ms transform ease-in-out; - box-shadow: inset 1em 1em var(--card-bg-color); - // Use a CSS trick to create a checkmark - transform-origin: bottom left; - clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); -} - -.report-checkbox:checked::before { - transform: scale(1); -} - -.report-checkbox:checked { - background: var(--accent-blue); - border-color: var(--accent-blue); -} - -.report-checkbox:hover { - border-color: var(--accent-blue); -} - .toolbar { display: flex; justify-content: space-between; align-items: center; } -.filter-container { - display: flex; - gap: 1rem; -} - .select-for-comparison { display: flex; align-items: center; 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 d71ef56..6ae2df3 100644 --- a/report-app/src/app/pages/report-list/report-list.ts +++ b/report-app/src/app/pages/report-list/report-list.ts @@ -11,7 +11,7 @@ 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'; +import {ReportFilters} from '../../shared/report-filters/report-filters'; @Component({ selector: 'app-report-list', @@ -22,7 +22,7 @@ import {MultiSelect} from '../../shared/multi-select/multi-select'; MessageSpinner, Score, ProviderLabel, - MultiSelect, + ReportFilters, ], templateUrl: './report-list.html', styleUrls: ['./report-list.scss'], @@ -30,90 +30,10 @@ import {MultiSelect} from '../../shared/multi-select/multi-select'; export class ReportListComponent { private reportsFetcher = inject(ReportsFetcher); private router = inject(Router); - private allGroups = this.reportsFetcher.reportGroups; protected isLoading = this.reportsFetcher.isLoadingReportsList; protected reportsToCompare = signal([]); protected isServer = isPlatformServer(inject(PLATFORM_ID)); - - protected selectedFramework = signal(null); - protected selectedModel = signal(null); - protected selectedRunner = signal(null); - protected selectedLabels = signal([]); - - protected allFrameworks = computed(() => { - const frameworks = new Map(); - this.allGroups().forEach(group => { - const framework = group.framework.fullStackFramework; - frameworks.set(framework.id, framework.displayName); - }); - return Array.from(frameworks.entries()).map(([id, displayName]) => ({ - id, - displayName, - })); - }); - - protected allModels = computed(() => { - const models = new Set(this.allGroups().map(g => g.model)); - - return Array.from(models).map(model => ({ - id: model, - displayName: model, - })); - }); - - protected allRunners = computed(() => { - const runners = new Map(); - - this.allGroups().forEach(group => { - if (group.runner) { - runners.set(group.runner.id, group.runner.displayName); - } - }); - - return Array.from(runners.entries()).map(([id, displayName]) => ({ - id, - displayName, - })); - }); - - 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; - const labelsMatch = labels.length === 0 || group.labels.some(l => labels.includes(l.trim())); - return frameworkMatch && modelMatch && runnerMatch && labelsMatch; - }); - }); - protected isCompareMode = signal(false); protected handleCompare() { diff --git a/report-app/src/app/pages/trajectory/trajectory.html b/report-app/src/app/pages/trajectory/trajectory.html new file mode 100644 index 0000000..a8a518b --- /dev/null +++ b/report-app/src/app/pages/trajectory/trajectory.html @@ -0,0 +1,5 @@ + + +
+ +
diff --git a/report-app/src/app/pages/trajectory/trajectory.scss b/report-app/src/app/pages/trajectory/trajectory.scss new file mode 100644 index 0000000..49f0d16 --- /dev/null +++ b/report-app/src/app/pages/trajectory/trajectory.scss @@ -0,0 +1,7 @@ +report-filters { + margin-bottom: 1rem; +} + +.card { + padding-top: 0.5rem; +} diff --git a/report-app/src/app/pages/trajectory/trajectory.ts b/report-app/src/app/pages/trajectory/trajectory.ts new file mode 100644 index 0000000..8648fe3 --- /dev/null +++ b/report-app/src/app/pages/trajectory/trajectory.ts @@ -0,0 +1,12 @@ +import {ChangeDetectionStrategy, Component} from '@angular/core'; +import {ScoreVisualization} from '../../shared/visualization/score-visualization'; +import {ReportFilters} from '../../shared/report-filters/report-filters'; + +@Component({ + selector: 'trajectory', + templateUrl: './trajectory.html', + styleUrls: ['./trajectory.scss'], + imports: [ScoreVisualization, ReportFilters], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class Trajectory {} diff --git a/report-app/src/app/shared/multi-select/multi-select.scss b/report-app/src/app/shared/multi-select/multi-select.scss index e0deba0..eb6744d 100644 --- a/report-app/src/app/shared/multi-select/multi-select.scss +++ b/report-app/src/app/shared/multi-select/multi-select.scss @@ -56,6 +56,6 @@ label { } label:hover { - background-color: #eff6ff; + background-color: var(--button-active-bg-color); border-radius: var(--border-radius); } diff --git a/report-app/src/app/shared/report-filters/report-filters.html b/report-app/src/app/shared/report-filters/report-filters.html new file mode 100644 index 0000000..f36c71a --- /dev/null +++ b/report-app/src/app/shared/report-filters/report-filters.html @@ -0,0 +1,33 @@ +@if (allFrameworks().length > 1) { + +} + +@if (allModels().length > 1) { + +} + +@if (allRunners().length > 1) { + +} + +@if (allLabels().length > 0) { + +} diff --git a/report-app/src/app/shared/report-filters/report-filters.scss b/report-app/src/app/shared/report-filters/report-filters.scss new file mode 100644 index 0000000..a8550c0 --- /dev/null +++ b/report-app/src/app/shared/report-filters/report-filters.scss @@ -0,0 +1,9 @@ +:host { + display: flex; + gap: 1rem; +} + +select, +multi-select { + max-width: 200px; +} diff --git a/report-app/src/app/shared/report-filters/report-filters.ts b/report-app/src/app/shared/report-filters/report-filters.ts new file mode 100644 index 0000000..69f3953 --- /dev/null +++ b/report-app/src/app/shared/report-filters/report-filters.ts @@ -0,0 +1,91 @@ +import {Component, computed, inject, signal} from '@angular/core'; +import {MultiSelect} from '../multi-select/multi-select'; +import {ReportsFetcher} from '../../services/reports-fetcher'; + +/** Renders out a toolbar that filters reports based on the user selection. */ +@Component({ + selector: 'report-filters', + templateUrl: 'report-filters.html', + styleUrl: 'report-filters.scss', + imports: [MultiSelect], +}) +export class ReportFilters { + private reportsFetcher = inject(ReportsFetcher); + protected selectedFramework = signal(null); + protected selectedModel = signal(null); + protected selectedRunner = signal(null); + protected selectedLabels = signal([]); + + protected allFrameworks = computed(() => { + const frameworks = new Map(); + this.reportsFetcher.reportGroups().forEach(group => { + const framework = group.framework.fullStackFramework; + frameworks.set(framework.id, framework.displayName); + }); + return Array.from(frameworks.entries()).map(([id, displayName]) => ({ + id, + displayName, + })); + }); + + protected allModels = computed(() => { + const models = new Set(this.reportsFetcher.reportGroups().map(g => g.model)); + + return Array.from(models).map(model => ({ + id: model, + displayName: model, + })); + }); + + protected allRunners = computed(() => { + const runners = new Map(); + + this.reportsFetcher.reportGroups().forEach(group => { + if (group.runner) { + runners.set(group.runner.id, group.runner.displayName); + } + }); + + return Array.from(runners.entries()).map(([id, displayName]) => ({ + id, + displayName, + })); + }); + + protected allLabels = computed(() => { + const labels = new Set(); + + for (const group of this.reportsFetcher.reportGroups()) { + 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, + })); + }); + + readonly filteredGroups = computed(() => { + const framework = this.selectedFramework(); + const model = this.selectedModel(); + const runner = this.selectedRunner(); + const labels = this.selectedLabels(); + const groups = this.reportsFetcher.reportGroups(); + + 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; + 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/shared/visualization/score-visualization.ts b/report-app/src/app/shared/visualization/score-visualization.ts index 9327fe2..525022b 100644 --- a/report-app/src/app/shared/visualization/score-visualization.ts +++ b/report-app/src/app/shared/visualization/score-visualization.ts @@ -2,6 +2,7 @@ import {afterRenderEffect, Component, ElementRef, inject, input, viewChild} from import {RunGroup} from '../../../../../runner/shared-interfaces'; import {GoogleChartsLoader} from '../../services/google-charts-loader'; import {AppResizeNotifier} from '../../services/app-resize-notifier'; +import {AppColorMode} from '../../services/app-color-mode'; @Component({ selector: 'score-visualization', @@ -10,6 +11,7 @@ import {AppResizeNotifier} from '../../services/app-resize-notifier'; export class ScoreVisualization { private googleChartsLoader = inject(GoogleChartsLoader); private notifier = inject(AppResizeNotifier); + private colorModeService = inject(AppColorMode); readonly groups = input.required(); readonly chartContainer = viewChild.required('chart'); @@ -57,6 +59,7 @@ export class ScoreVisualization { // Note: we need to call `_processData` synchronously // so the wrapping effect picks up the data dependency. const {dataRows, averageAppsCount} = this._processData(); + const colorMode = this.colorModeService.colorMode(); await this.googleChartsLoader.ready; @@ -77,19 +80,29 @@ export class ScoreVisualization { ); const chart = new google.visualization.LineChart(this.chartContainer().nativeElement); + const textColor = colorMode === 'dark' ? '#f9fafb' : '#1e293b'; + chart.draw(table, { curveType: 'function', title: `Score average over time (~${averageAppsCount.toFixed(0)} apps generated per day)`, + titleTextStyle: {color: textColor}, + backgroundColor: 'transparent', vAxis: { format: 'percent', + viewWindowMode: 'maximized', + textStyle: {color: textColor}, + maxValue: 1, }, + legend: {textStyle: {color: textColor}}, hAxis: { minTextSpacing: 20, - textStyle: {fontSize: 10}, + textStyle: {fontSize: 10, color: textColor}, }, chartArea: { left: 50, - right: 300, + right: 155, + bottom: 10, + top: 50, }, // TODO: Consider enabling trendlines. // trendlines: { 0: {}, 1: {} },