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
164 changes: 164 additions & 0 deletions packages/ui/src/features/autoresearch/AutoresearchPanel.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import type {
AutoresearchIteration,
AutoresearchRun,
} from "@posthog/core/autoresearch/schemas";
import { Flex } from "@radix-ui/themes";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { RunStats } from "./AutoresearchPanel";
import { IterationsTable } from "./IterationsTable";
import { MetricChart } from "./MetricChart";

/**
* The run dashboard's presentational stack — stat cards, metric chart, and
* iterations table — as `AutoresearchPanel` composes them, minus the header
* controls and dialogs (which need a live service).
*/
function RunDashboard({ run }: { run: AutoresearchRun }) {
return (
<Flex direction="column" gap="4">
<RunStats run={run} />
<MetricChart
iterations={run.iterations}
direction={run.config.direction}
targetValue={run.config.targetValue}
metricName={run.metricName ?? "the metric"}
unit={run.metricUnit}
/>
<IterationsTable
iterations={run.iterations}
direction={run.config.direction}
unit={run.metricUnit}
/>
</Flex>
);
}

const BASE_AT = Date.parse("2026-07-07T09:00:00Z");

const iterationsFrom = (
values: number[],
direction: AutoresearchRun["config"]["direction"],
summaries: (string | null)[] = [],
): AutoresearchIteration[] => {
let best: number | null = null;
return values.map((value, i) => {
best =
best === null || (direction === "minimize" ? value < best : value > best)
? value
: best;
return {
index: i + 1,
value,
bestValue: best,
delta: i === 0 ? null : value - values[i - 1],
summary: summaries[i] ?? null,
at: BASE_AT + i * 8 * 60_000,
};
});
};

const run = (overrides: Partial<AutoresearchRun> = {}): AutoresearchRun => ({
id: "run-1",
config: {
taskId: "task-1",
direction: "minimize",
targetValue: 380,
maxIterations: 12,
implementModel: null,
measureModel: null,
implementEffort: null,
measureEffort: null,
instructions: "Shrink the renderer bundle without breaking tests.",
},
status: "running",
metricName: "bundle size",
metricUnit: "kB",
phase: null,
originalModel: null,
originalEffort: null,
iterations: [],
startedAt: BASE_AT,
endedAt: null,
endReason: null,
interruptedReason: null,
lastError: null,
...overrides,
});

const meta: Meta<typeof RunDashboard> = {
title: "Autoresearch/RunDashboard",
component: RunDashboard,
// Match the panel's column width so cards, chart, and table size realistically.
decorators: [
(Story) => (
<div style={{ maxWidth: 760, margin: "2rem auto", padding: "0 1rem" }}>
<Story />
</div>
),
],
};

export default meta;
type Story = StoryObj<typeof RunDashboard>;

/** A minimize run mid-flight: noisy progress, best-so-far frontier, target line. */
export const MinimizeInProgress: Story = {
args: {
run: run({
iterations: iterationsFrom(
[512, 498, 505, 461, 442, 449, 407, 398],
"minimize",
[
"Baseline",
"Tree-shake icon imports",
"Revert: broke lazy routes",
"Split vendor chunk",
"Drop moment locales",
"Inline critical CSS (regression)",
"Lazy-load diff worker",
"Dedupe zod versions",
],
),
}),
},
};

/** A completed maximize run with no target — the loop spent its budget. */
export const MaximizeCompleted: Story = {
args: {
run: run({
status: "completed",
endedAt: BASE_AT + 10 * 8 * 60_000,
endReason: "max-iterations",
metricName: "cache hit rate",
metricUnit: "%",
config: {
...run().config,
direction: "maximize",
targetValue: null,
maxIterations: 10,
},
iterations: iterationsFrom(
[62, 71, 68, 74, 79, 77, 83, 82, 86, 85],
"maximize",
[
"Baseline",
"Warm cache on boot",
null,
"Bigger LRU",
null,
null,
"Precompute keys",
null,
"Batch invalidations",
null,
],
),
}),
},
};

