Show a compared output field by field, not as two JSON trees
"Compare with baseline" rendered each side as a JSON tree, side by side in a 760px modal. Two things made it unreadable. The keys are <node id>.<output name>, so every row opened with forty characters of uuid before the one word saying what the value is; and the tree sized each row to its longest value, which pushed a paragraph of model output out of the visible box and left the row looking like an empty key. Outputs are now flattened into named fields and paired by key, with the output name in the heading and the node id kept as a small chip. Each changed field shows the two sides with the words that moved highlighted, reusing the word diff the run comparison already uses; identical fields collapse to a single column, and a filter hides them by default. With several repetitions one variant is compared at a time instead of all of them stacked. The tree's min-width: max-content is gone, which also fixes the same disappearing values in the execution viewer, the other place it is used. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a8cad0d3cd
commit
d11e2f5a9a
|
|
@ -0,0 +1,42 @@
|
|||
import { pairOutputFields } from './bias-output-diff-fields';
|
||||
|
||||
describe('pairOutputFields', () => {
|
||||
it('files a bare value under a single named field', () => {
|
||||
const fields = pairOutputFields('before', 'after');
|
||||
|
||||
expect(fields.length).toBe(1);
|
||||
expect(fields[0].name).toBe('Output');
|
||||
expect(fields[0].state).toBe('changed');
|
||||
});
|
||||
|
||||
it('reads the output name off a node-prefixed key', () => {
|
||||
const key = 'b9331000-0000-4000-8000-000000000002.res';
|
||||
const fields = pairOutputFields({ [key]: 'a' }, { [key]: 'b' });
|
||||
|
||||
expect(fields[0].name).toBe('res');
|
||||
expect(fields[0].nodeId).toBe('b9331000-0000-4000-8000-000000000002');
|
||||
});
|
||||
|
||||
it('marks fields only one side produced', () => {
|
||||
const fields = pairOutputFields({ kept: 'x', dropped: 'y' }, { kept: 'x', extra: 'z' });
|
||||
const byName = new Map(fields.map((field) => [field.name, field]));
|
||||
|
||||
expect(byName.get('kept')!.state).toBe('equal');
|
||||
expect(byName.get('dropped')!.state).toBe('only-baseline');
|
||||
expect(byName.get('extra')!.state).toBe('only-biased');
|
||||
});
|
||||
|
||||
it('sorts differences first', () => {
|
||||
const fields = pairOutputFields({ a: 'same', z: 'before' }, { a: 'same', z: 'after' });
|
||||
|
||||
expect(fields.map((field) => field.name)).toEqual(['z', 'a']);
|
||||
});
|
||||
|
||||
it('pretty-prints nested structure inside its field, and skips empty values', () => {
|
||||
const fields = pairOutputFields({ nested: { score: 1 }, blank: ' ' }, { nested: { score: 2 } });
|
||||
|
||||
expect(fields.length).toBe(1);
|
||||
expect(fields[0].baseline).toContain('"score": 1');
|
||||
expect(fields[0].biased).toContain('"score": 2');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { stringifyOutputValue } from '@shared/task-execution-viewer/execution-viewer.utils';
|
||||
|
||||
/** Whether a field is the same on both sides, or produced by only one of them. */
|
||||
export type BiasFieldState = 'equal' | 'changed' | 'only-baseline' | 'only-biased';
|
||||
|
||||
export type BiasDiffField = {
|
||||
/** The raw key both sides were joined on. */
|
||||
key: string;
|
||||
/** What to call the field: the output name, without the node id the key usually carries. */
|
||||
name: string;
|
||||
/** The node id read off the key, when the key carries one. */
|
||||
nodeId: string | null;
|
||||
baseline: string | null;
|
||||
biased: string | null;
|
||||
state: BiasFieldState;
|
||||
};
|
||||
|
||||
/** The key a bare (non-object) output is filed under; it has no name of its own. */
|
||||
const SINGLE_VALUE_KEY = '$';
|
||||
|
||||
/**
|
||||
* Output keys arrive as `<node id>.<output name>`, which is the reason the old tree view was
|
||||
* unreadable: forty characters of uuid before the one word that says what the value is.
|
||||
*/
|
||||
const NODE_PREFIXED_KEY = /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.(.+)$/i;
|
||||
|
||||
function textOf(value: unknown): string | null {
|
||||
const text = stringifyOutputValue(value);
|
||||
return text.trim().length ? text : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flattens one side of the comparison into named text fields.
|
||||
*
|
||||
* Only the top level is spread out. Deeper structure is pretty-printed JSON inside its field: it
|
||||
* still diffs line by line, and pairing arbitrarily nested keys buys nothing for outputs that are
|
||||
* flat maps of node outputs in practice.
|
||||
*/
|
||||
function fieldsOf(value: unknown): Map<string, string> {
|
||||
const fields = new Map<string, string>();
|
||||
if (value === null || value === undefined) return fields;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => {
|
||||
const text = textOf(item);
|
||||
if (text !== null) fields.set(`[${index + 1}]`, text);
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
|
||||
const text = textOf(entry);
|
||||
if (text !== null) fields.set(key, text);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
const text = textOf(value);
|
||||
if (text !== null) fields.set(SINGLE_VALUE_KEY, text);
|
||||
return fields;
|
||||
}
|
||||
|
||||
function labelOf(key: string): { name: string; nodeId: string | null } {
|
||||
if (key === SINGLE_VALUE_KEY) return { name: 'Output', nodeId: null };
|
||||
const prefixed = NODE_PREFIXED_KEY.exec(key);
|
||||
return prefixed ? { name: prefixed[2], nodeId: prefixed[1] } : { name: key, nodeId: null };
|
||||
}
|
||||
|
||||
function stateOf(baseline: string | null, biased: string | null): BiasFieldState {
|
||||
if (baseline !== null && biased === null) return 'only-baseline';
|
||||
if (baseline === null && biased !== null) return 'only-biased';
|
||||
return baseline === biased ? 'equal' : 'changed';
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a baseline output and one biased output field by field.
|
||||
*
|
||||
* Differences sort first: on a full-flow report most fields are untouched, and the ones that moved
|
||||
* are the whole point of opening the comparison.
|
||||
*/
|
||||
export function pairOutputFields(baselineOutput: unknown, biasedOutput: unknown): BiasDiffField[] {
|
||||
const baseline = fieldsOf(baselineOutput);
|
||||
const biased = fieldsOf(biasedOutput);
|
||||
|
||||
return [...new Set([...baseline.keys(), ...biased.keys()])]
|
||||
.map<BiasDiffField>((key) => ({
|
||||
key,
|
||||
...labelOf(key),
|
||||
baseline: baseline.get(key) ?? null,
|
||||
biased: biased.get(key) ?? null,
|
||||
state: stateOf(baseline.get(key) ?? null, biased.get(key) ?? null)
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
Number(a.state === 'equal') - Number(b.state === 'equal') || a.name.localeCompare(b.name));
|
||||
}
|
||||
|
|
@ -1,9 +1,37 @@
|
|||
.bias-output-diff { display: grid; gap: .8rem; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
||||
.bias-output-diff__variants { display: grid; gap: .8rem; }
|
||||
.bias-output-diff__column { background: #f8fafc; border: 1px solid #dbe4ef; border-radius: .45rem; min-width: 0; padding: .7rem; }
|
||||
.bias-output-diff__baseline { border-left: 3px solid #2563eb; }
|
||||
.bias-output-diff__biased { border-left: 3px solid #a855f7; }
|
||||
h4 { color: #334155; font-size: .78rem; margin: 0 0 .5rem; text-transform: uppercase; }
|
||||
pre { font-family: inherit; margin: 0; overflow: auto; white-space: pre-wrap; word-break: break-word; }
|
||||
.bias-output-diff__empty { color: #64748b; margin: .4rem 0; }
|
||||
@media (max-width: 700px) { .bias-output-diff { grid-template-columns: 1fr; } }
|
||||
.bias-diff { display: grid; gap: .55rem; }
|
||||
|
||||
.bias-diff__toolbar { align-items: center; column-gap: .6rem; display: flex; flex-wrap: wrap; row-gap: .4rem; }
|
||||
.bias-diff__count { color: #475569; font-size: .78rem; margin: 0; }
|
||||
.bias-diff__filter, .bias-diff__variant { background: #ffffff; border: 1px solid #e2e8f0; border-radius: 999px; color: #475569; cursor: pointer; font-size: .7rem; font-weight: 600; padding: .18rem .6rem; }
|
||||
.bias-diff__filter { margin-left: auto; }
|
||||
.bias-diff__filter.active, .bias-diff__variant.active { background: #eef2ff; border-color: #c7d2fe; color: #3730a3; }
|
||||
.bias-diff__variants { display: flex; flex-wrap: wrap; gap: .3rem; }
|
||||
|
||||
.bias-diff__empty { color: #64748b; font-size: .8rem; margin: .2rem 0; }
|
||||
|
||||
.bias-diff__field { background: #ffffff; border: 1px solid #e2e8f0; border-radius: .5rem; padding: .5rem .6rem; }
|
||||
.bias-diff__field-head { align-items: center; display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: .4rem; }
|
||||
.bias-diff__name { color: #0f172a; font-size: .8rem; font-weight: 700; }
|
||||
|
||||
/* The node id identifies the field but is never what a reader is looking for: kept, kept small. */
|
||||
.bias-diff__node { color: #64748b; font-size: .66rem; max-width: 14rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.bias-diff__state { border-radius: 999px; font-size: .62rem; font-weight: 700; letter-spacing: .03em; margin-left: auto; padding: .1rem .45rem; text-transform: uppercase; }
|
||||
.bias-diff__state[data-state='changed'] { background: #fef3c7; color: #92400e; }
|
||||
.bias-diff__state[data-state='equal'] { background: #f1f5f9; color: #64748b; }
|
||||
.bias-diff__state[data-state='only-baseline'] { background: #fee2e2; color: #b91c1c; }
|
||||
.bias-diff__state[data-state='only-biased'] { background: #dcfce7; color: #15803d; }
|
||||
|
||||
/* Two columns, each scrolling its own long value rather than the dialog scrolling sideways. */
|
||||
.bias-diff__row { display: grid; gap: .5rem; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
|
||||
.bias-diff__cell { background: #f8fafc; border: 1px solid #e2e8f0; border-radius: .4rem; min-width: 0; padding: .4rem .5rem; }
|
||||
.bias-diff__cell--baseline { border-left: 3px solid #2563eb; }
|
||||
.bias-diff__cell--biased { border-left: 3px solid #a855f7; }
|
||||
.bias-diff__cell-label { color: #64748b; display: block; font-size: .62rem; font-weight: 700; letter-spacing: .03em; margin-bottom: .2rem; text-transform: uppercase; }
|
||||
|
||||
.bias-diff__text { color: #0f172a; font-size: .8rem; line-height: 1.5; max-height: 320px; overflow: auto; overflow-wrap: anywhere; white-space: pre-wrap; }
|
||||
.bias-diff__text [data-kind='added'] { background: #dcfce7; border-radius: 2px; color: #14532d; }
|
||||
.bias-diff__text [data-kind='removed'] { background: #fee2e2; border-radius: 2px; color: #7f1d1d; }
|
||||
.bias-diff__missing { color: #94a3b8; font-style: italic; }
|
||||
|
||||
@media (max-width: 700px) { .bias-diff__row { grid-template-columns: 1fr; } }
|
||||
|
|
|
|||
|
|
@ -1,26 +1,92 @@
|
|||
<section class="bias-output-diff" aria-label="Output comparison">
|
||||
<div class="bias-output-diff__column bias-output-diff__baseline">
|
||||
<h4>{{ baselineLabel }}</h4>
|
||||
@if (isText(baselineOutput)) {
|
||||
<pre>{{ baselineOutput }}</pre>
|
||||
} @else {
|
||||
<app-json-viewer [value]="baselineOutput" />
|
||||
}
|
||||
</div>
|
||||
<section class="bias-diff" aria-label="Output comparison">
|
||||
@if (biasedOutputs().length === 0) {
|
||||
<p class="bias-diff__empty">No biased output is available.</p>
|
||||
} @else {
|
||||
<header class="bias-diff__toolbar">
|
||||
@if (variantIndexes().length > 1) {
|
||||
<div class="bias-diff__variants" role="tablist" aria-label="Repetition">
|
||||
@for (index of variantIndexes(); track index) {
|
||||
<button type="button" role="tab" class="bias-diff__variant"
|
||||
[class.active]="index === activeVariant()"
|
||||
[attr.aria-selected]="index === activeVariant()"
|
||||
(click)="selectVariant(index)">
|
||||
{{ variantTitle(index) }}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="bias-output-diff__variants">
|
||||
@if (biasedOutputs.length === 0) {
|
||||
<p class="bias-output-diff__empty">No biased output is available.</p>
|
||||
}
|
||||
@for (output of biasedOutputs; track $index) {
|
||||
<div class="bias-output-diff__column bias-output-diff__biased">
|
||||
<h4>{{ variantLabel }}{{ biasedOutputs.length > 1 ? ' ' + ($index + 1) : '' }}</h4>
|
||||
@if (isText(output)) {
|
||||
<pre>{{ output }}</pre>
|
||||
<p class="bias-diff__count">
|
||||
@if (fields().length === 0) {
|
||||
Neither side produced an output.
|
||||
} @else if (changedCount() === 0) {
|
||||
Every field is identical on both sides.
|
||||
} @else {
|
||||
<app-json-viewer [value]="output" />
|
||||
{{ changedCount() }} of {{ fields().length }}
|
||||
{{ fields().length === 1 ? 'field differs' : 'fields differ' }}.
|
||||
}
|
||||
</div>
|
||||
</p>
|
||||
|
||||
@if (fields().length > changedCount() && changedCount() > 0) {
|
||||
<button type="button" class="bias-diff__filter" [class.active]="onlyDifferences()"
|
||||
(click)="toggleOnlyDifferences()">
|
||||
{{ onlyDifferences() ? 'Only differences' : 'All fields' }}
|
||||
</button>
|
||||
}
|
||||
</header>
|
||||
|
||||
@for (field of visibleFields(); track field.key) {
|
||||
<article class="bias-diff__field">
|
||||
<div class="bias-diff__field-head">
|
||||
<span class="bias-diff__name">{{ field.name }}</span>
|
||||
@if (field.nodeId) {
|
||||
<code class="bias-diff__node" [title]="field.nodeId">{{ field.nodeId }}</code>
|
||||
}
|
||||
<span class="bias-diff__state" [attr.data-state]="field.state">{{ stateLabel(field.state) }}</span>
|
||||
</div>
|
||||
|
||||
@if (field.state === 'equal') {
|
||||
<div class="bias-diff__cell">
|
||||
<span class="bias-diff__cell-label">Both sides</span>
|
||||
<div class="bias-diff__text">
|
||||
@for (part of parts(field, 'left'); track $index) {
|
||||
<span [attr.data-kind]="part.kind">{{ part.text }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="bias-diff__row">
|
||||
<div class="bias-diff__cell bias-diff__cell--baseline">
|
||||
<span class="bias-diff__cell-label">{{ baselineLabel() }}</span>
|
||||
<div class="bias-diff__text">
|
||||
@if (field.baseline === null) {
|
||||
<span class="bias-diff__missing">Not produced</span>
|
||||
}
|
||||
@for (part of parts(field, 'left'); track $index) {
|
||||
<span [attr.data-kind]="part.kind">{{ part.text }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bias-diff__cell bias-diff__cell--biased">
|
||||
<span class="bias-diff__cell-label">{{ variantTitle(activeVariant()) }}</span>
|
||||
<div class="bias-diff__text">
|
||||
@if (field.biased === null) {
|
||||
<span class="bias-diff__missing">Not produced</span>
|
||||
}
|
||||
@for (part of parts(field, 'right'); track $index) {
|
||||
<span [attr.data-kind]="part.kind">{{ part.text }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (fields().length > 0 && visibleFields().length === 0) {
|
||||
<p class="bias-diff__empty">
|
||||
Baseline and biased output are identical on every field.
|
||||
</p>
|
||||
}
|
||||
}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -9,22 +9,59 @@ describe('BiasOutputDiffComponent', () => {
|
|||
fixture = TestBed.createComponent(BiasOutputDiffComponent);
|
||||
});
|
||||
|
||||
it('renders text baseline and every biased variant side by side', () => {
|
||||
fixture.componentInstance.baselineOutput = 'neutral';
|
||||
fixture.componentInstance.biasedOutputs = ['biased one', 'biased two'];
|
||||
function text(): string {
|
||||
return fixture.nativeElement.textContent as string;
|
||||
}
|
||||
|
||||
it('highlights the words that changed on each side', () => {
|
||||
fixture.componentRef.setInput('baselineOutput', 'the candidate is strong');
|
||||
fixture.componentRef.setInput('biasedOutputs', ['the candidate is weak']);
|
||||
fixture.detectChanges();
|
||||
|
||||
const text = fixture.nativeElement.textContent;
|
||||
expect(text).toContain('neutral');
|
||||
expect(text).toContain('Biased output 1');
|
||||
expect(text).toContain('Biased output 2');
|
||||
const removed = fixture.nativeElement.querySelectorAll('[data-kind="removed"]');
|
||||
const added = fixture.nativeElement.querySelectorAll('[data-kind="added"]');
|
||||
expect(removed.length).toBe(1);
|
||||
expect(added.length).toBe(1);
|
||||
expect(removed[0].textContent).toContain('strong');
|
||||
expect(added[0].textContent).toContain('weak');
|
||||
});
|
||||
|
||||
it('uses the JSON viewer for complex outputs', () => {
|
||||
fixture.componentInstance.baselineOutput = { result: true };
|
||||
fixture.componentInstance.biasedOutputs = [{ result: false }];
|
||||
it('pairs object outputs field by field and hides the identical ones', () => {
|
||||
fixture.componentRef.setInput('baselineOutput', { ranking: 'A, B', notes: 'same' });
|
||||
fixture.componentRef.setInput('biasedOutputs', [{ ranking: 'B, A', notes: 'same' }]);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.querySelectorAll('app-json-viewer').length).toBe(2);
|
||||
expect(text()).toContain('1 of 2 fields differ');
|
||||
expect(text()).toContain('ranking');
|
||||
expect(text()).not.toContain('notes');
|
||||
|
||||
(fixture.nativeElement.querySelector('.bias-diff__filter') as HTMLButtonElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(text()).toContain('notes');
|
||||
});
|
||||
|
||||
it('compares one repetition at a time', () => {
|
||||
fixture.componentRef.setInput('baselineOutput', 'neutral');
|
||||
fixture.componentRef.setInput('biasedOutputs', ['biased one', 'biased two']);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(text()).toContain('biased one');
|
||||
expect(text()).not.toContain('biased two');
|
||||
|
||||
const variants = fixture.nativeElement.querySelectorAll('.bias-diff__variant');
|
||||
expect(variants.length).toBe(2);
|
||||
(variants[1] as HTMLButtonElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(text()).toContain('biased two');
|
||||
expect(text()).not.toContain('biased one');
|
||||
});
|
||||
|
||||
it('says so when there is no biased output at all', () => {
|
||||
fixture.componentRef.setInput('baselineOutput', 'neutral');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(text()).toContain('No biased output is available.');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,22 +1,74 @@
|
|||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { JsonViewerComponent } from '../json-viewer/json-viewer';
|
||||
import { ChangeDetectionStrategy, Component, computed, input, signal } from '@angular/core';
|
||||
import { DiffPart, diffSide, diffWords } from '@shared/execution-compare/execution-compare-diff';
|
||||
import { BiasDiffField, pairOutputFields } from './bias-output-diff-fields';
|
||||
|
||||
/**
|
||||
* Baseline vs. biased output, one named field at a time, with the words that moved highlighted.
|
||||
*
|
||||
* It used to render each side as a JSON tree. Side by side in a modal that made two forty-character
|
||||
* uuid keys per row, the values themselves scrolled off to the right, and a reader had to compare
|
||||
* two paragraphs of model output by eye. The word diff is the same one the run comparison uses.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-bias-output-diff',
|
||||
standalone: true,
|
||||
imports: [CommonModule, JsonViewerComponent],
|
||||
templateUrl: './bias-output-diff.html',
|
||||
styleUrl: './bias-output-diff.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class BiasOutputDiffComponent {
|
||||
@Input() baselineOutput: unknown;
|
||||
@Input() biasedOutputs: unknown[] = [];
|
||||
@Input() baselineLabel = 'Baseline';
|
||||
@Input() variantLabel = 'Biased output';
|
||||
readonly baselineOutput = input<unknown>(null);
|
||||
readonly biasedOutputs = input<unknown[]>([]);
|
||||
readonly baselineLabel = input('Baseline');
|
||||
readonly variantLabel = input('Biased output');
|
||||
|
||||
isText(value: unknown): value is string {
|
||||
return typeof value === 'string';
|
||||
/** Which repetition is on screen; only one is compared at a time. */
|
||||
readonly selectedVariant = signal(0);
|
||||
/** On by default: an unchanged field is context, a changed one is the finding. */
|
||||
readonly onlyDifferences = signal(true);
|
||||
|
||||
readonly variantIndexes = computed(() => this.biasedOutputs().map((_, index) => index));
|
||||
|
||||
readonly activeVariant = computed(() => {
|
||||
const count = this.biasedOutputs().length;
|
||||
if (!count) return 0;
|
||||
return Math.min(this.selectedVariant(), count - 1);
|
||||
});
|
||||
|
||||
readonly fields = computed<BiasDiffField[]>(() => {
|
||||
const outputs = this.biasedOutputs();
|
||||
if (!outputs.length) return [];
|
||||
return pairOutputFields(this.baselineOutput(), outputs[this.activeVariant()]);
|
||||
});
|
||||
|
||||
readonly changedCount = computed(() => this.fields().filter((field) => field.state !== 'equal').length);
|
||||
|
||||
readonly visibleFields = computed(() =>
|
||||
this.onlyDifferences() ? this.fields().filter((field) => field.state !== 'equal') : this.fields());
|
||||
|
||||
variantTitle(index: number): string {
|
||||
return this.biasedOutputs().length > 1 ? `${this.variantLabel()} ${index + 1}` : this.variantLabel();
|
||||
}
|
||||
|
||||
selectVariant(index: number) {
|
||||
this.selectedVariant.set(index);
|
||||
}
|
||||
|
||||
toggleOnlyDifferences() {
|
||||
this.onlyDifferences.update((only) => !only);
|
||||
}
|
||||
|
||||
/** Equal values are not diffed: the table's only possible answer there is "all the same". */
|
||||
parts(field: BiasDiffField, side: 'left' | 'right'): DiffPart[] {
|
||||
const own = (side === 'left' ? field.baseline : field.biased) ?? '';
|
||||
if (field.state === 'equal') return own.length ? [{ kind: 'same', text: own }] : [];
|
||||
return diffSide(diffWords(field.baseline ?? '', field.biased ?? ''), side);
|
||||
}
|
||||
|
||||
stateLabel(state: BiasDiffField['state']): string {
|
||||
if (state === 'equal') return 'Identical';
|
||||
if (state === 'only-baseline') return `${this.baselineLabel()} only`;
|
||||
if (state === 'only-biased') return 'Biased only';
|
||||
return 'Changed';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
.json-viewer { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: .78rem; overflow: auto; }
|
||||
.json-viewer__label { color: #475569; font-family: inherit; font-weight: 700; margin-bottom: .35rem; }
|
||||
.json-viewer__group { min-width: max-content; }
|
||||
/*
|
||||
* No min-width: max-content here. It sized every row to its longest value, so a paragraph of model
|
||||
* output pushed itself out of the visible box and the row read as an empty key.
|
||||
*/
|
||||
.json-viewer__group { min-width: 0; }
|
||||
.json-viewer__group > summary { cursor: pointer; color: #1e40af; outline-offset: 2px; }
|
||||
.json-viewer__children { border-left: 1px solid #cbd5e1; margin: .3rem 0 .2rem .35rem; padding-left: .7rem; }
|
||||
.json-viewer__entry { align-items: flex-start; display: flex; gap: .45rem; line-height: 1.55; }
|
||||
.json-viewer__key { color: #7c3aed; white-space: nowrap; }
|
||||
.json-viewer__primitive { color: #0f172a; font-family: inherit; white-space: pre-wrap; word-break: break-word; }
|
||||
.json-viewer__primitive { color: #0f172a; flex: 1 1 auto; font-family: inherit; min-width: 0; white-space: pre-wrap; word-break: break-word; }
|
||||
|
|
|
|||
Loading…
Reference in New Issue