From 643909b2d0563f20091844ae040b1288a8864cd5 Mon Sep 17 00:00:00 2001 From: Dany Marques Date: Thu, 12 Feb 2026 09:52:01 +0100 Subject: [PATCH 1/2] feat: display prompt names as badges in report list view Add promptNames field to RunGroup interface and collect prompt names during report grouping. Display them as neutral status badges in the report list UI with backward compatibility for older groups.json files. --- report-app/src/app/pages/report-list/report-list.html | 7 +++++++ report-app/src/app/pages/report-list/report-list.scss | 9 +++++++++ runner/orchestration/grouping.ts | 3 +++ runner/reporting/report-local-disk.ts | 5 +++++ runner/shared-interfaces.ts | 2 ++ 5 files changed, 26 insertions(+) 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 2093eca0..4856aa5d 100644 --- a/report-app/src/app/pages/report-list/report-list.html +++ b/report-app/src/app/pages/report-list/report-list.html @@ -37,6 +37,13 @@ } + @if (group.promptNames.length) { + + }
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 d8ccf521..bc8bbd36 100644 --- a/report-app/src/app/pages/report-list/report-list.scss +++ b/report-app/src/app/pages/report-list/report-list.scss @@ -106,6 +106,15 @@ h1, h2 { padding: 0 20px; } +.prompt-names { + margin-top: 0.4rem; + + .status-badge { + font-size: 0.75rem; + font-weight: 400; + } +} + .select-for-comparison input[type='checkbox'] { width: 20px; height: 20px; diff --git a/runner/orchestration/grouping.ts b/runner/orchestration/grouping.ts index 0135cf70..054c17c6 100644 --- a/runner/orchestration/grouping.ts +++ b/runner/orchestration/grouping.ts @@ -55,6 +55,7 @@ export function groupSimilarReports(inputRuns: RunInfo[]): RunGroup[] { const groupResults: AssessmentResult[] = []; const firstRun = groupRuns[0]; const labels = new Set(); + const promptNames = new Set(); let totalForGroup = 0; let maxForGroup = 0; let appsCount = 0; @@ -70,6 +71,7 @@ export function groupSimilarReports(inputRuns: RunInfo[]): RunGroup[] { totalForRun += result.score.totalPoints; maxForRun += result.score.maxOverallPoints; groupResults.push(result); + promptNames.add(result.promptDef.name); } // `|| 0` in case there are no results, otherwise we'll get NaN. @@ -90,6 +92,7 @@ export function groupSimilarReports(inputRuns: RunInfo[]): RunGroup[] { maxOverallPoints: maxForGroup / groupRuns.length || 0, appsCount, labels: Array.from(labels), + promptNames: Array.from(promptNames), environmentId: firstRun.details.summary.environmentId, framework: firstRun.details.summary.framework, model: firstRun.details.summary.model, diff --git a/runner/reporting/report-local-disk.ts b/runner/reporting/report-local-disk.ts index 1d3b8039..b695c364 100644 --- a/runner/reporting/report-local-disk.ts +++ b/runner/reporting/report-local-disk.ts @@ -37,6 +37,11 @@ export async function fetchReportsFromDisk(directory: string): Promise r.promptDef.name); + data.set(group.id, {group, run}); }), ); diff --git a/runner/shared-interfaces.ts b/runner/shared-interfaces.ts index b08c8980..9a61c19f 100644 --- a/runner/shared-interfaces.ts +++ b/runner/shared-interfaces.ts @@ -637,6 +637,8 @@ export interface RunGroup { }; /** Runner used to generate code for the runs in the group. */ runner?: CodegenRunnerInfo; + /** Names of prompts that were evaluated in this group. */ + promptNames: string[]; } /** Request information for a file generation. */ From 211a8eb3e49aa5bfc4648e11cbf261410a7d8e99 Mon Sep 17 00:00:00 2001 From: Dany Marques Date: Mon, 16 Mar 2026 12:03:01 +0100 Subject: [PATCH 2/2] feat(report-list): add collapsible prompt badge list component Extract prompt name badges into a dedicated `PromptBadgeList` component that automatically truncates overflow badges to a single row and shows a "+N more" toggle. Uses ResizeObserver and afterNextRender to measure badge layout after each render cycle and reacts to container width changes. - Add `PromptBadgeList` component with overflow detection via DOM measurement - Hide the container during measurement to avoid layout flicker - Show "Show less" / "+N more" toggle badge when names overflow one row - Move prompt-names styles from report-list.scss into the component - Replace inline badge list in report-list.html with the new component --- .../prompt-badge/prompt-badge-list.html | 21 +++ .../prompt-badge/prompt-badge-list.ts | 140 ++++++++++++++++++ .../app/pages/report-list/report-list.html | 6 +- .../app/pages/report-list/report-list.scss | 8 - .../src/app/pages/report-list/report-list.ts | 4 +- 5 files changed, 165 insertions(+), 14 deletions(-) create mode 100644 report-app/src/app/pages/report-list/prompt-badge/prompt-badge-list.html create mode 100644 report-app/src/app/pages/report-list/prompt-badge/prompt-badge-list.ts diff --git a/report-app/src/app/pages/report-list/prompt-badge/prompt-badge-list.html b/report-app/src/app/pages/report-list/prompt-badge/prompt-badge-list.html new file mode 100644 index 00000000..1da62d80 --- /dev/null +++ b/report-app/src/app/pages/report-list/prompt-badge/prompt-badge-list.html @@ -0,0 +1,21 @@ +
    + @for (name of visibleNames(); track name) { +
  • {{ name }}
  • + } + @if (showToggle()) { +
  • + @if (expanded()) { + Show less + } @else { + +{{ hiddenCount() }} more + } +
  • + } +
diff --git a/report-app/src/app/pages/report-list/prompt-badge/prompt-badge-list.ts b/report-app/src/app/pages/report-list/prompt-badge/prompt-badge-list.ts new file mode 100644 index 00000000..36ae26b2 --- /dev/null +++ b/report-app/src/app/pages/report-list/prompt-badge/prompt-badge-list.ts @@ -0,0 +1,140 @@ +import { + Component, + ElementRef, + Injector, + PLATFORM_ID, + afterNextRender, + computed, + effect, + inject, + input, + signal, + viewChild, +} from '@angular/core'; +import {isPlatformServer} from '@angular/common'; + +@Component({ + selector: 'prompt-badge-list', + templateUrl: './prompt-badge-list.html', + styles: ` + .prompt-names { + margin-top: 0.4rem; + display: flex; + width: 100%; + } + + .status-badge { + font-size: 0.75rem; + font-weight: 400; + } + + .toggle-badge { + cursor: pointer; + font-weight: 500; + + &:hover { + opacity: 0.8; + } + } + `, +}) +export class PromptBadgeList { + readonly promptNames = input.required(); + + private readonly isServer = isPlatformServer(inject(PLATFORM_ID)); + private readonly injector = inject(Injector); + private lastContainerWidth = 0; + private pendingMeasure = false; + private containerRef = viewChild.required>('container'); + + protected measuring = signal(true); + protected expanded = signal(false); + protected visibleCount = signal(Infinity); + + protected visibleNames = computed(() => { + const names = this.promptNames(); + if (this.expanded() || this.measuring()) { + return names; + } + const count = this.visibleCount(); + return isFinite(count) ? names.slice(0, count) : names; + }); + + protected hiddenCount = computed(() => { + if (this.expanded() || this.measuring() || !isFinite(this.visibleCount())) { + return 0; + } + return Math.max(0, this.promptNames().length - this.visibleCount()); + }); + + protected showToggle = computed(() => { + if (this.measuring()) { + return false; + } + if (this.expanded()) { + return isFinite(this.visibleCount()); + } + return this.hiddenCount() > 0; + }); + + constructor() { + effect(onCleanup => { + if (this.isServer) { + this.measuring.set(false); + return; + } + const el = this.containerRef().nativeElement; + const observer = new ResizeObserver(entries => { + if (this.pendingMeasure) { + return; + } + const newWidth = Math.round(entries[0].contentRect.width); + if (newWidth !== this.lastContainerWidth) { + this.lastContainerWidth = newWidth; + this.scheduleMeasure(); + } + }); + observer.observe(el); + this.scheduleMeasure(); + + onCleanup(() => observer.disconnect()); + }); + } + + protected toggle(event: Event): void { + event.preventDefault(); + event.stopPropagation(); + this.expanded.update(v => !v); + if (!this.expanded()) { + this.scheduleMeasure(); + } + } + + private scheduleMeasure(): void { + if (this.pendingMeasure) { + return; + } + this.pendingMeasure = true; + this.measuring.set(true); + afterNextRender({read: () => this.doMeasure()}, {injector: this.injector}); + } + + private doMeasure(): void { + this.pendingMeasure = false; + + const container = this.containerRef().nativeElement; + const badges = Array.from(container.querySelectorAll('[data-badge]')) as HTMLElement[]; + + if (badges.length === 0) { + this.measuring.set(false); + return; + } + + const firstTop = badges[0].offsetTop; + const overflowIdx = badges.findIndex(b => b.offsetTop > firstTop); + + // If overflow is detected, reserve one slot for the toggle badge + this.visibleCount.set(overflowIdx === -1 ? Infinity : Math.max(1, overflowIdx - 1)); + this.measuring.set(false); + } +} 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 4856aa5d..dcc1ca3c 100644 --- a/report-app/src/app/pages/report-list/report-list.html +++ b/report-app/src/app/pages/report-list/report-list.html @@ -38,11 +38,7 @@ }
@if (group.promptNames.length) { - + }
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 bc8bbd36..e3bd3552 100644 --- a/report-app/src/app/pages/report-list/report-list.scss +++ b/report-app/src/app/pages/report-list/report-list.scss @@ -106,14 +106,6 @@ h1, h2 { padding: 0 20px; } -.prompt-names { - margin-top: 0.4rem; - - .status-badge { - font-size: 0.75rem; - font-weight: 400; - } -} .select-for-comparison input[type='checkbox'] { width: 20px; 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 6ae2df3d..02787622 100644 --- a/report-app/src/app/pages/report-list/report-list.ts +++ b/report-app/src/app/pages/report-list/report-list.ts @@ -1,4 +1,4 @@ -import {Component, computed, inject, PLATFORM_ID, signal} from '@angular/core'; +import {Component, inject, PLATFORM_ID, signal} from '@angular/core'; import {Router, RouterLink} from '@angular/router'; import {ReportsFetcher} from '../../services/reports-fetcher'; import {DatePipe, isPlatformServer} from '@angular/common'; @@ -12,6 +12,7 @@ import {Score} from '../../shared/score/score'; import {ProviderLabel} from '../../shared/provider-label'; import {bucketToScoreVariable} from '../../shared/scoring'; import {ReportFilters} from '../../shared/report-filters/report-filters'; +import {PromptBadgeList} from './prompt-badge/prompt-badge-list'; @Component({ selector: 'app-report-list', @@ -23,6 +24,7 @@ import {ReportFilters} from '../../shared/report-filters/report-filters'; Score, ProviderLabel, ReportFilters, + PromptBadgeList, ], templateUrl: './report-list.html', styleUrls: ['./report-list.scss'],