Spaces:
Sleeping
Sleeping
| "use client"; | |
| import { useId, useRef, useState } from "react"; | |
| import { ParamKey } from "./paramHelpContent"; | |
| import { useParamHelp } from "./ParamHelpProvider"; | |
| /** | |
| * "How these parameters relate" — a collapsed disclosure with a clean | |
| * HTML/flex pipeline diagram inside. Presentation only. | |
| * | |
| * Centered single-column layout, max-width 460px, width 100% — never | |
| * wider than the panel, never overflowing. Each step is a neutral box; | |
| * between consecutive boxes sits a coloured "pill" naming the | |
| * parameter(s) that govern that transition, with a calm ↓ between | |
| * pill and the next box. | |
| */ | |
| export default function ParameterFlow() { | |
| const [open, setOpen] = useState(false); | |
| const panelId = useId(); | |
| return ( | |
| <div className="mb-4 -mt-2 text-xs"> | |
| <button | |
| type="button" | |
| onClick={() => setOpen((o) => !o)} | |
| aria-expanded={open} | |
| aria-controls={panelId} | |
| className="inline-flex items-center gap-1.5 rounded text-ink hover:text-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent" | |
| > | |
| <span | |
| aria-hidden | |
| style={{ | |
| display: "inline-block", | |
| transition: "transform 120ms ease", | |
| transform: open ? "rotate(90deg)" : "rotate(0deg)", | |
| }} | |
| > | |
| ▸ | |
| </span> | |
| <span className="font-medium">How these parameters relate</span> | |
| </button> | |
| {open && ( | |
| <div id={panelId} className="mt-3 w-full"> | |
| <Pipeline /> | |
| <Legend /> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| // ---------- diagram model ------------------------------------------------ | |
| type PillKind = "SPACE" | "EFFORT" | "VALIDATION"; | |
| interface Box { | |
| title: string; | |
| sub?: string; | |
| amber?: boolean; | |
| } | |
| /** A pill is a list of inline fragments: either a plain string or a | |
| * {bold} fragment so we can highlight just the parameter name. The | |
| * bold fragments carry an optional ``paramKey`` — when present, that | |
| * fragment is rendered as a button that opens the ParamHelp modal. */ | |
| type Frag = string | { bold: string; paramKey?: ParamKey }; | |
| interface Pill { | |
| kind: PillKind; | |
| /** Optional small text prefix (e.g. "↻ " for the loop pill). */ | |
| prefix?: string; | |
| fragments: Frag[]; | |
| } | |
| const PILL_STYLES: Record<PillKind, { bg: string; fg: string }> = { | |
| SPACE: { bg: "#E7EFF1", fg: "#2C5563" }, | |
| EFFORT: { bg: "#F6ECE0", fg: "#8A4E20" }, | |
| VALIDATION: { bg: "#ECEBE6", fg: "#5A6670" }, | |
| }; | |
| const BOXES: Box[] = [ | |
| { title: "~20,000 genes", sub: "every gene measured" }, | |
| { title: "Gene pool — N genes", sub: "the shortlist the engine draws from" }, | |
| { title: "One program", sub: "Select → Reduce → Fit" }, | |
| { title: "One generation", sub: "every program scored & ranked" }, | |
| { title: "Winning program" }, | |
| { title: "permutation p", sub: "could this be luck?", amber: true }, | |
| ]; | |
| const PILLS: Pill[] = [ | |
| { | |
| kind: "SPACE", | |
| fragments: [ | |
| { bold: "Prefilter top-N", paramKey: "prefilter_n" }, | |
| " — keep the N most promising genes (off by default = all ~20,000)", | |
| ], | |
| }, | |
| { | |
| kind: "SPACE", | |
| fragments: [ | |
| { bold: "Max sets", paramKey: "max_sets" }, | |
| " × ", | |
| { bold: "Genes/set", paramKey: "genes_per_set" }, | |
| " — pick 1–2 sets, each ≤ G genes", | |
| ], | |
| }, | |
| { | |
| kind: "EFFORT", | |
| fragments: [ | |
| { bold: "Population", paramKey: "population" }, | |
| " — programs compete each round · ", | |
| { bold: "λ", paramKey: "lambda" }, | |
| " — taxes extra genes", | |
| ], | |
| }, | |
| { | |
| kind: "EFFORT", | |
| prefix: "↻ ", | |
| fragments: [ | |
| "× ", | |
| { bold: "Generations", paramKey: "generations" }, | |
| " — breed the best, repeat · ", | |
| { bold: "Seed", paramKey: "seed" }, | |
| " — fixes the randomness", | |
| ], | |
| }, | |
| { | |
| kind: "VALIDATION", | |
| fragments: [ | |
| { bold: "Permutations", paramKey: "permutations" }, | |
| " — re-run on N shuffled-label sets", | |
| ], | |
| }, | |
| ]; | |
| // ---------- render ------------------------------------------------------- | |
| function Pipeline() { | |
| return ( | |
| <div | |
| style={{ | |
| maxWidth: 460, | |
| width: "100%", | |
| marginInline: "auto", | |
| display: "flex", | |
| flexDirection: "column", | |
| alignItems: "stretch", | |
| gap: 8, | |
| }} | |
| > | |
| {BOXES.map((b, i) => ( | |
| <div key={`group-${i}`} style={{ display: "contents" }}> | |
| <BoxRow box={b} /> | |
| {i < PILLS.length && ( | |
| <> | |
| <PillRow pill={PILLS[i]} /> | |
| <Arrow /> | |
| </> | |
| )} | |
| </div> | |
| ))} | |
| </div> | |
| ); | |
| } | |
| function BoxRow({ box }: { box: Box }) { | |
| return ( | |
| <div | |
| style={{ | |
| width: "100%", | |
| background: box.amber ? "#FBF1E6" : "#F5F4F0", | |
| border: `1px solid ${box.amber ? "#BC6B2E" : "#D9D6CE"}`, | |
| borderRadius: 12, | |
| padding: "12px 14px", | |
| textAlign: "center", | |
| fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', | |
| lineHeight: 1.3, | |
| }} | |
| > | |
| <div | |
| style={{ | |
| fontSize: 13, | |
| fontWeight: 600, | |
| color: "#23303A", | |
| }} | |
| > | |
| {box.title} | |
| </div> | |
| {box.sub && ( | |
| <div | |
| style={{ | |
| marginTop: 2, | |
| fontSize: 11.5, | |
| color: "#6E7F8C", | |
| }} | |
| > | |
| {box.sub} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| function PillRow({ pill }: { pill: Pill }) { | |
| const { bg, fg } = PILL_STYLES[pill.kind]; | |
| return ( | |
| <div | |
| style={{ | |
| display: "flex", | |
| justifyContent: "center", | |
| width: "100%", | |
| }} | |
| > | |
| <div | |
| style={{ | |
| maxWidth: "92%", | |
| background: bg, | |
| color: fg, | |
| borderRadius: 14, | |
| padding: "6px 12px", | |
| fontSize: 12.5, | |
| lineHeight: 1.35, | |
| textAlign: "center", | |
| whiteSpace: "normal", | |
| overflowWrap: "break-word", | |
| wordBreak: "normal", | |
| fontFamily: '"Helvetica Neue", Helvetica, Arial, sans-serif', | |
| }} | |
| > | |
| {pill.prefix && ( | |
| <span style={{ marginRight: 2 }} aria-hidden> | |
| {pill.prefix} | |
| </span> | |
| )} | |
| {pill.fragments.map((f, i) => { | |
| if (typeof f === "string") return <span key={i}>{f}</span>; | |
| if (!f.paramKey) { | |
| return ( | |
| <strong key={i} style={{ fontWeight: 600 }}> | |
| {f.bold} | |
| </strong> | |
| ); | |
| } | |
| return <ParamPillButton key={i} bold={f.bold} paramKey={f.paramKey} colour={fg} />; | |
| })} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| function ParamPillButton({ | |
| bold, | |
| paramKey, | |
| colour, | |
| }: { | |
| bold: string; | |
| paramKey: ParamKey; | |
| colour: string; | |
| }) { | |
| const ref = useRef<HTMLButtonElement>(null); | |
| const { open } = useParamHelp(); | |
| return ( | |
| <button | |
| ref={ref} | |
| type="button" | |
| aria-haspopup="dialog" | |
| aria-label={`About ${bold}`} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| e.preventDefault(); | |
| open(paramKey, ref.current); | |
| }} | |
| style={{ | |
| background: "transparent", | |
| border: "none", | |
| padding: 0, | |
| font: "inherit", | |
| color: colour, | |
| fontWeight: 600, | |
| textDecoration: "underline", | |
| textDecorationStyle: "dotted", | |
| textDecorationColor: `${colour}66`, | |
| textUnderlineOffset: 2, | |
| cursor: "pointer", | |
| }} | |
| onMouseOver={(e) => { | |
| (e.currentTarget.style as any).textDecorationStyle = "solid"; | |
| }} | |
| onMouseOut={(e) => { | |
| (e.currentTarget.style as any).textDecorationStyle = "dotted"; | |
| }} | |
| > | |
| {bold} | |
| </button> | |
| ); | |
| } | |
| function Arrow() { | |
| return ( | |
| <div | |
| aria-hidden | |
| style={{ | |
| textAlign: "center", | |
| color: "#B9B6AE", | |
| fontSize: 16, | |
| lineHeight: 1, | |
| userSelect: "none", | |
| }} | |
| > | |
| ↓ | |
| </div> | |
| ); | |
| } | |
| function Legend() { | |
| return ( | |
| <div | |
| style={{ | |
| maxWidth: 460, | |
| width: "100%", | |
| marginInline: "auto", | |
| marginTop: 12, | |
| display: "flex", | |
| flexWrap: "wrap", | |
| justifyContent: "center", | |
| gap: "6px 14px", | |
| fontSize: 11, | |
| color: "#6E7F8C", | |
| }} | |
| > | |
| <LegendItem fill="#E7EFF1" ink="#2C5563"> | |
| teal = where it searches | |
| </LegendItem> | |
| <LegendItem fill="#F6ECE0" ink="#8A4E20"> | |
| amber = how hard it searches | |
| </LegendItem> | |
| <LegendItem fill="#ECEBE6" ink="#5A6670"> | |
| grey = validation | |
| </LegendItem> | |
| </div> | |
| ); | |
| } | |
| function LegendItem({ | |
| fill, | |
| ink, | |
| children, | |
| }: { | |
| fill: string; | |
| ink: string; | |
| children: React.ReactNode; | |
| }) { | |
| return ( | |
| <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}> | |
| <span | |
| aria-hidden | |
| style={{ | |
| display: "inline-block", | |
| width: 10, | |
| height: 10, | |
| borderRadius: 999, | |
| background: fill, | |
| border: `1px solid ${ink}33`, | |
| }} | |
| /> | |
| {children} | |
| </span> | |
| ); | |
| } | |