/** Before the first metric report arrives: empty cards, chart, and table. */
export const NoIterationsYet: Story = {
args: { run: run() },
};
101 changes: 51 additions & 50 deletions packages/ui/src/features/autoresearch/AutoresearchPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import {
EmptyMedia,
EmptyTitle,
} from "@posthog/quill";
import { MetricCard, useChartTheme } from "@posthog/quill-charts";
import { Badge, Button, Callout, Flex, Select, Text } from "@radix-ui/themes";
import { type ReactNode, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
getConfigOptionByCategory,
useSessionStore,
Expand All @@ -24,7 +25,7 @@ import { usePendingPermissionsForTask } from "../sessions/useSession";
import { AutoresearchConfigDialog } from "./AutoresearchConfigDialog";
import { IterationsTable } from "./IterationsTable";
import { MetricChart } from "./MetricChart";
import { withMetricUnit } from "./metricFormat";
import { metricNumberFormat, withMetricUnit } from "./metricFormat";
import {
type AutoresearchModelOption,
stageValueLabel,
Expand Down Expand Up @@ -62,10 +63,6 @@ const INTERRUPTION_LABEL: Record<string, string> = {
"app-restart": "App restarted mid-run",
};

const numberFormat = new Intl.NumberFormat("en-US", {
maximumFractionDigits: 4,
});

interface AutoresearchPanelProps {
taskId: string;
}
Expand Down Expand Up @@ -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 (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
<StatCard
label="Best"
value={
summary.best ? (
<>
{withMetricUnit(numberFormat.format(summary.best.value), unit)}
<Text size="1" color="gray">
{" "}
(iter {summary.best.index})
</Text>
</>
) : (
"—"
)
<MetricCard
title="Best"
value={summary.best?.value ?? Number.NaN}
data={
iterations.length > 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"
/>
<StatCard
label="Last"
value={
summary.last
? withMetricUnit(numberFormat.format(summary.last.value), unit)
: "—"
<MetricCard
title="Last"
value={summary.last?.value ?? Number.NaN}
data={
iterations.length > 0
? iterations.map((iteration) => iteration.value)
: undefined
}
labels={labels}
theme={theme}
formatValue={formatMetricValue}
change={null}
dataAttr="autoresearch-stat-last"
/>
<StatCard
label="Iterations"
value={`${summary.iterationCount} / ${run.config.maxIterations}`}
<MetricCard
title="Iterations"
value={summary.iterationCount}
formatValue={(value) => `${value} / ${run.config.maxIterations}`}
change={null}
dataAttr="autoresearch-stat-iterations"
/>
<StatCard
label="Target"
value={
run.config.targetValue === null
? "—"
: withMetricUnit(numberFormat.format(run.config.targetValue), unit)
}
<MetricCard
title="Target"
value={run.config.targetValue ?? Number.NaN}
formatValue={formatMetricValue}
change={null}
dataAttr="autoresearch-stat-target"
/>
</div>
);
}

function StatCard({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="rounded-md border border-(--gray-5) bg-(--gray-2) px-3 py-2">
<Text as="div" size="1" color="gray">
{label}
</Text>
<Text as="div" size="2" weight="medium" className="tabular-nums">
{value}
</Text>
</div>
);
}

function stageText(
model: string | null,
effort: string | null,
Expand Down
40 changes: 19 additions & 21 deletions packages/ui/src/features/autoresearch/IterationsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,33 @@ 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[];
direction: AutoresearchDirection;
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<DeltaTone, "green" | "red" | "gray"> = {
improved: "green",
worsened: "red",
neutral: "gray",
};

export function IterationsTable({
iterations,
Expand Down Expand Up @@ -66,7 +66,10 @@ export function IterationsTable({
<Table.Cell>{iteration.index}</Table.Cell>
<Table.Cell>
<span className="flex items-center gap-1 tabular-nums">
{withMetricUnit(numberFormat.format(iteration.value), unit)}
{withMetricUnit(
metricNumberFormat.format(iteration.value),
unit,
)}
{best?.index === iteration.index && (
<Badge color="amber" size="1">
best
Expand All @@ -77,15 +80,10 @@ export function IterationsTable({
<Table.Cell>
<Text
size="1"
color={deltaColor(iteration.delta, direction)}
color={TONE_COLOR[deltaTone(iteration.delta, direction)]}
className="tabular-nums"
>
{iteration.delta === null
? "—"
: withMetricUnit(
`${iteration.delta > 0 ? "+" : ""}${numberFormat.format(iteration.delta)}`,
unit,
)}
{formatMetricDelta(iteration.delta, unit)}
</Text>
</Table.Cell>
<Table.Cell className="max-w-[320px]">
Expand Down
Loading
Loading