Show every LLM assessment as a history, and drop one that arrives too late
The report carried a single `judge`, so asking a second model overwrote the first - there was no way to compare two opinions, and reopening a report after a re-evaluation only ever showed the newest one. The viewer now renders `judgements`, newest first, each collapsible: the current one open and labelled so, the earlier ones a click away with their own verdicts, narrative and errors. A report saved with the old single field still reads, as a history of one. Also: closing the dialog while an assessment was running left `judging` stuck true forever, so reopening any report showed a disabled button stuck on "Evaluating...". And opening a different report while one was still running let the late answer land on it, silently replacing the report on screen with someone else's assessment. Both dialogs now carry a token that advances whenever what they're showing changes; a response that arrives after its token is stale gets discarded instead of applied. The job itself is unaffected - it keeps running server-side and its verdicts land on the report regardless, which is what makes reopening it later still show them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
03ac4901ed
commit
984af08f36
|
|
@ -167,6 +167,13 @@ export type BiasJudgeSummary = {
|
|||
judgedPairs: number;
|
||||
skippedPairs: number;
|
||||
errors: string[];
|
||||
/**
|
||||
* What this assessment said about each compared pair, by the path the comparison hands out.
|
||||
*
|
||||
* Carried so that a second opinion does not erase the first. The per-subject badges follow the
|
||||
* most recent assessment, which the service attaches to the pairs themselves.
|
||||
*/
|
||||
verdicts?: Record<string, BiasJudgeVerdict>;
|
||||
};
|
||||
|
||||
/** One element of a compared list value, which for an iterator container is one subject. */
|
||||
|
|
@ -297,8 +304,8 @@ export type BiasImpactReport = {
|
|||
warnings: string[];
|
||||
interventionDirection?: BiasInterventionDirection;
|
||||
schemaVersion?: number;
|
||||
/** Present only once someone has asked a model to assess the comparison. */
|
||||
judge?: BiasJudgeSummary | null;
|
||||
/** Every assessment asked of a model, newest first. Empty until one is asked for. */
|
||||
judgements?: BiasJudgeSummary[];
|
||||
simulation?: BiasSimulationContext | null;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -751,17 +751,22 @@ export class TaskExecutionsCallServiceFake extends TaskExecutionsCallServiceBase
|
|||
items: value.items.map((item) => ({ ...item, judgeVerdict: item.changed ? verdict : null }))
|
||||
}))
|
||||
},
|
||||
judge: {
|
||||
judge: { provider: judge?.provider ?? 'InternalOllama', model: judge?.model ?? 'demo-model' },
|
||||
judgedAt: new Date().toISOString(),
|
||||
impact: 'SUBSTANTIVE',
|
||||
attribution: 'INJECTION',
|
||||
narrative: 'Two of the three subjects are assessed differently, in the direction the probe describes. '
|
||||
+ 'A single pair of runs cannot separate that from ordinary model variation.',
|
||||
judgedPairs: 2,
|
||||
skippedPairs: 1,
|
||||
errors: []
|
||||
}
|
||||
// Newest first, keeping whatever assessments the report already had: asking a second model is
|
||||
// the point of asking again.
|
||||
judgements: [
|
||||
{
|
||||
judge: { provider: judge?.provider ?? 'InternalOllama', model: judge?.model ?? 'demo-model' },
|
||||
judgedAt: new Date().toISOString(),
|
||||
impact: 'SUBSTANTIVE',
|
||||
attribution: 'INJECTION',
|
||||
narrative: 'Two of the three subjects are assessed differently, in the direction the probe describes. '
|
||||
+ 'A single pair of runs cannot separate that from ordinary model variation.',
|
||||
judgedPairs: 2,
|
||||
skippedPairs: 1,
|
||||
errors: []
|
||||
},
|
||||
...(report.judgements ?? [])
|
||||
]
|
||||
};
|
||||
this.biasReports[index] = judged;
|
||||
return judged;
|
||||
|
|
|
|||
|
|
@ -432,7 +432,7 @@ describe('TaskExecutionsCallService bias APIs', () => {
|
|||
biasedSimulator: { provider: 'InternalOllama', model: 'llama3' },
|
||||
comparable: false
|
||||
},
|
||||
judge: {
|
||||
judgements: [{
|
||||
judge: { provider: 'InternalOllama', model: 'gemma:7b' },
|
||||
judgedAt: '2026-07-21T11:00:00',
|
||||
impact: 'DECISIVE',
|
||||
|
|
@ -440,8 +440,9 @@ describe('TaskExecutionsCallService bias APIs', () => {
|
|||
narrative: 'The decision flipped.',
|
||||
judgedPairs: 1,
|
||||
skippedPairs: 1,
|
||||
errors: ['one pair could not be assessed']
|
||||
}
|
||||
errors: ['one pair could not be assessed'],
|
||||
verdicts: { 'immediate:node-1.response': { impact: 'DECISIVE', attribution: 'INJECTION', confidence: 0.9, changedAspects: [], rationale: 'flipped', error: null } }
|
||||
}]
|
||||
});
|
||||
|
||||
const mapped = await result;
|
||||
|
|
@ -451,14 +452,36 @@ describe('TaskExecutionsCallService bias APIs', () => {
|
|||
expect(value.items[1].judgeVerdict).toBeNull();
|
||||
expect(mapped.immediateImpact.iterations![0].containerNodeName).toBe('score-cvs');
|
||||
expect(mapped.outcomeChanges).toEqual([{ baselineOutcomeCodes: ['REVISE'], biasedOutcomeCodes: ['ACCEPT'] }]);
|
||||
expect(mapped.judge!.impact).toBe('DECISIVE');
|
||||
expect(mapped.judge!.judge.model).toBe('gemma:7b');
|
||||
expect(mapped.judge!.errors).toEqual(['one pair could not be assessed']);
|
||||
expect(mapped.judgements!.length).toBe(1);
|
||||
expect(mapped.judgements![0].impact).toBe('DECISIVE');
|
||||
expect(mapped.judgements![0].judge.model).toBe('gemma:7b');
|
||||
expect(mapped.judgements![0].errors).toEqual(['one pair could not be assessed']);
|
||||
expect(mapped.simulation!.comparable).toBe(false);
|
||||
expect(mapped.simulation!.baselineSimulator!.parameters).toEqual({ seed: 7 });
|
||||
expect(mapped.simulation!.biasedSimulator!.model).toBe('llama3');
|
||||
});
|
||||
|
||||
it('reads a report that could only hold one assessment as a history of one', async () => {
|
||||
const result = firstValueFrom(service.getBiasImpactReport('report-1'));
|
||||
httpMock.expectOne(`${environment.apiUrl}/executions/bias-impact-reports/report-1`).flush({
|
||||
...report,
|
||||
judge: {
|
||||
judge: { provider: 'InternalOllama', model: 'gemma:7b' },
|
||||
judgedAt: '2026-07-21T11:00:00',
|
||||
impact: 'COSMETIC',
|
||||
attribution: 'NON_DETERMINISM',
|
||||
narrative: 'Only the wording moved.',
|
||||
judgedPairs: 1,
|
||||
skippedPairs: 0,
|
||||
errors: []
|
||||
}
|
||||
});
|
||||
|
||||
const mapped = await result;
|
||||
expect(mapped.judgements!.length).toBe(1);
|
||||
expect(mapped.judgements![0].impact).toBe('COSMETIC');
|
||||
});
|
||||
|
||||
it('leaves the new sections empty for a report that predates them', async () => {
|
||||
const result = firstValueFrom(service.getBiasImpactReport('report-1'));
|
||||
httpMock.expectOne(`${environment.apiUrl}/executions/bias-impact-reports/report-1`).flush(report);
|
||||
|
|
@ -466,7 +489,7 @@ describe('TaskExecutionsCallService bias APIs', () => {
|
|||
const mapped = await result;
|
||||
expect(mapped.immediateImpact.values).toEqual([]);
|
||||
expect(mapped.immediateImpact.iterations).toEqual([]);
|
||||
expect(mapped.judge).toBeNull();
|
||||
expect(mapped.judgements).toEqual([]);
|
||||
expect(mapped.simulation).toBeNull();
|
||||
expect(mapped.schemaVersion).toBe(1);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -485,7 +485,7 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase {
|
|||
summary: String(value['summary'] ?? ''),
|
||||
warnings: this.toStringArray(value['warnings']),
|
||||
schemaVersion: this.toNumber(value['schemaVersion'], 1),
|
||||
judge: this.toJudgeSummary(value['judge']),
|
||||
judgements: this.toJudgements(value),
|
||||
simulation: this.toSimulationContext(value['simulation']),
|
||||
...(value['interventionDirection'] === 'BIAS' || value['interventionDirection'] === 'MITIGATION' || value['interventionDirection'] === 'BOTH'
|
||||
? { interventionDirection: value['interventionDirection'] }
|
||||
|
|
@ -581,6 +581,22 @@ export class TaskExecutionsCallService extends TaskExecutionsCallServiceBase {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The assessments, newest first.
|
||||
*
|
||||
* <p>A report written when there was room for only one carries a `judge` object instead; read as
|
||||
* a history of one so an older report still shows what a model said about it.
|
||||
*/
|
||||
private toJudgements(value: Record<string, unknown>): BiasJudgeSummary[] {
|
||||
const history = Array.isArray(value['judgements'])
|
||||
? value['judgements'].map((item) => this.toJudgeSummary(item))
|
||||
: [];
|
||||
const present = history.filter((summary): summary is BiasJudgeSummary => summary !== null);
|
||||
if (present.length) return present;
|
||||
const legacy = this.toJudgeSummary(value['judge']);
|
||||
return legacy ? [legacy] : [];
|
||||
}
|
||||
|
||||
private toJudgeSummary(raw: unknown): BiasJudgeSummary | null {
|
||||
if (!raw || typeof raw !== 'object') return null;
|
||||
const value = this.toRecord(raw);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,15 @@ export class BiasCompareDialogHostComponent {
|
|||
readonly judging = signal(false);
|
||||
readonly judgeError = signal<string | null>(null);
|
||||
|
||||
/**
|
||||
* Which assessment is the one still worth showing.
|
||||
*
|
||||
* <p>Closing the dialog does not stop the job - it runs on the server and stores its verdicts on
|
||||
* the report, so nothing is lost and reopening shows them. What must not happen is a late answer
|
||||
* landing on a dialog that has since been closed, or reopened on a different report.
|
||||
*/
|
||||
private judgeToken = 0;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const state = this.state();
|
||||
|
|
@ -39,6 +48,8 @@ export class BiasCompareDialogHostComponent {
|
|||
this.inlineError.set(null);
|
||||
this.judgeError.set(null);
|
||||
this.loading.set(false);
|
||||
this.judging.set(false);
|
||||
this.judgeToken++;
|
||||
if (!state) return;
|
||||
|
||||
this.runCompare(state.baselineExecutionId, state.biasedExecutionId);
|
||||
|
|
@ -60,15 +71,18 @@ export class BiasCompareDialogHostComponent {
|
|||
const report = this.report();
|
||||
if (!report || this.judging()) return;
|
||||
|
||||
const token = ++this.judgeToken;
|
||||
this.judging.set(true);
|
||||
this.judgeError.set(null);
|
||||
try {
|
||||
const judged = await this.reportJudge.assess(report.id);
|
||||
if (token !== this.judgeToken) return;
|
||||
if (judged) this.report.set(judged);
|
||||
} catch (error) {
|
||||
if (token !== this.judgeToken) return;
|
||||
this.judgeError.set(extractBiasErrorMessage(error, 'Unable to evaluate this comparison with an LLM.'));
|
||||
} finally {
|
||||
this.judging.set(false);
|
||||
if (token === this.judgeToken) this.judging.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,14 @@ export class BiasImpactExperimentDialogHostComponent {
|
|||
readonly state = this.dialog.state;
|
||||
readonly judging = signal(false);
|
||||
readonly judgeError = signal<string | null>(null);
|
||||
/**
|
||||
* Which assessment is the one still worth showing.
|
||||
*
|
||||
* <p>Closing the dialog does not stop the job - it runs on the server and stores its verdicts on
|
||||
* the report, so nothing is lost and reopening shows them. What must not happen is a late answer
|
||||
* landing on a dialog that has since been closed, or reopened on something else.
|
||||
*/
|
||||
private judgeToken = 0;
|
||||
readonly selectedAnnotationIds = signal<string[]>([]);
|
||||
readonly direction = signal<BiasInterventionDirection>('BIAS');
|
||||
readonly repetitions = signal(3);
|
||||
|
|
@ -60,6 +68,9 @@ export class BiasImpactExperimentDialogHostComponent {
|
|||
this.currentJob.set(null);
|
||||
this.inlineError.set(null);
|
||||
this.report.set(null);
|
||||
this.judgeError.set(null);
|
||||
this.judging.set(false);
|
||||
this.judgeToken++;
|
||||
});
|
||||
this.destroyRef.onDestroy(() => this.cancelPolling());
|
||||
}
|
||||
|
|
@ -125,15 +136,18 @@ export class BiasImpactExperimentDialogHostComponent {
|
|||
const report = this.report();
|
||||
if (!report || this.judging()) return;
|
||||
|
||||
const token = ++this.judgeToken;
|
||||
this.judging.set(true);
|
||||
this.judgeError.set(null);
|
||||
try {
|
||||
const judged = await this.reportJudge.assess(report.id);
|
||||
if (token !== this.judgeToken) return;
|
||||
if (judged) this.report.set(judged);
|
||||
} catch (error) {
|
||||
if (token !== this.judgeToken) return;
|
||||
this.judgeError.set(extractBiasErrorMessage(error, 'Unable to evaluate this comparison with an LLM.'));
|
||||
} finally {
|
||||
this.judging.set(false);
|
||||
if (token === this.judgeToken) this.judging.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,10 +42,15 @@ code { font-size: .78rem; overflow-wrap: anywhere; }
|
|||
.bias-impact-report__judge-btn:disabled { cursor: progress; opacity: .7; }
|
||||
.bias-impact-report__error { background: #fff1f2; border-left: 3px solid #e11d48; color: #9f1239; font-size: .84rem; margin: 0; padding: .6rem .75rem; }
|
||||
|
||||
.bias-impact-report__judgement { background: #f5f3ff; border: 1px solid #ddd6fe; border-radius: .45rem; padding: .7rem .8rem; }
|
||||
.bias-impact-report__judgement-heading { align-items: baseline; display: flex; flex-wrap: wrap; gap: .6rem; }
|
||||
.bias-impact-report__judgement-heading h4 { margin: 0; }
|
||||
.bias-impact-report__judgement-heading span, .bias-impact-report__judgement-heading time { color: #6d28d9; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .74rem; }
|
||||
/* A history: the newest open, the ones before it a click away rather than a wall of narratives. */
|
||||
.bias-impact-report__judgements { display: grid; gap: .4rem; }
|
||||
.bias-impact-report__judgements h4 { align-items: center; display: flex; gap: .4rem; margin: 0; }
|
||||
.bias-impact-report__judgements-count { background: #ede9fe; border-radius: 999px; color: #5b21b6; font-size: .7rem; font-weight: 700; padding: .1rem .45rem; }
|
||||
.bias-impact-report__judgement { background: #f5f3ff; border: 1px solid #ddd6fe; border-radius: .45rem; padding: .5rem .7rem; }
|
||||
.bias-impact-report__judgement > summary { align-items: center; cursor: pointer; display: flex; flex-wrap: wrap; gap: .5rem; }
|
||||
.bias-impact-report__judgement-model { color: #4c1d95; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: .76rem; font-weight: 700; }
|
||||
.bias-impact-report__judgement-current { background: #ddd6fe; border-radius: 999px; color: #4c1d95; font-size: .64rem; font-weight: 700; letter-spacing: .03em; padding: .1rem .4rem; text-transform: uppercase; }
|
||||
.bias-impact-report__judgement > summary time { color: #6d28d9; font-size: .72rem; margin-left: auto; }
|
||||
.bias-impact-report__judgement-text { color: #1e293b; font-size: .85rem; line-height: 1.5; margin: .45rem 0 0; }
|
||||
.bias-impact-report__judgement-error { color: #9f1239; font-size: .78rem; margin: .3rem 0 0; }
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<div class="bias-impact-report__header-actions">
|
||||
<button type="button" class="bias-impact-report__judge-btn" [disabled]="judging"
|
||||
(click)="evaluateWithLlm.emit()">
|
||||
{{ judging ? 'Evaluating…' : (judge ? 'Re-evaluate with LLM' : 'Evaluate impact with LLM') }}
|
||||
{{ judging ? 'Evaluating…' : (judge ? 'Evaluate again with LLM' : 'Evaluate impact with LLM') }}
|
||||
</button>
|
||||
<button type="button" class="bias-impact-report__highlight-btn" (click)="highlightOnCanvas.emit()">
|
||||
Highlight on canvas
|
||||
|
|
@ -54,23 +54,40 @@
|
|||
|
||||
<p class="bias-impact-report__summary">{{ currentReport.summary }}</p>
|
||||
|
||||
@if (judge; as assessment) {
|
||||
<section class="bias-impact-report__judgement">
|
||||
<div class="bias-impact-report__judgement-heading">
|
||||
<h4>LLM assessment</h4>
|
||||
<span>{{ assessment.judge.provider }} · {{ assessment.judge.model }}</span>
|
||||
<time [attr.datetime]="assessment.judgedAt">{{ assessment.judgedAt | date:'medium' }}</time>
|
||||
</div>
|
||||
@if (assessment.narrative) {
|
||||
<p class="bias-impact-report__judgement-text">{{ assessment.narrative }}</p>
|
||||
}
|
||||
<p class="bias-impact-report__hint">
|
||||
An assessment by a model, not a measurement: {{ assessment.judgedPairs }} pair(s) assessed,
|
||||
{{ assessment.skippedPairs }} skipped. With one run per side a difference cannot be separated
|
||||
from ordinary model variation with certainty.
|
||||
</p>
|
||||
@for (error of assessment.errors; track error) {
|
||||
<p class="bias-impact-report__judgement-error">{{ error }}</p>
|
||||
@if (judgements.length > 0) {
|
||||
<section class="bias-impact-report__judgements">
|
||||
<h4>
|
||||
LLM assessments
|
||||
<span class="bias-impact-report__judgements-count">{{ judgements.length }}</span>
|
||||
</h4>
|
||||
@for (assessment of judgements; track assessment.judgedAt; let index = $index) {
|
||||
<details class="bias-impact-report__judgement" [open]="judgementOpen(index)">
|
||||
<summary>
|
||||
<span class="bias-impact-report__judgement-model">{{ judgementLabel(assessment) }}</span>
|
||||
<span class="bias-impact-report__verdict-badge"
|
||||
[attr.data-state]="(assessment.impact ?? 'unclear').toLowerCase()">
|
||||
{{ assessment.impact ?? 'unclear' }} · {{ assessment.attribution ?? 'UNCLEAR' }}
|
||||
</span>
|
||||
@if (index === 0) { <span class="bias-impact-report__judgement-current">Current</span> }
|
||||
<time [attr.datetime]="assessment.judgedAt">{{ assessment.judgedAt | date:'medium' }}</time>
|
||||
</summary>
|
||||
@if (assessment.narrative) {
|
||||
<p class="bias-impact-report__judgement-text">{{ assessment.narrative }}</p>
|
||||
}
|
||||
<p class="bias-impact-report__hint">
|
||||
An assessment by a model, not a measurement: {{ assessment.judgedPairs }} pair(s) assessed,
|
||||
{{ assessment.skippedPairs }} skipped.
|
||||
@if (index === 0) {
|
||||
With one run per side a difference cannot be separated from ordinary model variation
|
||||
with certainty.
|
||||
} @else {
|
||||
The per-subject badges below follow the current assessment, not this one.
|
||||
}
|
||||
</p>
|
||||
@for (error of assessment.errors; track error) {
|
||||
<p class="bias-impact-report__judgement-error">{{ error }}</p>
|
||||
}
|
||||
</details>
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ describe('BiasImpactReportViewerComponent', () => {
|
|||
it('shows a stored assessment as an assessment, with the model behind it', () => {
|
||||
fixture.componentInstance.report = {
|
||||
...report,
|
||||
judge: {
|
||||
judgements: [{
|
||||
judge: { provider: 'InternalOllama', model: 'gemma:7b' },
|
||||
judgedAt: '2026-07-21T11:00:00.000Z',
|
||||
impact: 'SUBSTANTIVE',
|
||||
|
|
@ -147,7 +147,7 @@ describe('BiasImpactReportViewerComponent', () => {
|
|||
judgedPairs: 1,
|
||||
skippedPairs: 2,
|
||||
errors: []
|
||||
}
|
||||
}]
|
||||
};
|
||||
fixture.detectChanges();
|
||||
const text = fixture.nativeElement.textContent;
|
||||
|
|
@ -157,7 +157,39 @@ describe('BiasImpactReportViewerComponent', () => {
|
|||
expect(text).toContain('The second candidate is assessed differently.');
|
||||
expect(text).toContain('An assessment by a model, not a measurement');
|
||||
expect(fixture.nativeElement.querySelector('.bias-impact-report__judge-btn').textContent)
|
||||
.toContain('Re-evaluate with LLM');
|
||||
.toContain('Evaluate again with LLM');
|
||||
});
|
||||
|
||||
it('keeps every assessment, with the newest open and named as the current one', () => {
|
||||
const assessment = (model: string, judgedAt: string, narrative: string) => ({
|
||||
judge: { provider: 'InternalOllama', model },
|
||||
judgedAt,
|
||||
impact: 'SUBSTANTIVE' as const,
|
||||
attribution: 'INJECTION' as const,
|
||||
narrative,
|
||||
judgedPairs: 1,
|
||||
skippedPairs: 2,
|
||||
errors: []
|
||||
});
|
||||
fixture.componentInstance.report = {
|
||||
...report,
|
||||
judgements: [
|
||||
assessment('gemma:7b', '2026-07-21T12:00:00.000Z', 'The second candidate is assessed differently.'),
|
||||
assessment('llama3', '2026-07-21T11:00:00.000Z', 'An earlier opinion, kept.')
|
||||
]
|
||||
};
|
||||
fixture.detectChanges();
|
||||
|
||||
const entries = fixture.nativeElement.querySelectorAll('.bias-impact-report__judgement') as NodeListOf<HTMLDetailsElement>;
|
||||
expect(entries.length).toBe(2);
|
||||
expect(entries[0].open).toBe(true);
|
||||
expect(entries[1].open).toBe(false);
|
||||
expect(entries[0].textContent).toContain('gemma:7b');
|
||||
expect(entries[0].textContent).toContain('Current');
|
||||
expect(entries[1].textContent).toContain('An earlier opinion, kept.');
|
||||
// The badges on the subjects belong to the current one; the older entry says so.
|
||||
expect(entries[1].textContent).toContain('follow the current assessment');
|
||||
expect(fixture.nativeElement.textContent).toContain('LLM: SUBSTANTIVE · INJECTION');
|
||||
});
|
||||
|
||||
it('marks a pair the model could not answer for without hiding its figures', () => {
|
||||
|
|
|
|||
|
|
@ -57,8 +57,14 @@ export class BiasImpactReportViewerComponent {
|
|||
return this.report?.immediateImpact?.iterations ?? [];
|
||||
}
|
||||
|
||||
/** Every assessment, newest first. */
|
||||
get judgements(): BiasJudgeSummary[] {
|
||||
return this.report?.judgements ?? [];
|
||||
}
|
||||
|
||||
/** The one the per-subject badges belong to: the most recent. */
|
||||
get judge(): BiasJudgeSummary | null {
|
||||
return this.report?.judge ?? null;
|
||||
return this.judgements[0] ?? null;
|
||||
}
|
||||
|
||||
/** Whether there is anything the new sections can show, or only the old raw outputs. */
|
||||
|
|
@ -154,6 +160,15 @@ export class BiasImpactReportViewerComponent {
|
|||
return `Subject ${index}`;
|
||||
}
|
||||
|
||||
/** Open on the newest and closed on the rest: a history is for looking back, not for scrolling. */
|
||||
judgementOpen(index: number): boolean {
|
||||
return index === 0;
|
||||
}
|
||||
|
||||
judgementLabel(summary: BiasJudgeSummary): string {
|
||||
return `${summary.judge.provider} · ${summary.judge.model}`;
|
||||
}
|
||||
|
||||
verdictLabel(verdict: BiasJudgeVerdict | null): string {
|
||||
if (!verdict) return 'Not assessed';
|
||||
if (verdict.error) return 'Assessment failed';
|
||||
|
|
|
|||
Loading…
Reference in New Issue