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: {} },