diff --git a/frontend/src/api/hooks/bench.ts b/frontend/src/api/hooks/bench.ts new file mode 100644 index 0000000..dd69cb6 --- /dev/null +++ b/frontend/src/api/hooks/bench.ts @@ -0,0 +1,12 @@ +/** React Query hook for the SKLD-bench summary endpoint. */ +import { useQuery } from "@tanstack/react-query"; + +import { apiClient } from "@/api/client"; +import type { BenchSummary } from "@/types"; + +export function useBenchSummary() { + return useQuery({ + queryKey: ["bench", "summary"] as const, + queryFn: () => apiClient.get("/api/bench/summary"), + }); +} diff --git a/frontend/src/api/hooks/runs.ts b/frontend/src/api/hooks/runs.ts index 4596a4a..1cf0b9c 100644 --- a/frontend/src/api/hooks/runs.ts +++ b/frontend/src/api/hooks/runs.ts @@ -42,11 +42,15 @@ export function useRunReport(runId: string | null) { }); } -export function useRunDimensions(runId: string | null) { +export function useRunDimensions( + runId: string | null, + opts: { refetchInterval?: number | false } = {}, +) { return useQuery({ queryKey: [...runKey(runId ?? ""), "dimensions"], queryFn: () => apiClient.get(`/api/runs/${runId}/dimensions`), enabled: !!runId, + refetchInterval: opts.refetchInterval ?? false, }); } diff --git a/frontend/src/api/hooks/seeds.ts b/frontend/src/api/hooks/seeds.ts new file mode 100644 index 0000000..e6347ec --- /dev/null +++ b/frontend/src/api/hooks/seeds.ts @@ -0,0 +1,18 @@ +/** + * React Query hooks for /api/seeds (the curated seed-library registry). + * + * See docs/clean-code.md §8 — no raw fetch() in components. + */ + +import { useQuery } from "@tanstack/react-query"; + +import { apiClient } from "@/api/client"; + +import type { SeedSummary } from "@/components/specializationInput/types"; + +export function useSeeds() { + return useQuery({ + queryKey: ["seeds"] as const, + queryFn: () => apiClient.get("/api/seeds"), + }); +} diff --git a/frontend/src/components/EvolutionArena.tsx b/frontend/src/components/EvolutionArena.tsx index d601cdc..c04036f 100644 --- a/frontend/src/components/EvolutionArena.tsx +++ b/frontend/src/components/EvolutionArena.tsx @@ -1,89 +1,65 @@ import { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router-dom"; +import { apiClient } from "@/api/client"; +import { useBenchSummary } from "@/api/hooks/bench"; +import { useRun, useRunDimensions } from "@/api/hooks/runs"; +import { useEvolutionSocket } from "@/hooks/useEvolutionSocket"; +import type { CompetitorView, DimensionStatus } from "@/types"; + import AtomicRunDetail from "./AtomicRunDetail"; import AtomicSidebar from "./AtomicSidebar"; import BreedingReport from "./BreedingReport"; import LiveFeedLog from "./LiveFeedLog"; import SkillVariantCard from "./SkillVariantCard"; -import StatusGlow from "./StatusGlow"; -import { useEvolutionSocket } from "../hooks/useEvolutionSocket"; -import type { BenchSummary, CompetitorView, DimensionStatus, RunDetail } from "../types"; - +import ArenaHeader from "./evolutionArena/ArenaHeader"; +import BaselineContext from "./evolutionArena/BaselineContext"; +import CompletedDimensions from "./evolutionArena/CompletedDimensions"; +import CompositeScoring from "./evolutionArena/CompositeScoring"; +import PerDimensionPipeline from "./evolutionArena/PerDimensionPipeline"; + +const BUDGET_CAP = 10; +const DIMENSIONS_POLL_MS = 5000; + +/** + * Live evolution "arena" — watches a run via WebSocket events + cached + * REST snapshots and renders the atomic-mode status board. On completion + * it hands off to ``AtomicRunDetail`` for the post-run showcase. + * + * Data comes from three sources: + * - ``useEvolutionSocket`` for real-time events (wins while active) + * - ``useRun`` / ``useRunDimensions`` / ``useBenchSummary`` for REST + * fallbacks + polling while active + * + * Right-column cards, header, and pipeline tracker live as sub-components + * under ``evolutionArena/`` so this file stays focused on composition. + */ export default function EvolutionArena() { const { runId } = useParams<{ runId: string }>(); - const [runDetail, setRunDetail] = useState(null); - const [dimensions, setDimensions] = useState([]); - const [benchBaseline, setBenchBaseline] = useState<{ - rawComposite: number | null; - families: number; - challenges: number; - } | null>(null); const [elapsed, setElapsed] = useState(0); - const startTime = useState(() => Date.now())[0]; + const [startTime] = useState(() => Date.now()); - // Fetch run detail once on mount + // Kick the permanent demo run if we're routed to it. useEffect(() => { - if (!runId) return; - // For the permanent demo, ensure it's running before fetching if (runId === "demo-live") { - fetch("/api/debug/demo", { method: "POST" }).catch(() => {}); + apiClient.post("/api/debug/demo").catch(() => undefined); } - fetch(`/api/runs/${runId}`) - .then((r) => r.json()) - .then((d: RunDetail) => setRunDetail(d)) - .catch(() => undefined); }, [runId]); - // Fetch dimension status for atomic runs - useEffect(() => { - if (!runId) return; - fetch(`/api/runs/${runId}/dimensions`) - .then((r) => { - if (!r.ok) return []; - return r.json() as Promise; - }) - .then(setDimensions) - .catch(() => setDimensions([])); - }, [runId]); - - // Fetch bench baseline data (once) - useEffect(() => { - fetch("/api/bench/summary") - .then((r) => (r.ok ? (r.json() as Promise) : null)) - .then((data) => { - if (data?.overall) { - setBenchBaseline({ - rawComposite: data.overall.raw_composite ?? null, - families: data.families.length, - challenges: data.overall.challenges, - }); - } - }) - .catch(() => {}); - }, []); - - // Poll dimensions while run is active (every 5s) - useEffect(() => { - if (!runId || runDetail?.status === "complete" || runDetail?.status === "failed") return; - const id = setInterval(() => { - fetch(`/api/runs/${runId}/dimensions`) - .then((r) => (r.ok ? (r.json() as Promise) : [])) - .then(setDimensions) - .catch(() => {}); - }, 5000); - return () => clearInterval(id); - }, [runId, runDetail?.status]); - + const { data: runDetail = null } = useRun(runId ?? null); const runAlreadyDone = runDetail?.status === "complete" || runDetail?.status === "failed"; - // Only open the WebSocket for runs that are still active + // Poll dimensions every 5s while the run is active; stop once it's done. + const { data: fetchedDimensions = [] } = useRunDimensions(runId ?? null, { + refetchInterval: runAlreadyDone ? false : DIMENSIONS_POLL_MS, + }); + + const { data: benchData } = useBenchSummary(); const sockState = useEvolutionSocket(runAlreadyDone ? null : (runId ?? null)); const isComplete = sockState.isComplete || runDetail?.status === "complete"; const isFailed = sockState.isFailed || runDetail?.status === "failed"; - // Tick elapsed timer useEffect(() => { if (isComplete || isFailed) return; const id = setInterval(() => { @@ -138,10 +114,11 @@ export default function EvolutionArena() { })); }, [sockState.competitors]); - // For demo/fake runs, build DimensionStatus[] from socket events - const effectiveDimensions = useMemo(() => { - if (dimensions.length > 0) return dimensions; - // Fallback: build from socket state for demo runs + // For demo/fake runs, reconstruct dimensions from the socket stream + // rather than the REST endpoint (which has nothing to return for + // scripted runs). + const effectiveDimensions: DimensionStatus[] = useMemo(() => { + if (fetchedDimensions.length > 0) return fetchedDimensions; return sockState.atomicDimensions.map((d) => ({ id: d.dimension, dimension: d.dimension, @@ -156,23 +133,30 @@ export default function EvolutionArena() { fitness_score: d.fitness ?? null, genome_id: null, })); - }, [dimensions, sockState.atomicDimensions]); + }, [fetchedDimensions, sockState.atomicDimensions]); - // Derive which dimension is currently running const activeDimension = useMemo(() => { const running = effectiveDimensions.find((d) => d.status === "running"); if (running) return running.dimension; return sockState.activeDimension ?? null; }, [effectiveDimensions, sockState.activeDimension]); - const completedDims = effectiveDimensions.filter((d) => d.status === "complete").length; + const completedDims = effectiveDimensions.filter((d) => d.status === "complete"); const totalDims = effectiveDimensions.length; + const benchBaseline = benchData?.overall + ? { + rawComposite: benchData.overall.raw_composite ?? null, + challenges: benchData.overall.challenges, + families: benchData.families.length, + } + : null; + // --- All hooks above this line --- if (!runId) return null; - // Completed runs → AtomicRunDetail showcase + // Completed runs → AtomicRunDetail showcase. if (isComplete) { return ( @@ -180,12 +164,18 @@ export default function EvolutionArena() { } const showBreeding = !!sockState.latestBreedingReport; - const elapsedFmt = `${Math.floor(elapsed / 60) - .toString() - .padStart(2, "0")}:${(elapsed % 60).toString().padStart(2, "0")}`; - const budgetCap = 10; + const handleCancel = async () => { + if (!window.confirm("Cancel this run? Completed dimensions will be saved.")) return; + try { + await apiClient.post(`/api/runs/${runId}/cancel`); + } catch (err) { + alert(`Cancel error: ${String(err)}`); + } + }; + + const activeDim = sockState.atomicDimensions.find((d) => d.dimension === activeDimension); + const bestThisDimension = sockState.generations.at(-1)?.best_fitness ?? null; - // --- Atomic in-progress layout --- return (
- {/* Header */} -
-
-

- {activeDimension - ? `Dimension ${completedDims + 1} of ${totalDims} · ${activeDimension.replace(/-/g, " ")}` - : `Evolving ${totalDims} dimensions`} -

-

- {specialization || "Atomic Evolution"} -

-
- - - {isFailed ? "FAILED" : "RUNNING"} - - - {completedDims}/{totalDims} dimensions - -
-
-
-
-

- Elapsed -

-

{elapsedFmt}

-

- Budget Used -

-

- ${sockState.totalCostUsd.toFixed(2)} / ${budgetCap.toFixed(2)} -

-
- {!isComplete && !isFailed && ( - - )} -
-
+ - {/* Connection/error banners */} {sockState.status === "closed" && !isComplete && !isFailed && (
Connection lost. Reconnecting... @@ -266,89 +210,19 @@ export default function EvolutionArena() { )}
- {/* Main column */}
- {/* Phase status — shows what the engine is doing during long waits */} - {(() => { - const activeDim = sockState.atomicDimensions.find( - (d) => d.dimension === activeDimension, - ); - if (!activeDim?.phaseDetail) return null; - // Don't show once competitors are running - if (variantGroups.length > 0) return null; - return ( -
-
-

{activeDim.phaseDetail}

-
- ); - })()} - - {/* Current challenge — single card, not a list */} - {challenges.length > 0 && - (() => { - const ch = challenges[challenges.length - 1]; - return ( -
- - {ch.difficulty} - -

{ch.prompt}

-
- ); - })()} - - {/* Competition */} -
-
-
-

Competition

-

- Baseline vs seed vs spawn — scored with 6-layer composite. -

-
- - {variantGroups.length > 0 ? `${variantGroups.length} competitors` : "waiting"} - -
-
- {variantGroups.length === 0 ? ( -
-
-

- Generating skill variants — each variant is a complete SKILL.md package with - scripts, references, and examples. This typically takes 1-2 minutes per - dimension. -

-
- ) : ( - [...variantGroups].reverse().map((g) => { - const isBaseline = g.competitorId === 0; - const labels = ["Baseline (Raw Sonnet)", "Seed (V1)", "Spawn (V2)"]; - return ( - - ); - }) - )} + {activeDim?.phaseDetail && variantGroups.length === 0 && ( +
+
+

{activeDim.phaseDetail}

-
+ )} + + {challenges.length > 0 && ( + + )} + + {showBreeding && (
- {/* Right column: completed dimensions + judging */}
- {/* Completed dimensions summary */} - {completedDims > 0 && ( -
-

- Completed Dimensions -

-
- {effectiveDimensions - .filter((d) => d.status === "complete") - .map((d) => ( -
- - {d.dimension.replace(/-/g, " ")} - - - {d.fitness_score?.toFixed(2) ?? "—"} - -
- ))} -
-
-
- - Avg Fitness - - - {( - effectiveDimensions - .filter((d) => d.status === "complete" && d.fitness_score != null) - .reduce((sum, d) => sum + (d.fitness_score ?? 0), 0) / - Math.max(completedDims, 1) - ).toFixed(3)} - -
-
-
- )} - - {/* Composite Scoring */} -
-

- Composite Scoring -

-

- Each variant is scored through 6 layers, weighted into one composite fitness. -

-
- {[ - { - label: "Behavioral Tests", - weight: "40%", - desc: "ExUnit — does the code work?", - }, - { label: "Compilation", weight: "15%", desc: "mix compile — does it build?" }, - { label: "AST Quality", weight: "15%", desc: "Structure, coverage, pipes" }, - { label: "String Match", weight: "10%", desc: "L0 expected patterns" }, - { label: "Template", weight: "10%", desc: "Modern HEEx idioms" }, - { label: "Brevity", weight: "10%", desc: "Conciseness" }, - ].map((layer) => ( -
- - {layer.weight} - - {layer.label} - - {layer.desc} - -
- ))} -
- {activeDimension && sockState.generations.at(-1)?.best_fitness != null && ( -
-
- - Best This Dimension - - - {sockState.generations.at(-1)!.best_fitness!.toFixed(3)} - -
-
- )} -
- - {/* Baseline Context */} -
-

- Baseline — Raw Sonnet -

-

- What Claude Sonnet scores with no skill on the same challenges. The goal: evolved - skill consistently beats baseline. -

- {benchBaseline ? ( -
-
- Raw Composite - - {benchBaseline.rawComposite?.toFixed(3) ?? "—"} - -
-
- Challenges - - {benchBaseline.challenges} - -
-
- Families - - {benchBaseline.families} - -
- {runDetail?.baseline_fitness != null && ( -
- This Family Baseline - - {runDetail.baseline_fitness.toFixed(3)} - -
- )} -
- ) : ( -

Loading baseline data...

- )} -
- - {/* Per-dimension pipeline steps */} -
-

- Per-Dimension Pipeline -

-
- {[ - { step: "1", label: "Design focused challenge", done: challenges.length > 0 }, - { step: "2", label: "Spawn seed + alternative", done: variantGroups.length >= 2 }, - { - step: "3", - label: "Compete on challenge", - done: sockState.finishedCompetitors >= 2, - }, - { - step: "4", - label: "Score (6-layer composite)", - done: sockState.currentJudgingLayer >= 5, - }, - { - step: "5", - label: "Pick winner", - done: sockState.generations.at(-1)?.status === "complete", - }, - ].map((s) => ( -
- - {s.done ? "✓" : s.step} - - - {s.label} - -
- ))} -
-
+ + + + 0} + variantsSpawned={variantGroups.length >= 2} + competitorsFinished={sockState.finishedCompetitors >= 2} + scored={sockState.currentJudgingLayer >= 5} + winnerPicked={sockState.generations.at(-1)?.status === "complete"} + />
); } + +interface CurrentChallengeProps { + challenge: { difficulty: string; prompt: string }; +} + +function CurrentChallenge({ challenge }: CurrentChallengeProps) { + const difficultyClass = + challenge.difficulty === "hard" + ? "bg-error/10 text-error" + : challenge.difficulty === "medium" + ? "bg-warning/10 text-warning" + : "bg-tertiary/10 text-tertiary"; + return ( +
+ + {challenge.difficulty} + +

{challenge.prompt}

+
+ ); +} + +interface CompetitionPanelProps { + variantGroups: { + variantIndex: number; + skillId: string; + competitorId: number; + competitors: CompetitorView[]; + }[]; + challenges: { id: string; difficulty: string; prompt: string; index: number }[]; +} + +function CompetitionPanel({ variantGroups, challenges }: CompetitionPanelProps) { + const labels = ["Baseline (Raw Sonnet)", "Seed (V1)", "Spawn (V2)"]; + return ( +
+
+
+

Competition

+

+ Baseline vs seed vs spawn — scored with 6-layer composite. +

+
+ + {variantGroups.length > 0 ? `${variantGroups.length} competitors` : "waiting"} + +
+
+ {variantGroups.length === 0 ? ( +
+
+

+ Generating skill variants — each variant is a complete SKILL.md package with scripts, + references, and examples. This typically takes 1-2 minutes per dimension. +

+
+ ) : ( + [...variantGroups] + .reverse() + .map((g) => ( + + )) + )} +
+
+ ); +} diff --git a/frontend/src/components/PackageExplorer.test.tsx b/frontend/src/components/PackageExplorer.test.tsx new file mode 100644 index 0000000..2f5a5ff --- /dev/null +++ b/frontend/src/components/PackageExplorer.test.tsx @@ -0,0 +1,72 @@ +// @vitest-environment jsdom +// +// Smoke test for PackageExplorer — renders with realistic fixture data, +// asserts the master-detail layout reaches the DOM (installable section, +// metadata section, download link). Detailed pure-logic assertions live +// in packageExplorer/buildFiles.test.ts. + +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import type { RunReportChallenge, RunReportGenome } from "@/types"; + +import PackageExplorer from "./PackageExplorer"; + +const compositeGenome: RunReportGenome = { + id: "composite_abc", + generation: 1, + skill_md_content: "---\nname: composite\n---\n# composite", + frontmatter: { name: "composite", description: "x" }, + supporting_files: { + "scripts/validate.sh": "#!/bin/bash\necho ok", + "references/guide.md": "# guide", + }, + traits: [], + meta_strategy: "engineer_composite", + parent_ids: [], + mutations: [], + mutation_rationale: "", + maturity: "draft", + pareto_objectives: {}, + deterministic_scores: {}, +} as unknown as RunReportGenome; + +const winner: RunReportGenome = { + ...compositeGenome, + id: "gen_seed_streams_winner", + meta_strategy: "seed_pipeline_winner", +}; + +const challenge: RunReportChallenge = { + id: "easy-01", + prompt: "stub", + difficulty: "easy", + evaluation_criteria: {}, + verification_method: "judge_review", + setup_files: {}, + gold_standard_hints: "", +} as unknown as RunReportChallenge; + +describe("PackageExplorer", () => { + it("renders the installable section, metadata section, and download link", () => { + render( + , + ); + + expect(screen.getByText(/Skill package · Pre-download view/)).toBeTruthy(); + // Installable section: SKILL.md + the two supporting files = 3 + expect(screen.getByText(/Installable · 3/)).toBeTruthy(); + // Metadata: PACKAGE.md + REPORT.md + 1 parent + 1 challenge = 4 + expect(screen.getByText(/Evolution metadata · 4/)).toBeTruthy(); + const download = screen.getByRole("link", { name: /Download \.zip/ }); + expect(download.getAttribute("href")).toBe("/api/runs/run-smoke-1/export?format=skill_dir"); + expect(screen.getByText(/Gold Standard Checklist/)).toBeTruthy(); + }); +}); diff --git a/frontend/src/components/PackageExplorer.tsx b/frontend/src/components/PackageExplorer.tsx index e838ac2..bb67c08 100644 --- a/frontend/src/components/PackageExplorer.tsx +++ b/frontend/src/components/PackageExplorer.tsx @@ -2,8 +2,12 @@ import { useMemo, useState } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; +import type { RunReportChallenge, RunReportGenome } from "@/types"; + import CodeViewer from "./CodeViewer"; -import type { RunReportChallenge, RunReportGenome } from "../types"; +import FileTree from "./packageExplorer/FileTree"; +import GoldStandardChecklist from "./packageExplorer/GoldStandardChecklist"; +import { buildFiles } from "./packageExplorer/buildFiles"; interface PackageExplorerProps { compositeSkillMd: string | null; @@ -14,21 +18,6 @@ interface PackageExplorerProps { familyLabel: string; } -type VirtualFile = { - path: string; - content: string; - // "markdown" = render via ReactMarkdown. "code" = render via CodeViewer - // which picks syntax based on file extension. - language: "markdown" | "code"; - // "installable" = part of the downloadable .zip, loaded by Claude. - // "metadata" = evolution artifact, NOT loaded at runtime, kept for audit. - kind: "installable" | "metadata"; -}; - -function detectLanguage(path: string): "markdown" | "code" { - return path.endsWith(".md") ? "markdown" : "code"; -} - /** * Package explorer — separates the real installable skill package from * evolution metadata so a user can see exactly what they'd download and @@ -41,12 +30,11 @@ function detectLanguage(path: string): "markdown" | "code" { * - Metadata section: parents/, challenges/, PACKAGE.md, REPORT.md. * Preserved for audit + browsing but NOT in the deployable zip. * - Gold Standard Checklist: transparent comparison against the Skill - * Authoring Constraints in CLAUDE.md. Shows which files are present - * and which would be produced by a richer evolution pipeline. + * Authoring Constraints in CLAUDE.md. * - * The tree is rendered as a custom flat list grouped by section so we can - * force SKILL.md at the very top without fighting the shared FileTree's - * directories-first sort. + * File list building lives in packageExplorer/buildFiles.ts; the tree + * widget is packageExplorer/FileTree; the checklist is its own component. + * This file is the master-detail orchestrator. */ export default function PackageExplorer({ compositeSkillMd, @@ -56,91 +44,18 @@ export default function PackageExplorer({ runId, familyLabel, }: PackageExplorerProps) { - const { installable, metadata } = useMemo(() => { - const inst: VirtualFile[] = []; - const meta: VirtualFile[] = []; - - // 1. SKILL.md — the one real file that always ships. - inst.push({ - path: "SKILL.md", - content: compositeSkillMd ?? "# (composite SKILL.md not loaded)", - language: "markdown", - kind: "installable", - }); - - // 2. Real supporting_files from the composite genome. These are the - // rich-package artifacts (scripts/*, references/*, test_fixtures/*, - // assets/*) produced by post-assembly enrichment OR by a production - // engine that natively generates rich directory packages. Sorted so - // directories group together visually. - const composite = genomes.find((g) => g.meta_strategy === "engineer_composite"); - const supportingFiles = composite?.supporting_files ?? {}; - const sortedPaths = Object.keys(supportingFiles).sort((a, b) => { - // Group by top-level directory, then alphabetical within. - const dirA = a.split("/")[0]; - const dirB = b.split("/")[0]; - if (dirA !== dirB) return dirA.localeCompare(dirB); - return a.localeCompare(b); - }); - for (const path of sortedPaths) { - inst.push({ - path, - content: supportingFiles[path], - language: detectLanguage(path), - kind: "installable", - }); - } - - // 3. PACKAGE.md — metadata, synthesized. - const winnerGenomes = genomes.filter((g) => g.meta_strategy === "seed_pipeline_winner"); - meta.push({ - path: "_meta/PACKAGE.md", - content: buildPackageMd({ + const { installable, metadata } = useMemo( + () => + buildFiles({ + compositeSkillMd, + genomes, + challenges, + learningLog, runId, familyLabel, - winnerCount: winnerGenomes.length, - challengeCount: challenges.length, - genomeCount: genomes.length, - installableCount: inst.length, }), - language: "markdown", - kind: "metadata", - }); - - // 4. REPORT.md — integration report from learning_log - const reportEntry = learningLog.find((e) => e.startsWith("[integration_report] ")); - if (reportEntry) { - meta.push({ - path: "_meta/REPORT.md", - content: reportEntry.replace("[integration_report] ", ""), - language: "markdown", - kind: "metadata", - }); - } - - // 5. parents/ — 12 winning variant SKILL.md files - for (const g of winnerGenomes) { - const dim = deriveDimensionFromId(g.id); - meta.push({ - path: `_meta/parents/${dim}.md`, - content: g.skill_md_content, - language: "markdown", - kind: "metadata", - }); - } - - // 6. challenges/ — all 24 JSON specs - for (const c of challenges) { - meta.push({ - path: `_meta/challenges/${c.id}.json`, - content: JSON.stringify(c, null, 2), - language: "code", - kind: "metadata", - }); - } - - return { installable: inst, metadata: meta }; - }, [compositeSkillMd, genomes, challenges, learningLog, runId, familyLabel]); + [compositeSkillMd, genomes, challenges, learningLog, runId, familyLabel], + ); const allFiles = useMemo(() => [...installable, ...metadata], [installable, metadata]); @@ -148,110 +63,43 @@ export default function PackageExplorer({ const [selectedPath, setSelectedPath] = useState("SKILL.md"); const selectedFile = allFiles.find((f) => f.path === selectedPath); - // Gold Standard Checklist — compare against the CLAUDE.md Skill Authoring - // Constraints. For each recommended file, is it present in the - // installable set? - const checklist = useMemo(() => { - const present = new Set(installable.map((f) => f.path)); - return [ - { - path: "SKILL.md", - label: "SKILL.md with frontmatter + body", - kind: "required", - present: present.has("SKILL.md"), - }, - { - path: "scripts/validate.sh", - label: "scripts/validate.sh (structural self-check)", - kind: "recommended", - present: present.has("scripts/validate.sh"), - }, - { - path: "scripts/main_helper.py", - label: "scripts/main_helper.py (deterministic helper)", - kind: "recommended", - present: present.has("scripts/main_helper.py"), - }, - { - path: "references/guide.md", - label: "references/guide.md (domain reference)", - kind: "recommended", - present: present.has("references/guide.md"), - }, - { - path: "test_fixtures/", - label: "test_fixtures/ (sample input files)", - kind: "optional", - present: Array.from(present).some((p) => p.startsWith("test_fixtures/")), - }, - { - path: "assets/", - label: "assets/ (templates, configs)", - kind: "optional", - present: Array.from(present).some((p) => p.startsWith("assets/")), - }, - ]; - }, [installable]); - const isMarkdown = selectedFile?.language === "markdown"; - const bodyOnly = useMemo(() => { + const markdownBody = useMemo(() => { if (!selectedFile || !isMarkdown) return ""; + // Strip YAML frontmatter from the rendered view — the browser shows + // the body only, since the frontmatter is already surfaced in the + // header/badge row above. const m = selectedFile.content.match(/^---\s*\n[\s\S]*?\n---\s*\n([\s\S]*)$/); return m?.[1] ?? selectedFile.content; }, [selectedFile, isMarkdown]); return (
- {/* Header with honest framing */} -
-
-
-

- Skill package · Pre-download view -

-

- - {installable.length} installable {installable.length === 1 ? "file" : "files"} - {" "} - ship in the downloadable zip and are loaded by Claude at runtime. {metadata.length}{" "} - additional metadata files are preserved for auditing the evolution process but are NOT - part of the deployable package. -

-
- - ↓ Download .zip - -
-
+ - {/* Master-detail: sectioned file list on left, viewer on right */}
- {/* Gold Standard Checklist */} -
-

- Gold Standard Checklist -

-

- What this package contains vs. the Skill Authoring Constraints in{" "} - CLAUDE.md. -

-
- {checklist.map((item) => ( -
- - {item.present ? "✓" : "○"} - -
-

{item.label}

-

- {item.kind} ·{" "} - {item.present ? "present in this package" : "not generated by this run"} -

-
-
- ))} -
-
+
); } -interface FileRowProps { - file: VirtualFile; - selected: boolean; - onSelect: () => void; - indent?: number; +interface ExplorerHeaderProps { + installableCount: number; + metadataCount: number; + runId: string; } -function FileRow({ file, selected, onSelect, indent = 0 }: FileRowProps) { - // Show only the basename in the list. - const basename = file.path.split("/").pop() ?? file.path; - const icon = basename.endsWith(".json") ? "{ }" : "📄"; +function ExplorerHeader({ installableCount, metadataCount, runId }: ExplorerHeaderProps) { return ( - +
+
+
+

+ Skill package · Pre-download view +

+

+ + {installableCount} installable {installableCount === 1 ? "file" : "files"} + {" "} + ship in the downloadable zip and are loaded by Claude at runtime. {metadataCount}{" "} + additional metadata files are preserved for auditing the evolution process but are NOT + part of the deployable package. +

+
+ + ↓ Download .zip + +
+
); } -interface FileTreeSectionProps { - files: VirtualFile[]; - selectedPath: string; - onSelect: (path: string) => void; - stripPrefix?: string; - defaultOpen?: boolean; +interface FileViewerProps { + file: { path: string; content: string; kind: "installable" | "metadata" }; + isMarkdown: boolean; + markdownBody: string; } -/** - * Collapsible tree for a section of files. Groups files under their - * top-level subdirectory so ``scripts/*`` and ``references/*`` render as - * collapsible directory groups. Standalone top-level files render above - * the directory groups. - * - * Props: - * - ``stripPrefix``: an optional path prefix to strip from each file - * (e.g. ``"_meta/"`` for the metadata section) before grouping. - * - ``defaultOpen``: whether directories start expanded (true) or - * collapsed (false). - */ -function FileTreeSection({ - files, - selectedPath, - onSelect, - stripPrefix = "", - defaultOpen = true, -}: FileTreeSectionProps) { - const { topLevel, dirs, allDirNames } = useMemo(() => { - const top: VirtualFile[] = []; - const grouped = new Map(); - const dirNames: string[] = []; - for (const f of files) { - const rest = stripPrefix ? f.path.replace(new RegExp(`^${stripPrefix}`), "") : f.path; - if (!rest.includes("/")) { - top.push(f); - } else { - const dirName = rest.split("/")[0]; - if (!grouped.has(dirName)) { - grouped.set(dirName, []); - dirNames.push(dirName); - } - grouped.get(dirName)!.push(f); - } - } - return { topLevel: top, dirs: grouped, allDirNames: dirNames }; - }, [files, stripPrefix]); - - const [openDirs, setOpenDirs] = useState>(() => - defaultOpen ? new Set(allDirNames) : new Set(), - ); - - const toggle = (dir: string) => { - setOpenDirs((prev) => { - const next = new Set(prev); - if (next.has(dir)) next.delete(dir); - else next.add(dir); - return next; - }); - }; - - // The indent for nested items depends on whether we're stripping a prefix. - // Files inside a stripped-prefix path look as if they live at the root so - // directory rows get no extra indent. - const indent = 0; - +function FileViewer({ file, isMarkdown, markdownBody }: FileViewerProps) { return ( -
- {topLevel.map((f) => ( - onSelect(f.path)} - indent={indent} - /> - ))} - {Array.from(dirs.entries()).map(([dir, dirFiles]) => { - const isOpen = openDirs.has(dir); - return ( -
- - {isOpen && - dirFiles.map((f) => ( - onSelect(f.path)} - indent={indent + 1} - /> - ))} + <> +
+

+ {file.path} +

+ {file.kind === "installable" ? ( + + Installable + + ) : ( + + Metadata + + )} +
+
+ {isMarkdown ? ( +
+ {markdownBody}
- ); - })} -
+ ) : ( + + )} +
+ ); } - -function deriveDimensionFromId(id: string): string { - let slug = id - .replace(/^gen_seed_/, "") - .replace(/_winner$/, "") - .replace(/^elixir_phoenix_liveview_?/, ""); - slug = slug.replace(/_/g, "-"); - return slug || id; -} - -interface PackageMdInput { - runId: string; - familyLabel: string; - winnerCount: number; - challengeCount: number; - genomeCount: number; - installableCount: number; -} - -function buildPackageMd({ - runId, - familyLabel, - winnerCount, - challengeCount, - genomeCount, - installableCount, -}: PackageMdInput): string { - return `# ${familyLabel} — Package Metadata - -**Run ID**: \`${runId}\` -**Evolution mode**: atomic -**Generation**: 1 - -## What's actually in the .zip - -${installableCount} installable file${installableCount === 1 ? "" : "s"} (SKILL.md + scripts/, references/, test_fixtures/, assets/) ship in the downloadable package. Everything under -\`_meta/\` — including this file — is preserved for auditing the evolution -process but is NOT loaded by Claude at runtime. - -| Location | Role | -|-----------------|------------------------------------------| -| \`SKILL.md\` | The composite skill — deploy this | -| \`_meta/PACKAGE.md\` | This manifest (metadata only) | -| \`_meta/REPORT.md\` | Engineer integration report | -| \`_meta/parents/*.md\` | ${winnerCount} winning variant sources | -| \`_meta/challenges/*.json\` | ${challengeCount} L1 test specs | - -## Totals - -- **${genomeCount}** SkillGenomes (seeds + winners + composite) -- **${winnerCount}** winning variants merged into the composite -- **${challengeCount}** L1 test challenges sampled - -## How to deploy - -1. Extract the .zip into your project. -2. Place \`SKILL.md\` at \`.claude/skills//SKILL.md\`. -3. Delete the \`_meta/\` directory if present — Claude ignores it, but - removing it keeps your deployment clean. -4. Done. Claude Code will pick up the skill on next restart. - -## What's NOT in this package - -A richer skill would also include: - -- \`scripts/validate.sh\` — structural self-check -- \`scripts/main_helper.py\` — deterministic helper (parser, formatter, generator) -- \`references/guide.md\` — domain reference doc Claude reads on demand -- \`test_fixtures/\` — sample inputs -- \`assets/\` — templates and static resources - -The evolution pipeline that produced this skill only generated prose rules, -not helper scripts or reference files. A production pipeline could add a -\`.claude/skills/scripter/\` agent per dimension to produce those. - -## Generation lineage - -This is **Generation 1** — produced by evolving a pre-existing seed plus -spawned alternatives across 12 dimensions. A re-evolution would produce -Generation 2 with this composite as the seed input. - -For full context on assembly decisions, see \`_meta/REPORT.md\`. -`; -} diff --git a/frontend/src/components/PipelineSteps.tsx b/frontend/src/components/PipelineSteps.tsx index d2bdac6..ee8fae8 100644 --- a/frontend/src/components/PipelineSteps.tsx +++ b/frontend/src/components/PipelineSteps.tsx @@ -1,704 +1,15 @@ import { useEffect, useRef, useState } from "react"; -interface Step { - number: number; - title: string; - description: string; - metric: string; - isLoop?: boolean; - visual: () => JSX.Element; -} - -/* ── Mini-visualizations per step ─────────────────────────────────── */ - -/** Step 1: network graph of ecosystem nodes */ -function VisualResearch() { - return ( - - {/* nodes */} - {[ - [20, 40], - [45, 15], - [45, 65], - [70, 30], - [70, 55], - [95, 20], - [95, 45], - [95, 70], - ].map(([cx, cy], i) => ( - - - - ))} - {/* edges */} - {[ - [20, 40, 45, 15], - [20, 40, 45, 65], - [45, 15, 70, 30], - [45, 65, 70, 55], - [70, 30, 95, 20], - [70, 30, 95, 45], - [70, 55, 95, 45], - [70, 55, 95, 70], - ].map(([x1, y1, x2, y2], i) => ( - - ))} - - ); -} - -/** Step 2: ranked bars with top 7 highlighted */ -function VisualSelect() { - const heights = [95, 82, 78, 72, 68, 60, 55, 45, 38, 30]; - return ( - - {heights.map((h, i) => ( - - ))} - - - ); -} - -/** Step 3: tree decomposition */ -function VisualDecompose() { - return ( - - - {[20, 50, 80].map((x, i) => ( - - - - {[x - 8, x, x + 8].map((cx, j) => ( - - - - - ))} - - ))} - - ); -} - -/** Step 4: tiered challenge grid */ -function VisualChallenges() { - const tiers = [ - { y: 8, count: 8, color: "text-green-400/40" }, - { y: 24, count: 6, color: "text-yellow-400/40" }, - { y: 40, count: 4, color: "text-orange-400/40" }, - { y: 56, count: 2, color: "text-red-400/40" }, - ]; - return ( - - {tiers.map((tier, ti) => - Array.from({ length: tier.count }).map((_, i) => ( - - )), - )} - {["E", "M", "H", "L"].map((label, i) => ( - - {label} - - ))} - - ); -} - -/** Step 5: score bar dropping */ -function VisualBaseline() { - const bars = [ - { label: "L0", w: 93, color: "text-primary/40" }, - { label: "+C", w: 54, color: "text-tertiary/50" }, - { label: "=F", w: 51, color: "text-on-surface-dim/30" }, - ]; - return ( - - {bars.map((bar, i) => ( - - - {bar.label} - - - - {bar.w}% - - - ))} - - ); -} - -/** Step 6: file tree */ -function VisualSeed() { - const files = [ - { indent: 0, name: "SKILL.md", accent: true }, - { indent: 0, name: "scripts/", accent: false }, - { indent: 1, name: "validate.sh", accent: false }, - { indent: 1, name: "helper.py", accent: false }, - { indent: 0, name: "references/", accent: false }, - { indent: 1, name: "guide.md", accent: false }, - ]; - return ( - - {files.map((f, i) => ( - - - - {f.name} - - - ))} - - ); -} - -/** Step 7: branching variants */ -function VisualSpawn() { - return ( - - {/* center seed */} - - - S - - {/* branch lines + variant dots */} - {[15, 30, 45, 60].map((y, i) => ( - - - - - - - ))} - - ); -} - -/** Step 8: versus matchup */ -function VisualCompete() { - return ( - - {/* left variant */} - - - V1 - - {/* VS */} - - vs - - {/* right variant */} - - - V2 - - {/* challenge dots */} - {[28, 40, 52].map((y, i) => ( - - - - - ))} - - ); -} - -/** Step 9: scoring layers stacked */ -function VisualScore() { - const layers = [ - { label: "L0", w: 85, color: "text-on-surface-dim/20" }, - { label: "CC", w: 70, color: "text-on-surface-dim/25" }, - { label: "AST", w: 55, color: "text-primary/25" }, - { label: "BEH", w: 90, color: "text-tertiary/40" }, - ]; - return ( - - {layers.map((l, i) => ( - - - - {l.label} - - - ))} - - ); -} - -/** Step 10: selection funnel */ -function VisualBreed() { - return ( - - {/* population dots top */} - {[20, 35, 50, 65, 80, 95].map((x, i) => ( - - ))} - {/* funnel lines */} - - - {/* winners bottom */} - {[35, 55, 75].map((x, i) => ( - - ))} - {/* arrows */} - - - - - - - - ); -} - -/** Step 11: merging pieces */ -function VisualAssemble() { - return ( - - {/* incoming pieces */} - {[ - [10, 10], - [10, 30], - [10, 50], - [10, 70], - [35, 10], - [35, 30], - [35, 50], - [35, 70], - ].map(([x, y], i) => ( - - - - - ))} - {/* composite result */} - - - SKILL - - - .md - - - ); -} - -/** Step 12: launch / ship */ -function VisualShip() { - return ( - - {/* checkmarks */} - {Array.from({ length: 7 }).map((_, i) => ( - - - - - ))} - {/* arrow up */} - - - {/* Registry label */} - - - Registry - - - ); -} - -const STEPS: Step[] = [ - { - number: 1, - title: "Research Domain", - description: "Analyze the target ecosystem for skill families worth evolving", - metric: "34 candidates identified", - visual: VisualResearch, - }, - { - number: 2, - title: "Select Lighthouse Families", - description: "Rank by community impact, complexity, and LLM failure rate", - metric: "7 families selected", - visual: VisualSelect, - }, - { - number: 3, - title: "Decompose into Capabilities", - description: "Break each skill into atomic, independently-evolvable dimensions", - metric: "83 dimensions total", - visual: VisualDecompose, - }, - { - number: 4, - title: "Generate SKLD-bench Challenges", - description: "Author challenges per tier: easy, medium, hard, legendary", - metric: "867 challenges authored", - visual: VisualChallenges, - }, - { - number: 5, - title: "Run Baselines", - description: "Score raw Sonnet with no skill guidance to establish the floor", - metric: "93.3% L0, 51.1% composite", - visual: VisualBaseline, - }, - { - number: 6, - title: "Research & Create Seed Skill", - description: "Build a golden-template package: SKILL.md + scripts + references", - metric: "7 seed packages", - visual: VisualSeed, - }, - { - number: 7, - title: "Spawn Variants", - description: "Generate diverse alternatives per dimension", - metric: "2 variants x 12 dimensions", - isLoop: true, - visual: VisualSpawn, - }, - { - number: 8, - title: "Compete", - description: "Run both variants against sampled challenges from the bench", - metric: "4 dispatches per dimension", - isLoop: true, - visual: VisualCompete, - }, - { - number: 9, - title: "Score", - description: "L0 string match + Compile + AST + Behavioral = composite fitness", - metric: "6-layer composite scorer", - isLoop: true, - visual: VisualScore, - }, - { - number: 10, - title: "Judge & Breed", - description: "Pick winners, mutate losers based on execution traces", - metric: "Repeat N generations", - isLoop: true, - visual: VisualBreed, - }, - { - number: 11, - title: "Assemble Composite", - description: "Merge winning variants from all dimensions into one skill", - metric: "1 composite package", - visual: VisualAssemble, - }, - { - number: 12, - title: "Ship", - description: "Install test, extract findings to the Bible, publish to Registry", - metric: "7/7 positive skill lift", - visual: VisualShip, - }, -]; +import { STEPS } from "./pipelineSteps/steps"; /** * Animated process flow showing the 12 steps of the SKLD pipeline. - * Steps appear as the user scrolls down using Intersection Observer. + * Each step fades in as the user scrolls to it via IntersectionObserver. + * + * The 12 SVG illustrations live in ``pipelineSteps/foundationVisuals`` + * (steps 1-6, one-shot setup) and ``pipelineSteps/loopVisuals`` (steps + * 7-12, the evolution loop + shipping). Step metadata + ordering lives + * in ``pipelineSteps/steps.ts``. This file is the observer + layout. */ export default function PipelineSteps() { const [visibleSteps, setVisibleSteps] = useState>(new Set()); @@ -759,7 +70,6 @@ export default function PipelineSteps() { isVisible ? "translate-y-0 opacity-100" : "translate-y-8 opacity-0" }`} > - {/* Timeline dot */}
- {/* Content card */}
{step.description}

{step.metric}

- {/* Visual illustration */}
diff --git a/frontend/src/components/SpecializationInput.tsx b/frontend/src/components/SpecializationInput.tsx index 1f2e8cb..1fdd214 100644 --- a/frontend/src/components/SpecializationInput.tsx +++ b/frontend/src/components/SpecializationInput.tsx @@ -1,57 +1,35 @@ -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; +import { useSeeds } from "@/api/hooks/seeds"; + import InviteGate from "./InviteGate"; import ParameterInput from "./ParameterInput"; -import PrimaryButton from "./PrimaryButton"; import SkillUploader from "./SkillUploader"; import SpecAssistantChat from "./SpecAssistantChat"; -import type { EvolveRequest, EvolveResponse } from "../types"; - -type SourceMode = "scratch" | "upload" | "fork"; - -interface UploadResponse { - upload_id: string | null; - filename: string; - valid: boolean; - frontmatter?: Record; - skill_md_content?: string; - supporting_files?: string[]; - errors?: string[]; -} - -interface SeedSummary { - id: string; - title: string; - description: string; - category: string; - difficulty: "easy" | "medium" | "hard"; -} - -const DIFFICULTY_COLOR: Record = { - easy: "text-tertiary", - medium: "text-warning", - hard: "text-error", -}; - -const SOURCE_MODES: { value: SourceMode; label: string; hint: string }[] = [ - { - value: "scratch", - label: "From Scratch", - hint: "Describe a domain and evolve a new Skill from the golden template.", - }, - { - value: "upload", - label: "Upload Existing", - hint: "Bring your own SKILL.md (or zipped Skill dir) and evolve it forward.", - }, - { - value: "fork", - label: "Fork from Registry", - hint: "Pick a curated Gen 0 Skill from the library as your starting point.", - }, -]; +import EvolutionModePicker from "./specializationInput/EvolutionModePicker"; +import RunEstimateCard from "./specializationInput/RunEstimateCard"; +import SeedPicker from "./specializationInput/SeedPicker"; +import SourceModePicker from "./specializationInput/SourceModePicker"; +import { estimateCost } from "./specializationInput/estimateCost"; +import { startEvolution } from "./specializationInput/startEvolution"; +import type { + EvolutionMode, + GeneratedPackage, + SeedSummary, + SourceMode, + UploadResponse, +} from "./specializationInput/types"; +import { DIFFICULTY_COLOR } from "./specializationInput/types"; +/** + * "Start an Evolution Run" form. + * + * Orchestrates three source modes (from scratch / upload / fork registry), + * four parameter knobs, and the run-estimate footer. The fetch layer, form + * sub-views, and pure estimators live in ``specializationInput/*`` so this + * file stays focused on state composition and the submit flow. + */ export default function SpecializationInput() { const navigate = useNavigate(); const [searchParams] = useSearchParams(); @@ -62,147 +40,58 @@ export default function SpecializationInput() { const [populationSize, setPopulationSize] = useState(5); const [numGenerations, setNumGenerations] = useState(3); const [budget, setBudget] = useState(10); - // v2.0 — Auto lets the Taxonomist decide; Atomic and Classic force the mode. - const [evolutionMode, setEvolutionMode] = useState<"auto" | "atomic" | "molecular">("auto"); + const [evolutionMode, setEvolutionMode] = useState("auto"); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); const [upload, setUpload] = useState(null); const [forkedSeed, setForkedSeed] = useState(null); - const [allSeeds, setAllSeeds] = useState(null); - const [seedCategoryFilter, setSeedCategoryFilter] = useState("all"); const [inviteCode, setInviteCode] = useState(null); - const [generatedPackage, setGeneratedPackage] = useState<{ - skillMdContent: string; - supportingFiles: Record; - specialization: string; - } | null>(null); + const [generatedPackage, setGeneratedPackage] = useState(null); - const handleValidated = useCallback((code: string) => { - setInviteCode(code); - }, []); + const { data: allSeeds = null } = useSeeds(); - // Fetch all seeds once — used by both the ?seed= auto-select path - // AND the inline picker grid when fork mode is active. + // Auto-select the seed named in ?seed= as soon as the library loads. useEffect(() => { - fetch("/api/seeds") - .then((r) => r.json() as Promise) - .then((seeds) => { - setAllSeeds(seeds); - if (seedParam) { - const match = seeds.find((s) => s.id === seedParam); - if (match) { - setForkedSeed(match); - } - } - }) - .catch(() => setAllSeeds([])); - }, [seedParam]); + if (!seedParam || !allSeeds) return; + const match = allSeeds.find((s) => s.id === seedParam); + if (match) setForkedSeed(match); + }, [seedParam, allSeeds]); - const seedCategories = allSeeds - ? ["all", ...Array.from(new Set(allSeeds.map((s) => s.category)))] - : ["all"]; + const handleValidated = useCallback((code: string) => { + setInviteCode(code); + }, []); - const visibleSeeds = - allSeeds?.filter((s) => seedCategoryFilter === "all" || s.category === seedCategoryFilter) ?? - []; + const estimate = useMemo( + () => estimateCost({ populationSize, numGenerations }), + [populationSize, numGenerations], + ); const submit = async () => { setSubmitting(true); setError(null); try { - let res: Response; - if (sourceMode === "scratch") { - if (!specialization.trim() && !generatedPackage) { - throw new Error("Specialization is required"); - } - if (generatedPackage) { - // Use the AI-generated skill package as parent - res = await fetch("/api/evolve/from-parent", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - parent_source: "generated", - skill_md_content: generatedPackage.skillMdContent, - supporting_files: generatedPackage.supportingFiles, - specialization: generatedPackage.specialization || specialization, - population_size: populationSize, - num_generations: numGenerations, - max_budget_usd: budget, - invite_code: inviteCode ?? undefined, - }), - }); - } else { - const body: EvolveRequest & { - invite_code?: string; - evolution_mode?: string; - } = { - mode: "domain", - specialization, - population_size: populationSize, - num_generations: numGenerations, - max_budget_usd: budget, - invite_code: inviteCode ?? undefined, - // "auto" maps to undefined so the Taxonomist decides server-side - evolution_mode: evolutionMode === "auto" ? undefined : evolutionMode, - }; - res = await fetch("/api/evolve", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(body), - }); - } - } else if (sourceMode === "upload") { - if (!upload?.upload_id) { - throw new Error("Upload a valid SKILL.md or zip first"); - } - res = await fetch("/api/evolve/from-parent", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - parent_source: "upload", - parent_id: upload.upload_id, - specialization: specialization || undefined, - population_size: populationSize, - num_generations: numGenerations, - max_budget_usd: budget, - invite_code: inviteCode ?? undefined, - }), - }); - } else { - // fork - if (!forkedSeed) { - throw new Error("No seed selected to fork"); - } - res = await fetch("/api/evolve/from-parent", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - parent_source: "registry", - parent_id: forkedSeed.id, - specialization: specialization || undefined, - population_size: populationSize, - num_generations: numGenerations, - max_budget_usd: budget, - invite_code: inviteCode ?? undefined, - }), - }); - } - - if (!res.ok) { - const text = await res.text(); - throw new Error(`HTTP ${res.status}: ${text}`); - } - const data = (await res.json()) as EvolveResponse; - navigate(`/runs/${data.run_id}`); + const runId = await startEvolution({ + sourceMode, + specialization, + populationSize, + numGenerations, + budget, + evolutionMode, + inviteCode, + upload, + forkedSeedId: forkedSeed?.id ?? null, + generatedPackage, + }); + navigate(`/runs/${runId}`); } catch (err) { setError(err instanceof Error ? err.message : String(err)); setSubmitting(false); } }; - // Invite-gated: if no code has been validated yet, render the gate instead - // of the form. The gate handles its own "already validated" fast-path via - // localStorage, so returning users with a saved code see the form directly. + // Invite gate: no code yet => show gate. The gate handles its own + // "already validated" fast-path via localStorage, so returning users + // with a saved code see the form directly. if (inviteCode === null) { return ; } @@ -214,251 +103,52 @@ export default function SpecializationInput() {

Start an Evolution Run

- {/* Source mode toggle */}
-

- Starting Point -

-
- {SOURCE_MODES.map((m) => { - const selected = sourceMode === m.value; - return ( - - ); - })} -
+
- {/* Source-specific body */} {sourceMode === "scratch" && ( -
-

- Specialization Blueprint -

-