From 006d38bb11ce0bd3dbf2e21a2ef36ec3b27f3759 Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 7 Jul 2026 09:42:50 +0100 Subject: [PATCH 1/5] feat(autoresearch): export run report as HTML or PNG Adds an Export dropdown to the autoresearch run header that downloads the report as a self-contained HTML file or a PNG image. The report is generated from run data (header, stats, inline SVG metric chart, iterations table, brief) with inline styles and no external resources; the PNG path rasterizes the same markup via SVG foreignObject onto a canvas. Dependency-free. Generated-By: PostHog Code Task-Id: c0f4e30c-8329-433a-a2cb-0565df4ecb8b --- .../autoresearch/AutoresearchPanel.tsx | 42 +- .../autoresearch/reportExport.test.ts | 126 ++++++ .../src/features/autoresearch/reportExport.ts | 408 ++++++++++++++++++ 3 files changed, 574 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/features/autoresearch/reportExport.test.ts create mode 100644 packages/ui/src/features/autoresearch/reportExport.ts diff --git a/packages/ui/src/features/autoresearch/AutoresearchPanel.tsx b/packages/ui/src/features/autoresearch/AutoresearchPanel.tsx index 8cd14534bd..ea560bd3f9 100644 --- a/packages/ui/src/features/autoresearch/AutoresearchPanel.tsx +++ b/packages/ui/src/features/autoresearch/AutoresearchPanel.tsx @@ -1,4 +1,11 @@ -import { ChartLineUp, Pause, Play, Plus, Stop } from "@phosphor-icons/react"; +import { + ChartLineUp, + Export, + Pause, + Play, + Plus, + Stop, +} from "@phosphor-icons/react"; import type { AutoresearchService } from "@posthog/core/autoresearch/autoresearch"; import { AUTORESEARCH_SERVICE } from "@posthog/core/autoresearch/identifiers"; import type { @@ -14,7 +21,16 @@ import { EmptyMedia, EmptyTitle, } from "@posthog/quill"; -import { Badge, Button, Callout, Flex, Select, Text } from "@radix-ui/themes"; +import { toast } from "@posthog/ui/primitives/toast"; +import { + Badge, + Button, + Callout, + DropdownMenu, + Flex, + Select, + Text, +} from "@radix-ui/themes"; import { type ReactNode, useEffect, useMemo, useState } from "react"; import { getConfigOptionByCategory, @@ -25,6 +41,7 @@ import { AutoresearchConfigDialog } from "./AutoresearchConfigDialog"; import { IterationsTable } from "./IterationsTable"; import { MetricChart } from "./MetricChart"; import { withMetricUnit } from "./metricFormat"; +import { exportRunAsHtml, exportRunAsPng } from "./reportExport"; import { type AutoresearchModelOption, stageValueLabel, @@ -258,6 +275,27 @@ function RunHeader({ + + + + + + exportRunAsHtml(run)}> + HTML file + + { + void exportRunAsPng(run).catch(() => + toast.error("Couldn't render the report image"), + ); + }} + > + PNG image + + + {runs.length > 1 && ( diff --git a/packages/ui/src/features/autoresearch/reportExport.test.ts b/packages/ui/src/features/autoresearch/reportExport.test.ts new file mode 100644 index 0000000000..b20f79ae74 --- /dev/null +++ b/packages/ui/src/features/autoresearch/reportExport.test.ts @@ -0,0 +1,126 @@ +import type { + AutoresearchIteration, + AutoresearchRun, +} from "@posthog/core/autoresearch/schemas"; +import { describe, expect, it } from "vitest"; +import { + buildReportBody, + buildReportHtml, + reportFileName, +} from "./reportExport"; + +const EXPORTED_AT = new Date("2026-07-07T12:00:00Z"); + +function makeIteration( + overrides: Partial = {}, +): AutoresearchIteration { + return { + index: 1, + value: 100, + bestValue: 100, + delta: null, + summary: "Baseline", + at: Date.UTC(2026, 6, 7, 10, 0), + ...overrides, + }; +} + +function makeRun(overrides: Partial = {}): AutoresearchRun { + return { + id: "run-1", + config: { + taskId: "task-1", + direction: "minimize", + targetValue: 80, + maxIterations: 10, + implementModel: null, + measureModel: null, + implementEffort: null, + measureEffort: null, + instructions: "Shrink the bundle without breaking tests.", + }, + status: "completed", + metricName: "bundle size", + metricUnit: "kB", + phase: null, + originalModel: null, + originalEffort: null, + iterations: [ + makeIteration(), + makeIteration({ index: 2, value: 90, bestValue: 90, delta: -10 }), + ], + startedAt: Date.UTC(2026, 6, 7, 9, 0), + endedAt: Date.UTC(2026, 6, 7, 11, 0), + endReason: "target-reached", + interruptedReason: null, + lastError: null, + ...overrides, + }; +} + +describe("buildReportBody", () => { + it("includes the header, stats, chart, iterations, and brief", () => { + const body = buildReportBody(makeRun(), EXPORTED_AT); + expect(body).toContain("bundle size"); + expect(body).toContain("minimize"); + expect(body).toContain("Completed"); + expect(body).toContain("2 / 10"); + expect(body).toContain(" { + const body = buildReportBody(makeRun(), EXPORTED_AT); + expect(body).toContain(">best"); + // -10 on a minimize run is an improvement. + expect(body).toContain("-10 kB"); + }); + + it("escapes agent-provided text", () => { + const run = makeRun({ + metricName: ``, + iterations: [makeIteration({ summary: "" })], + }); + const body = buildReportBody(run, EXPORTED_AT); + expect(body).not.toContain("" })], - }); - const body = buildReportBody(run, EXPORTED_AT); - expect(body).not.toContain("