= {
"app-restart": "App restarted mid-run",
};
-const numberFormat = new Intl.NumberFormat("en-US", {
- maximumFractionDigits: 4,
-});
-
interface AutoresearchPanelProps {
taskId: string;
}
@@ -378,65 +375,69 @@ function PendingPermissionNotice({
);
}
-function RunStats({ run }: { run: AutoresearchRun }) {
+export function RunStats({ run }: { run: AutoresearchRun }) {
const summary = useMemo(() => summarizeRun(run), [run]);
+ const theme = useChartTheme();
const unit = run.metricUnit;
+ const iterations = run.iterations;
+ const labels = useMemo(
+ () => iterations.map((iteration) => `iter ${iteration.index}`),
+ [iterations],
+ );
+ const formatMetricValue = (value: number) =>
+ Number.isNaN(value)
+ ? "—"
+ : withMetricUnit(metricNumberFormat.format(value), unit);
return (
-
- {withMetricUnit(numberFormat.format(summary.best.value), unit)}
-
- {" "}
- (iter {summary.best.index})
-
- >
- ) : (
- "—"
- )
+ 0
+ ? iterations.map((iteration) => iteration.bestValue)
+ : undefined
}
+ labels={labels}
+ theme={theme}
+ formatValue={formatMetricValue}
+ change={null}
+ subtitle={summary.best ? `iter ${summary.best.index}` : undefined}
+ dataAttr="autoresearch-stat-best"
/>
- 0
+ ? iterations.map((iteration) => iteration.value)
+ : undefined
}
+ labels={labels}
+ theme={theme}
+ formatValue={formatMetricValue}
+ change={null}
+ dataAttr="autoresearch-stat-last"
/>
- `${value} / ${run.config.maxIterations}`}
+ change={null}
+ dataAttr="autoresearch-stat-iterations"
/>
-
);
}
-function StatCard({ label, value }: { label: string; value: ReactNode }) {
- return (
-
-
- {label}
-
-
- {value}
-
-
- );
-}
-
function stageText(
model: string | null,
effort: string | null,
diff --git a/packages/ui/src/features/autoresearch/IterationsTable.tsx b/packages/ui/src/features/autoresearch/IterationsTable.tsx
index 0b5427af17..e964a4dd75 100644
--- a/packages/ui/src/features/autoresearch/IterationsTable.tsx
+++ b/packages/ui/src/features/autoresearch/IterationsTable.tsx
@@ -2,10 +2,16 @@ import type {
AutoresearchDirection,
AutoresearchIteration,
} from "@posthog/core/autoresearch/schemas";
-import { computeBest, isImprovement } from "@posthog/core/autoresearch/stats";
+import { computeBest } from "@posthog/core/autoresearch/stats";
import { Badge, Table, Text } from "@radix-ui/themes";
import { useMemo } from "react";
-import { withMetricUnit } from "./metricFormat";
+import {
+ type DeltaTone,
+ deltaTone,
+ formatMetricDelta,
+ metricNumberFormat,
+ withMetricUnit,
+} from "./metricFormat";
interface IterationsTableProps {
iterations: AutoresearchIteration[];
@@ -13,22 +19,16 @@ interface IterationsTableProps {
unit: string | null;
}
-const numberFormat = new Intl.NumberFormat("en-US", {
- maximumFractionDigits: 4,
-});
-
const timeFormat = new Intl.DateTimeFormat("en-US", {
hour: "2-digit",
minute: "2-digit",
});
-function deltaColor(
- delta: number | null,
- direction: AutoresearchDirection,
-): "green" | "red" | "gray" {
- if (delta === null || delta === 0) return "gray";
- return isImprovement(delta, 0, direction) ? "green" : "red";
-}
+const TONE_COLOR: Record = {
+ improved: "green",
+ worsened: "red",
+ neutral: "gray",
+};
export function IterationsTable({
iterations,
@@ -66,7 +66,10 @@ export function IterationsTable({
{iteration.index}
- {withMetricUnit(numberFormat.format(iteration.value), unit)}
+ {withMetricUnit(
+ metricNumberFormat.format(iteration.value),
+ unit,
+ )}
{best?.index === iteration.index && (
best
@@ -77,15 +80,10 @@ export function IterationsTable({
- {iteration.delta === null
- ? "—"
- : withMetricUnit(
- `${iteration.delta > 0 ? "+" : ""}${numberFormat.format(iteration.delta)}`,
- unit,
- )}
+ {formatMetricDelta(iteration.delta, unit)}
diff --git a/packages/ui/src/features/autoresearch/MetricChart.tsx b/packages/ui/src/features/autoresearch/MetricChart.tsx
index 879fb878f5..d20bc3a7b6 100644
--- a/packages/ui/src/features/autoresearch/MetricChart.tsx
+++ b/packages/ui/src/features/autoresearch/MetricChart.tsx
@@ -2,13 +2,15 @@ import type {
AutoresearchDirection,
AutoresearchIteration,
} from "@posthog/core/autoresearch/schemas";
+import {
+ LineChart,
+ ReferenceLine,
+ type Series,
+ useChartTheme,
+} from "@posthog/quill-charts";
import { Text } from "@radix-ui/themes";
import { useMemo } from "react";
-import { withMetricUnit } from "./metricFormat";
-
-const WIDTH = 640;
-const HEIGHT = 220;
-const PADDING = { top: 12, right: 16, bottom: 24, left: 52 };
+import { formatChartValue, withMetricUnit } from "./metricFormat";
interface MetricChartProps {
iterations: AutoresearchIteration[];
@@ -18,19 +20,6 @@ interface MetricChartProps {
unit: string | null;
}
-const wholeNumberFormat = new Intl.NumberFormat("en-US", {
- maximumFractionDigits: 0,
-});
-const fractionalNumberFormat = new Intl.NumberFormat("en-US", {
- maximumFractionDigits: 2,
-});
-
-function formatValue(value: number): string {
- return (
- Math.abs(value) >= 1000 ? wholeNumberFormat : fractionalNumberFormat
- ).format(value);
-}
-
/**
* Metric value per iteration (solid, with dots) plus the best-so-far
* frontier (dashed) and the optional target line.
@@ -42,53 +31,37 @@ export function MetricChart({
metricName,
unit,
}: MetricChartProps) {
- const chart = useMemo(() => {
- if (iterations.length === 0) return null;
+ const theme = useChartTheme();
+ // Canvas colors must be concrete — `var(--…)` strings don't paint. The
+ // theme's palette is already resolved from CSS variables.
+ const valueColor = theme.colors[0] ?? "#1d4aff";
+ const bestColor = theme.axisColor ?? "#8b8d98";
- const values = iterations.map((iteration) => iteration.value);
- const bests = iterations.map((iteration) => iteration.bestValue);
- const all = [...values, ...bests];
- if (targetValue !== null) all.push(targetValue);
-
- let min = Math.min(...all);
- let max = Math.max(...all);
- if (min === max) {
- min -= 1;
- max += 1;
- }
- const span = max - min;
- min -= span * 0.05;
- max += span * 0.05;
-
- const innerWidth = WIDTH - PADDING.left - PADDING.right;
- const innerHeight = HEIGHT - PADDING.top - PADDING.bottom;
- const x = (index: number) =>
- PADDING.left +
- (iterations.length === 1
- ? innerWidth / 2
- : (index / (iterations.length - 1)) * innerWidth);
- const y = (value: number) =>
- PADDING.top + ((max - value) / (max - min)) * innerHeight;
-
- const valuePoints = iterations.map(
- (iteration, i) => `${x(i)},${y(iteration.value)}`,
- );
- const bestPoints = iterations.map(
- (iteration, i) => `${x(i)},${y(iteration.bestValue)}`,
- );
-
- return {
- x,
- y,
- min,
- max,
- valuePath: valuePoints.join(" "),
- bestPath: bestPoints.join(" "),
- targetY: targetValue === null ? null : y(targetValue),
- };
- }, [iterations, targetValue]);
+ const series: Series[] = useMemo(
+ () => [
+ {
+ key: "value",
+ label: "value",
+ data: iterations.map((iteration) => iteration.value),
+ color: valueColor,
+ points: { radius: 3 },
+ },
+ {
+ key: "best",
+ label: "best so far",
+ data: iterations.map((iteration) => iteration.bestValue),
+ color: bestColor,
+ stroke: { pattern: [4, 4] },
+ },
+ ],
+ [iterations, valueColor, bestColor],
+ );
+ const labels = useMemo(
+ () => iterations.map((iteration) => String(iteration.index)),
+ [iterations],
+ );
- if (!chart) {
+ if (iterations.length === 0) {
return (
@@ -99,119 +72,53 @@ export function MetricChart({
}
return (
-
-