Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<ul
#container
class="status-badge-group prompt-names"
[style.visibility]="measuring() ? 'hidden' : 'visible'"
>
@for (name of visibleNames(); track name) {
<li class="status-badge neutral" data-badge>{{ name }}</li>
}
@if (showToggle()) {
<li
class="status-badge neutral toggle-badge"
(click)="toggle($event)"
>
@if (expanded()) {
Show less
} @else {
+{{ hiddenCount() }} more
}
</li>
}
</ul>
Original file line number Diff line number Diff line change
@@ -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<string[]>();

private readonly isServer = isPlatformServer(inject(PLATFORM_ID));
private readonly injector = inject(Injector);
private lastContainerWidth = 0;
private pendingMeasure = false;
private containerRef = viewChild.required<ElementRef<HTMLElement>>('container');

protected measuring = signal(true);
protected expanded = signal(false);
protected visibleCount = signal<number>(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);
}
}
3 changes: 3 additions & 0 deletions report-app/src/app/pages/report-list/report-list.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
</ul>
}
</div>
@if (group.promptNames.length) {
<prompt-badge-list [promptNames]="group.promptNames" />
}
</div>
<div class="run-meta-container">
<div class="run-meta">
Expand Down
1 change: 1 addition & 0 deletions report-app/src/app/pages/report-list/report-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ h1, h2 {
padding: 0 20px;
}


.select-for-comparison input[type='checkbox'] {
width: 20px;
height: 20px;
Expand Down
4 changes: 3 additions & 1 deletion report-app/src/app/pages/report-list/report-list.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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',
Expand All @@ -23,6 +24,7 @@ import {ReportFilters} from '../../shared/report-filters/report-filters';
Score,
ProviderLabel,
ReportFilters,
PromptBadgeList,
],
templateUrl: './report-list.html',
styleUrls: ['./report-list.scss'],
Expand Down
3 changes: 3 additions & 0 deletions runner/orchestration/grouping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function groupSimilarReports(inputRuns: RunInfo[]): RunGroup[] {
const groupResults: AssessmentResult[] = [];
const firstRun = groupRuns[0];
const labels = new Set<string>();
const promptNames = new Set<string>();
let totalForGroup = 0;
let maxForGroup = 0;
let appsCount = 0;
Expand All @@ -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.
Expand All @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions runner/reporting/report-local-disk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export async function fetchReportsFromDisk(directory: string): Promise<FetchedLo
// were part of the same invocation. Add a unique suffix to the ID to
// prevent further grouping.
run.group = group.id = `${group.id}-l${index}`;

// Derive prompt names from the run data for backward compatibility
// with older groups.json files that don't have the field.
group.promptNames ??= run.results.map(r => r.promptDef.name);

data.set(group.id, {group, run});
}),
);
Expand Down
2 changes: 2 additions & 0 deletions runner/shared-interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
Loading