Run the selected probes against this completed execution step.
diff --git a/src/app/shared/bias-impact-experiment-dialog/bias-impact-experiment-dialog.ts b/src/app/shared/bias-impact-experiment-dialog/bias-impact-experiment-dialog.ts
index de9b54c..1451f21 100644
--- a/src/app/shared/bias-impact-experiment-dialog/bias-impact-experiment-dialog.ts
+++ b/src/app/shared/bias-impact-experiment-dialog/bias-impact-experiment-dialog.ts
@@ -5,6 +5,7 @@ import { BiasImpactReportViewerComponent } from '@shared/bias-impact-report-view
import { SideEffectPolicySelectorComponent } from '@shared/side-effect-policy-selector/side-effect-policy-selector';
import { ConfirmDialogService } from '@services/dialogs/confirm-dialog';
import { BiasImpactExperimentDialogService } from '@services/dialogs/bias-impact-experiment-dialog';
+import { BiasComparisonViewStateService } from '@services/bias/bias-comparison-view-state';
import { NotificationService } from '@services/notifications/notification';
import { TaskExecutionsService } from '@services/task-executions/task-executions';
import { BiasImpactJob, BiasImpactReport, BiasSideEffectError, ExternalSideEffectPolicy } from '@models/bias-impact';
@@ -23,6 +24,7 @@ export class BiasImpactExperimentDialogHostComponent {
private readonly executions = inject(TaskExecutionsService);
private readonly confirmation = inject(ConfirmDialogService);
private readonly notifications = inject(NotificationService);
+ private readonly comparisonViewState = inject(BiasComparisonViewStateService);
private readonly destroyRef = inject(DestroyRef);
private pollSubscription: Subscription | null = null;
@@ -93,6 +95,13 @@ export class BiasImpactExperimentDialogHostComponent {
this.dialog.close();
}
+ highlightOnCanvas() {
+ const report = this.report();
+ if (!report) return;
+ this.comparisonViewState.show({ report });
+ this.close();
+ }
+
private startPolling(job: BiasImpactJob) {
this.currentJob.set(job);
this.pollSubscription = this.executions.pollBiasImpactJob(job.id).subscribe({
diff --git a/src/app/shared/bias-impact-report-list/bias-impact-report-list.css b/src/app/shared/bias-impact-report-list/bias-impact-report-list.css
new file mode 100644
index 0000000..e027d15
--- /dev/null
+++ b/src/app/shared/bias-impact-report-list/bias-impact-report-list.css
@@ -0,0 +1,34 @@
+.bias-report-list { display: grid; gap: .5rem; padding: .75rem; }
+.bias-report-list__progress { background: #eff6ff; border-left: 3px solid #2563eb; color: #1e3a8a; margin: 0; padding: .6rem .7rem; font-size: .84rem; }
+.bias-report-list__error { background: #fff1f2; border-left: 3px solid #e11d48; color: #9f1239; margin: 0; padding: .6rem .7rem; font-size: .84rem; }
+.bias-report-list__error-row { align-items: center; display: flex; gap: .6rem; justify-content: space-between; }
+.bias-report-list__empty { color: #64748b; font-size: .84rem; margin: 0; padding: .5rem; }
+
+.bias-report-list__row {
+ align-items: center;
+ background: #f8fafc;
+ border: 1px solid #e2e8f0;
+ border-radius: .5rem;
+ cursor: pointer;
+ display: grid;
+ gap: .2rem .75rem;
+ grid-template-columns: auto auto 1fr auto auto auto;
+ padding: .6rem .75rem;
+ text-align: left;
+ transition: background-color 120ms ease;
+}
+.bias-report-list__row:hover { background: #f1f5f9; }
+
+.bias-report-list__kind { background: #e0e7ff; border-radius: 999px; color: #3730a3; font-size: .68rem; font-weight: 700; padding: .15rem .45rem; white-space: nowrap; }
+.bias-report-list__date { color: #64748b; font-size: .78rem; white-space: nowrap; }
+.bias-report-list__summary { color: #1e293b; font-size: .84rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.bias-report-list__node { color: #64748b; font-size: .72rem; overflow-wrap: anywhere; }
+.bias-report-list__annotations { color: #64748b; font-size: .76rem; white-space: nowrap; }
+.bias-report-list__changed { color: #64748b; font-size: .76rem; font-weight: 600; white-space: nowrap; }
+.bias-report-list__changed.changed { color: #b45309; }
+
+.bias-report-list__detail { display: grid; gap: .9rem; padding: .75rem; }
+
+@media (max-width: 640px) {
+ .bias-report-list__row { grid-template-columns: 1fr; }
+}
diff --git a/src/app/shared/bias-impact-report-list/bias-impact-report-list.html b/src/app/shared/bias-impact-report-list/bias-impact-report-list.html
new file mode 100644
index 0000000..988c330
--- /dev/null
+++ b/src/app/shared/bias-impact-report-list/bias-impact-report-list.html
@@ -0,0 +1,40 @@
+@if (detailMode()) {
+
+
+ @if (detailLoading()) {
+
Loading report…
+ }
+ @if (detailError(); as error) {
+
{{ error }}
+ }
+ @if (selectedReport(); as report) {
+
+ }
+
+} @else {
+
+ @if (loading()) {
+
Loading bias impact reports…
+ } @else if (listError(); as error) {
+
+ } @else if (!reports().length) {
+
No bias impact reports for this execution yet.
+ } @else {
+ @for (report of reports(); track report.id) {
+
+ }
+ }
+
+}
diff --git a/src/app/shared/bias-impact-report-list/bias-impact-report-list.spec.ts b/src/app/shared/bias-impact-report-list/bias-impact-report-list.spec.ts
new file mode 100644
index 0000000..2c43d9b
--- /dev/null
+++ b/src/app/shared/bias-impact-report-list/bias-impact-report-list.spec.ts
@@ -0,0 +1,119 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { of, throwError } from 'rxjs';
+import { vi } from 'vitest';
+import { BiasImpactReportListComponent } from './bias-impact-report-list';
+import { TaskExecutionsService } from '@services/task-executions/task-executions';
+import { BiasImpactReport } from '@models/bias-impact';
+
+function makeReport(overrides: Partial
= {}): BiasImpactReport {
+ return {
+ id: 'report-1',
+ experimentId: 'experiment-1',
+ kind: 'ISOLATED_STEP',
+ baselineExecutionId: 'execution-1',
+ biasedExecutionId: null,
+ nodeId: 'node-1',
+ annotationIds: ['annotation-1', 'annotation-2'],
+ repetitions: 3,
+ createdAt: '2026-07-21T10:00:00',
+ rawOutputsIncluded: true,
+ immediateImpact: {
+ outputChanged: true,
+ maximumTextDifference: 0.4,
+ changeRate: 0.5,
+ baselineOutput: { output: 'baseline' },
+ biasedOutputs: [{ output: 'variant' }]
+ },
+ downstreamImpact: [],
+ routingChanges: [],
+ mockedSideEffects: [],
+ summary: 'Output changed',
+ warnings: [],
+ ...overrides
+ };
+}
+
+describe('BiasImpactReportListComponent', () => {
+ let fixture: ComponentFixture;
+ let listBiasImpactReports: ReturnType;
+ let getBiasImpactReport: ReturnType;
+
+ beforeEach(async () => {
+ listBiasImpactReports = vi.fn().mockReturnValue(of([makeReport()]));
+ getBiasImpactReport = vi.fn().mockReturnValue(of(makeReport()));
+
+ await TestBed.configureTestingModule({
+ imports: [BiasImpactReportListComponent],
+ providers: [
+ { provide: TaskExecutionsService, useValue: { listBiasImpactReports, getBiasImpactReport } }
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(BiasImpactReportListComponent);
+ });
+
+ it('loads and renders the reports for the given execution', () => {
+ fixture.componentRef.setInput('executionId', 'execution-1');
+ fixture.detectChanges();
+
+ expect(listBiasImpactReports).toHaveBeenCalledWith('execution-1');
+ expect(fixture.componentInstance.reports().length).toBe(1);
+ expect(fixture.nativeElement.querySelectorAll('.bias-report-list__row').length).toBe(1);
+ });
+
+ it('shows an empty state when there are no reports', () => {
+ listBiasImpactReports.mockReturnValue(of([]));
+ fixture.componentRef.setInput('executionId', 'execution-1');
+ fixture.detectChanges();
+
+ expect(fixture.nativeElement.textContent).toContain('No bias impact reports');
+ });
+
+ it('shows an inline error with a retry action when the list fails to load', () => {
+ listBiasImpactReports.mockReturnValue(throwError(() => new Error('network down')));
+ fixture.componentRef.setInput('executionId', 'execution-1');
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.listError()).toContain('Unable to load');
+ listBiasImpactReports.mockReturnValue(of([makeReport()]));
+ fixture.componentInstance.retry();
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.listError()).toBeNull();
+ expect(fixture.componentInstance.reports().length).toBe(1);
+ });
+
+ it('opens the report detail via getBiasImpactReport and can navigate back to the list', () => {
+ fixture.componentRef.setInput('executionId', 'execution-1');
+ fixture.detectChanges();
+
+ fixture.componentInstance.openDetail('report-1');
+ fixture.detectChanges();
+
+ expect(getBiasImpactReport).toHaveBeenCalledWith('report-1');
+ expect(fixture.componentInstance.detailMode()).toBe(true);
+ expect(fixture.nativeElement.querySelector('app-bias-impact-report-viewer')).not.toBeNull();
+
+ fixture.componentInstance.closeDetail();
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.detailMode()).toBe(false);
+ });
+
+ it('shows the same inline message for a report that is missing or not accessible', () => {
+ getBiasImpactReport.mockReturnValue(throwError(() => ({ status: 404 })));
+ fixture.componentRef.setInput('executionId', 'execution-1');
+ fixture.detectChanges();
+
+ fixture.componentInstance.openDetail('missing-report');
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.detailError()).toBe('Report not found or not accessible.');
+
+ getBiasImpactReport.mockReturnValue(throwError(() => ({ status: 403 })));
+ fixture.componentInstance.openDetail('someone-elses-report');
+ fixture.detectChanges();
+
+ expect(fixture.componentInstance.detailError()).toBe('Report not found or not accessible.');
+ });
+});
diff --git a/src/app/shared/bias-impact-report-list/bias-impact-report-list.ts b/src/app/shared/bias-impact-report-list/bias-impact-report-list.ts
new file mode 100644
index 0000000..3bd70ed
--- /dev/null
+++ b/src/app/shared/bias-impact-report-list/bias-impact-report-list.ts
@@ -0,0 +1,103 @@
+import { CommonModule } from '@angular/common';
+import { ChangeDetectionStrategy, Component, computed, effect, inject, input, signal } from '@angular/core';
+import { MatButtonModule } from '@angular/material/button';
+import { BiasImpactReport } from '@models/bias-impact';
+import { TaskExecutionsService } from '@services/task-executions/task-executions';
+import { BiasComparisonViewStateService } from '@services/bias/bias-comparison-view-state';
+import { BiasImpactReportViewerComponent } from '@shared/bias-impact-report-viewer/bias-impact-report-viewer';
+
+@Component({
+ selector: 'app-bias-impact-report-list',
+ standalone: true,
+ imports: [CommonModule, MatButtonModule, BiasImpactReportViewerComponent],
+ templateUrl: './bias-impact-report-list.html',
+ styleUrl: './bias-impact-report-list.css',
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class BiasImpactReportListComponent {
+ private readonly executions = inject(TaskExecutionsService);
+ private readonly comparisonViewState = inject(BiasComparisonViewStateService);
+ private lastExecutionId: string | null = null;
+
+ readonly executionId = input(null);
+
+ readonly reports = signal([]);
+ readonly loading = signal(false);
+ readonly listError = signal(null);
+
+ readonly selectedReport = signal(null);
+ readonly detailLoading = signal(false);
+ readonly detailError = signal(null);
+
+ readonly detailMode = computed(() =>
+ this.selectedReport() !== null || this.detailLoading() || this.detailError() !== null
+ );
+
+ constructor() {
+ effect(() => {
+ const executionId = this.executionId();
+ if (executionId === this.lastExecutionId) return;
+ this.lastExecutionId = executionId;
+ this.closeDetail();
+ this.loadReports(executionId);
+ });
+ }
+
+ retry() {
+ this.loadReports(this.executionId());
+ }
+
+ openDetail(reportId: string) {
+ this.selectedReport.set(null);
+ this.detailError.set(null);
+ this.detailLoading.set(true);
+ this.executions.getBiasImpactReport(reportId).subscribe({
+ next: (report) => {
+ this.detailLoading.set(false);
+ this.selectedReport.set(report);
+ },
+ error: () => {
+ this.detailLoading.set(false);
+ this.detailError.set('Report not found or not accessible.');
+ }
+ });
+ }
+
+ closeDetail() {
+ this.selectedReport.set(null);
+ this.detailLoading.set(false);
+ this.detailError.set(null);
+ }
+
+ highlightOnCanvas() {
+ const report = this.selectedReport();
+ if (!report) return;
+ this.comparisonViewState.show({ report });
+ }
+
+ annotationCount(report: BiasImpactReport): number {
+ return report.annotationIds?.length ?? 0;
+ }
+
+ private loadReports(executionId: string | null) {
+ if (!executionId) {
+ this.reports.set([]);
+ this.listError.set(null);
+ this.loading.set(false);
+ return;
+ }
+
+ this.loading.set(true);
+ this.listError.set(null);
+ this.executions.listBiasImpactReports(executionId).subscribe({
+ next: (reports) => {
+ this.loading.set(false);
+ this.reports.set(reports);
+ },
+ error: () => {
+ this.loading.set(false);
+ this.listError.set('Unable to load the bias impact reports for this execution.');
+ }
+ });
+ }
+}
diff --git a/src/app/shared/bias-impact-report-viewer/bias-impact-report-viewer.css b/src/app/shared/bias-impact-report-viewer/bias-impact-report-viewer.css
index 9cdb582..3c8e242 100644
--- a/src/app/shared/bias-impact-report-viewer/bias-impact-report-viewer.css
+++ b/src/app/shared/bias-impact-report-viewer/bias-impact-report-viewer.css
@@ -2,6 +2,9 @@
.bias-impact-report__header, .bias-impact-report__section-heading, .bias-impact-report__node-heading, .bias-impact-report__routing { align-items: center; display: flex; gap: .65rem; justify-content: space-between; }
.bias-impact-report__header h3, .bias-impact-report__section h4 { margin: 0; }
.bias-impact-report__header time { color: #64748b; font-size: .84rem; }
+.bias-impact-report__header-actions { align-items: center; display: flex; gap: .65rem; }
+.bias-impact-report__highlight-btn { background: #ccfbf1; border: 1px solid #99f6e4; border-radius: .4rem; color: #0f766e; cursor: pointer; font-size: .78rem; font-weight: 600; padding: .35rem .65rem; }
+.bias-impact-report__highlight-btn:hover { background: #99f6e4; }
.bias-impact-report__kind { background: #e0e7ff; border-radius: 999px; color: #3730a3; font-size: .72rem; font-weight: 700; padding: .2rem .5rem; }
.bias-impact-report__metadata { display: grid; gap: .55rem; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin: 0; }
.bias-impact-report__metadata div { background: #f8fafc; border-radius: .4rem; padding: .55rem; }
diff --git a/src/app/shared/bias-impact-report-viewer/bias-impact-report-viewer.html b/src/app/shared/bias-impact-report-viewer/bias-impact-report-viewer.html
index e850c18..5c9c5fb 100644
--- a/src/app/shared/bias-impact-report-viewer/bias-impact-report-viewer.html
+++ b/src/app/shared/bias-impact-report-viewer/bias-impact-report-viewer.html
@@ -5,7 +5,12 @@
{{ currentReport.kind }}
Bias impact report
-