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
91 changes: 16 additions & 75 deletions frontend/src/components/experiment-trials-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,11 @@ import {
taskHasCancellableWork,
} from "@/lib/job-status";
import {
ANALYSIS_CLASSIFICATION_LABELS,
formatPartialRewardBadgeValue,
formatRewardPercent,
formatRewardValue,
getMatrixStatus,
getQaGlyphMatrixClass,
getRewardStyle,
getVisibleAnalysisClassification,
QA_TONE_MATRIX_CLASS,
STATUS_CONFIG,
STATUS_GLYPH_BOX,
type MatrixStatus,
Expand Down Expand Up @@ -335,12 +331,12 @@ const ANALYSIS_LEGEND_ITEMS: Array<{
},
{
key: "good",
label: "Good",
label: "Pass",
dotClass: ANALYSIS_CONFIG.GOOD_SUCCESS.dotClass,
},
{
key: "bad",
label: "Bad",
label: "Fail",
dotClass: ANALYSIS_CONFIG.BAD_SUCCESS.dotClass,
},
{
Expand Down Expand Up @@ -1566,22 +1562,6 @@ export function ExperimentTrialsTable({
"analysis-failed": "var(--paper-a-failed)",
};

// Good/bad chips show the tone painted on both outcome shapes — a ✓✗
// pair — so the legend teaches the cell semiotics (shape = outcome,
// color = QA verdict) instead of implying green means "pass".
const renderToneGlyphPair = (tone: "good" | "bad") => (
<span className="inline-flex items-center gap-[2px]">
{(["pass", "fail"] as const).map((shape) => (
<span
key={shape}
className={`inline-flex h-[14px] w-[16px] items-center justify-center rounded-[3px] border ${QA_TONE_MATRIX_CLASS[tone]}`}
>
<StatusIcon status={shape} className="h-2 w-2" />
</span>
))}
</span>
);

const renderAnalyzerChip = (item: (typeof ANALYSIS_LEGEND_ITEMS)[number]) => {
const isDimmed = dimmedAnalysisKeys.has(item.key);
return (
Expand All @@ -1595,14 +1575,10 @@ export function ExperimentTrialsTable({
isDimmed ? "line-through opacity-[0.38]" : ""
}`}
>
{item.key === "good" || item.key === "bad" ? (
renderToneGlyphPair(item.key)
) : (
<span
className={`inline-block h-2 w-2 rounded-full ${item.animate ? "animate-pulse" : ""}`}
style={{ background: ANALYZER_CHIP_COLOR[item.key] }}
/>
)}
<span
className={`inline-block h-2 w-2 rounded-full ${item.animate ? "animate-pulse" : ""}`}
style={{ background: ANALYZER_CHIP_COLOR[item.key] }}
/>
<span>{item.label}</span>
</Button>
</TooltipTrigger>
Expand All @@ -1617,46 +1593,31 @@ export function ExperimentTrialsTable({
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center gap-2.5 border-r border-dashed border-[color:var(--paper-line)] pr-2.5 pl-1.5 font-mono text-[9.5px] leading-tight text-[color:var(--paper-ink-3)]">
<span className="relative inline-flex gap-[3px]">
<span className="relative inline-flex">
<span
className={`flex items-center justify-center border-transparent bg-[color:var(--paper-pass)] text-white ${STATUS_GLYPH_BOX}`}
>
<StatusIcon status="pass" />
</span>
<span
className={`flex items-center justify-center border-transparent text-white ${STATUS_GLYPH_BOX} ${
showAnalysis
? "bg-[color:var(--paper-pass)]"
: "bg-[color:var(--paper-fail)]"
}`}
>
<StatusIcon status="fail" />
</span>
{showAnalysis && (
<span className="absolute -top-[2px] -right-[2px] h-[7px] w-[7px] animate-pulse rounded-full bg-[color:var(--paper-a-analyzing)] ring-[1.5px] ring-[color:var(--paper-surface)]" />
<span className="absolute -top-[2px] -right-[2px] h-[7px] w-[7px] rounded-full bg-[color:var(--paper-a-good)] ring-[1.5px] ring-[color:var(--paper-surface)]" />
)}
</span>
<span className="flex flex-col gap-0.5">
<span className="inline-flex items-center gap-1.5 whitespace-nowrap">
<StatusIcon status="pass" className="h-2.5 w-2.5" />
<StatusIcon status="fail" className="h-2.5 w-2.5" />
shape = trial result
<span className="inline-block h-2.5 w-2.5 rounded-[2px] bg-[color:var(--paper-pass)]" />
trial result
</span>
{showAnalysis && (
<span className="inline-flex items-center gap-1.5 whitespace-nowrap">
<span className="inline-block h-2.5 w-2.5 rounded-[2px] bg-[color:var(--paper-pass)]" />
<span className="inline-block h-2.5 w-2.5 rounded-[2px] bg-[color:var(--paper-fail)]" />
color = QA verdict
<span className="mx-[1px] inline-block h-2 w-2 rounded-full bg-[color:var(--paper-a-good)]" />
QA result
</span>
)}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
How to read a cell: ✓/✗ is the trial outcome; once QA classifies a
trial, green means the outcome is good (expected) and red means bad — a
green ✗ is a good failure. A pulsing dot means QA is still running.
</TooltipContent>
<TooltipContent>How to read a cell</TooltipContent>
</Tooltip>
);

Expand Down Expand Up @@ -2474,26 +2435,6 @@ export function ExperimentTrialsTable({
dimmedAnalysisKeys.has(analysisLegendKey)
? "opacity-25"
: "";
// A classified binary outcome takes its QA
// tone as the glyph fill: shape = outcome,
// color = QA verdict. A good failure is a
// green ✗, a bad success a red ✓. The
// classification (and with it the fill and
// label) is null when QA is hidden.
const classification =
getVisibleAnalysisClassification(
showAnalysis,
trial
);
const qaGlyphClass = getQaGlyphMatrixClass(
status,
classification
);
const cellLabel = classification
? ANALYSIS_CLASSIFICATION_LABELS[
classification
]
: config.shortLabel;
const baseTitle = getTrialTitle(
trial,
status
Expand Down Expand Up @@ -2535,9 +2476,9 @@ export function ExperimentTrialsTable({
trialGroups,
});
}}
className={`relative grid place-items-center gap-0 p-0 leading-none transition-transform hover:-translate-y-px ${STATUS_GLYPH_BOX} ${qaGlyphClass ?? config.matrixClass} ${isPartial ? "font-mono text-[9.5px] font-semibold tracking-[-0.02em] tabular-nums" : ""}`}
className={`relative grid place-items-center gap-0 p-0 leading-none transition-transform hover:-translate-y-px ${STATUS_GLYPH_BOX} ${config.matrixClass} ${isPartial ? "font-mono text-[9.5px] font-semibold tracking-[-0.02em] tabular-nums" : ""}`}
style={getRewardStyle(trial.reward)}
aria-label={`Trial ${trialIndex + 1} ${cellLabel}`}
aria-label={`Trial ${trialIndex + 1} ${config.shortLabel}`}
title={fullTitle}
>
{isPartial ? (
Expand All @@ -2546,7 +2487,7 @@ export function ExperimentTrialsTable({
<StatusIcon status={status} />
)}
</Button>
{analysisIndicator && !qaGlyphClass && (
{analysisIndicator && (
<span
aria-hidden="true"
className={`pointer-events-none absolute -top-[1px] -right-[1px] h-[4px] w-[4px] rounded-full ring-[1px] ring-[color:var(--paper-surface)] ${analysisIndicator.dotClass} ${analysisIndicator.animate ? "animate-pulse" : ""}`}
Expand Down
24 changes: 3 additions & 21 deletions frontend/src/components/trial-detail-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,11 @@ import {
sumTaskTrialCost,
} from "@/lib/format";
import {
ANALYSIS_CLASSIFICATION_LABELS,
formatPartialRewardBadgeValue,
formatRewardPercent,
formatRewardValue,
getMatrixStatus,
getQaGlyphMatrixClass,
getRewardStyle,
getVisibleAnalysisClassification,
STATUS_CONFIG,
STATUS_GLYPH_BOX,
type MatrixStatus,
Expand Down Expand Up @@ -1258,21 +1255,6 @@ export function TrialDetailPanel({
groupTrial.error_message,
);
const groupConfig = STATUS_CONFIG[groupStatus];
// Same cell semiotics as the experiment matrix: a
// classified binary outcome takes its QA tone as the
// glyph fill (good failure = green ✗, bad success = red ✓).
// The shared gate keeps QA hidden on public share views.
const groupClassification = getVisibleAnalysisClassification(
showAnalysis,
groupTrial,
);
const groupQaGlyphClass = getQaGlyphMatrixClass(
groupStatus,
groupClassification,
);
const groupLabel = groupClassification
? ANALYSIS_CLASSIFICATION_LABELS[groupClassification]
: groupConfig.shortLabel;
const isPartial = groupStatus === "partial";
const partialLabel = isPartial
? formatPartialRewardBadgeValue(groupTrial.reward)
Expand All @@ -1288,7 +1270,7 @@ export function TrialDetailPanel({
className={cn(
"flex items-center justify-center p-0 leading-none transition hover:opacity-90",
STATUS_GLYPH_BOX,
groupQaGlyphClass ?? groupConfig.matrixClass,
groupConfig.matrixClass,
isPartial
? "font-mono text-[9.5px] font-semibold tracking-[-0.02em] tabular-nums"
: "",
Expand All @@ -1297,8 +1279,8 @@ export function TrialDetailPanel({
: "",
)}
style={getRewardStyle(groupTrial.reward)}
aria-label={`Trial ${index + 1} ${groupLabel}`}
title={`${groupLabel} • Trial ${index + 1}`}
aria-label={`Trial ${index + 1} ${groupConfig.shortLabel}`}
title={`${groupConfig.shortLabel} • Trial ${index + 1}`}
>
{isPartial ? (
partialLabel
Expand Down
71 changes: 0 additions & 71 deletions frontend/src/lib/status-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from "lucide-react";
import type { CSSProperties } from "react";

import type { AnalysisClassification, Trial } from "@/lib/types";

/**
* Trial status types that map to visual states in the UI.
* These are derived from trial.status and trial.reward values.
Expand All @@ -27,75 +25,6 @@ export type MatrixStatus =
| "queued"
| "running";

/**
* QA judgment tone for a classified trial. The glyph semiotics are:
* shape = execution outcome (✓ pass / ✗ fail), fill color = QA verdict
* (green = good, red = bad). This makes an expected "good failure" render
* as a green ✗ instead of a red ✗ with a nearly-invisible green dot, and
* a "bad success" as a red ✓ instead of an unqualified green ✓.
*/
export type QaTone = "good" | "bad";

/**
* Matrix glyph fill for a QA tone. Reuses the pass/fail paper tokens so a
* recolored cell is indistinguishable in hue from an unclassified one —
* only the shape-vs-color meaning changes.
*/
export const QA_TONE_MATRIX_CLASS: Record<QaTone, string> = {
good: "bg-paper-pass text-white border-paper-pass hover:opacity-90",
bad: "bg-paper-fail text-white border-paper-fail hover:opacity-90",
};

/** Human label per QA classification, shared by every trial-glyph surface. */
export const ANALYSIS_CLASSIFICATION_LABELS: Record<
AnalysisClassification,
string
> = {
GOOD_SUCCESS: "Good success",
GOOD_FAILURE: "Good failure",
BAD_SUCCESS: "Bad success",
BAD_FAILURE: "Bad failure",
HARNESS_ERROR: "Harness error",
};

/**
* The classification a view is allowed to show, or null. This is the ONLY
* place the QA-visibility gate lives: public share surfaces pass
* `showAnalysis: false` to hide QA entirely, and a classification counts
* only once the trial's analysis succeeded. Every glyph fill, label,
* aria-label, and dot consumer must derive from this function so the gate
* cannot drift between surfaces.
*/
export function getVisibleAnalysisClassification(
showAnalysis: boolean,
trial: Pick<Trial, "analysis_status" | "analysis">,
): AnalysisClassification | null {
if (!showAnalysis || trial.analysis_status !== "success") return null;
return trial.analysis?.classification ?? null;
}

/**
* Glyph fill override for a classified trial, or null when the outcome
* glyph keeps its default color. Only binary outcomes (pass/fail) take the
* QA tone; partials keep their warm ramp and non-terminal or non-scored
* states keep their neutral styling, with the QA tone still surfaced
* through the corner dot. GOOD_SUCCESS pass and BAD_FAILURE fail resolve
* to the same color they already had — the rule stays uniform.
*/
export function getQaGlyphMatrixClass(
status: MatrixStatus,
classification: AnalysisClassification | null | undefined
): string | null {
if (status !== "pass" && status !== "fail") return null;
if (classification === "GOOD_SUCCESS" || classification === "GOOD_FAILURE") {
return QA_TONE_MATRIX_CLASS.good;
}
if (classification === "BAD_SUCCESS" || classification === "BAD_FAILURE") {
return QA_TONE_MATRIX_CLASS.bad;
}
return null;
}

/**
* Status configuration for consistent styling across the UI.
*
Expand Down
Loading