frontend: Rugnux live stage - steps tagged in a few letters, one colour per kind
The step segments on rugnux's bar and the chips under it carry a tag of a few letters for the kind of step (Pre, Spots, Idx, Int, Scale, MR, Ref, Out, ...), from a small table on the step's title - a title the table does not know gives its first word - and the full title on hover; no counts, no pass numbers. Every kind has a colour of its own (Okabe-Ito, the blue lightened for the dark stage; a kind outside the table takes the next spare one), the same on every pass and on its chip; consecutive passes of one kind are one chip with their time together and, on the bar, segments parted by a thin divider with the tag on the first. The centre panel shows only the step rugnux is on while there is no model; the steps behind it are on the strip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HVeDQsU5a8AJUdJPgWevAK
This commit is contained in:
1 parent
9230543395
commit
bae1cc3b7b
2 files changed
+82
-47
No files matched your search
@@ -3,7 +3,7 @@ import {green, indigo, lime, orange, red, grey} from "@mui/material/colors";
|
||||
import {keyframes} from "@mui/system";
|
||||
import {useQuery} from "@tanstack/react-query";
|
||||
import {getConfigRugnuxOptions, getRugnuxStatusOptions} from "../client/@tanstack/react-query.gen";
|
||||
import {rugnux_live_status, rugnux_live_step} from "../client";
|
||||
import {rugnux_live_status} from "../client";
|
||||
import {useTokenRequired} from "../auth";
|
||||
import EmptyState from "./EmptyState";
|
||||
import RugnuxTriage from "./RugnuxTriage";
|
||||
@@ -16,7 +16,6 @@ import RugnuxProgress from "./RugnuxProgress";
|
||||
import {lastImageTime, ModelScores, StepTable, VerdictAlert} from "./RugnuxParts";
|
||||
import BoltIcon from "@mui/icons-material/Bolt";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import {lazy, Suspense, useEffect, useRef, useState} from "react";
|
||||
|
||||
// The live page as one control-room screen: a dark stage the height of the window, edged in the colour of the
|
||||
@@ -110,34 +109,21 @@ function Later({text}: { text: string }) {
|
||||
return <Typography sx={{color: "text.disabled", m: "auto", textAlign: "center", px: 2}}>{text}</Typography>;
|
||||
}
|
||||
|
||||
// The run's steps as a console - the running one large with its progress, the ones behind it as a list with
|
||||
// their durations - filling the centre until there is a model to show. Their notes are in the Details fold.
|
||||
// The step rugnux is on, large with its progress, filling the centre until there is a model to show; the
|
||||
// steps behind it are on the strip under the bars.
|
||||
function RunConsole({s}: { s: rugnux_live_status }) {
|
||||
const running = s.state === "Running";
|
||||
const current = running && s.steps.length > 0 && s.steps[s.steps.length - 1].duration_s === undefined
|
||||
? s.steps[s.steps.length - 1] : undefined;
|
||||
const done = current ? s.steps.slice(0, -1) : s.steps;
|
||||
const row = (step: rugnux_live_step, i: number) => (
|
||||
<Box key={i} sx={{display: "grid", gridTemplateColumns: "24px 1fr auto", alignItems: "center", gap: 1.5, py: 0.6,
|
||||
borderBottom: "1px solid", borderColor: "divider"}}>
|
||||
<CheckIcon sx={{fontSize: 18, color: step.duration_s === undefined ? "text.disabled" : green[400]}}/>
|
||||
<Typography noWrap sx={{fontWeight: 600, minWidth: 0}}>{step.title}</Typography>
|
||||
<Typography sx={{fontVariantNumeric: "tabular-nums", color: "text.secondary", whiteSpace: "nowrap"}}>
|
||||
{step.duration_s !== undefined ? `${step.duration_s.toFixed(1)} s` : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
if (!current)
|
||||
return <Later text={running ? "waiting for images" : s.state === "Failed" ? "failed" : "no model"}/>;
|
||||
return (
|
||||
<Box sx={{overflowY: "auto", minHeight: 0, display: "flex", flexDirection: "column", gap: 2}}>
|
||||
{current && <Box sx={{py: 2}}>
|
||||
<Typography sx={{...LABEL_SX, color: indigo[300]}}>now</Typography>
|
||||
<Typography sx={{fontSize: "2rem", fontWeight: 800, lineHeight: 1.1, animation: `${pulse} 2.4s ease-in-out infinite`,
|
||||
"@media (prefers-reduced-motion: reduce)": {animation: "none"}}}>
|
||||
{current.title}{s.step_progress !== undefined && <Box component="span" sx={{color: indigo[300], ml: 2}}>{(100 * s.step_progress).toFixed(0)} %</Box>}
|
||||
</Typography>
|
||||
</Box>}
|
||||
{s.steps.length === 0 && <Later text={running ? "Rugnux starts on the first images" : "No steps ran"}/>}
|
||||
<Box>{done.map(row)}</Box>
|
||||
<Box sx={{py: 2}}>
|
||||
<Typography sx={{...LABEL_SX, color: indigo[300]}}>now</Typography>
|
||||
<Typography sx={{fontSize: "2rem", fontWeight: 800, lineHeight: 1.1, animation: `${pulse} 2.4s ease-in-out infinite`,
|
||||
"@media (prefers-reduced-motion: reduce)": {animation: "none"}}}>
|
||||
{current.title}{s.step_progress !== undefined && <Box component="span" sx={{color: indigo[300], ml: 2}}>{(100 * s.step_progress).toFixed(0)} %</Box>}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {Box, Tooltip, Typography} from "@mui/material";
|
||||
import {keyframes} from "@mui/system";
|
||||
import {deepOrange, grey, indigo, lightBlue, lime} from "@mui/material/colors";
|
||||
import {rugnux_live_status} from "../client";
|
||||
import {deepOrange, grey, indigo, lime} from "@mui/material/colors";
|
||||
import {rugnux_live_status, rugnux_live_step} from "../client";
|
||||
import {lastImageTime} from "./RugnuxParts";
|
||||
|
||||
// The detector's and rugnux's progress one under the other on one time axis (seconds since the first image):
|
||||
// the detector's bar fills as images arrive, up to the collection's expected end; rugnux's bar is its steps,
|
||||
// one segment each, as wide as the step took - the running one growing to "now" - so what takes how long is
|
||||
// read off the bar, and the strip under the bars names every step with its duration. The last image and the
|
||||
// deadline are lines across both bars.
|
||||
// read off the bar. Each kind of step has a tag of a few letters and a colour of its own, the same on every
|
||||
// pass and on the strip under the bars that gives each kind its time; the full titles are on hover. The last
|
||||
// image and the deadline are lines across both bars.
|
||||
|
||||
const stripes = keyframes`
|
||||
from { background-position: 0 0; }
|
||||
@@ -28,7 +29,38 @@ const runningSx = {
|
||||
"@media (prefers-reduced-motion: reduce)": {animation: "none"},
|
||||
};
|
||||
|
||||
const STEP_COLOURS = [indigo[400], lightBlue[700], indigo[600], lightBlue[900], indigo[300], lightBlue[500]];
|
||||
// The step's tag from its title; a title no row knows is its first word.
|
||||
const TAGS: [RegExp, string][] = [
|
||||
[/^pre-?(scan|pass)/i, "Pre"], [/spot/i, "Spots"], [/index/i, "Idx"], [/integrat/i, "Int"], [/gpu analysis/i, "GPU"],
|
||||
[/scaling/i, "Scale"], [/^merge/i, "Merge"], [/molecular replacement/i, "MR"], [/geometry refinement/i, "Geom"],
|
||||
[/refinement/i, "Ref"], [/validation/i, "Val"], [/analys/i, "Chk"], [/writ/i, "Out"], [/distance/i, "Dist"],
|
||||
[/doubled-cell/i, "Cell"], [/calibration/i, "Cal"], [/reading/i, "Read"],
|
||||
];
|
||||
const tagOf = (title: string) => TAGS.find(([re]) => re.test(title))?.[1] ?? title.split(/[\s,]/)[0].slice(0, 6);
|
||||
|
||||
// One colour per kind of step - Okabe-Ito, which keeps its kinds apart for colour-blind readers too, with
|
||||
// its blue lightened for the dark stage; a kind not in the table takes the next free one.
|
||||
const KIND_COLOURS: Record<string, string> = {
|
||||
Pre: "#56B4E9", Spots: "#E69F00", Idx: "#F0E442", Int: "#3C8DD6", GPU: "#3C8DD6", Scale: "#009E73",
|
||||
Merge: "#009E73", MR: "#CC79A7", Ref: "#D55E00", Out: "#9E9E9E",
|
||||
};
|
||||
const SPARE_COLOURS = ["#8DD3C7", "#BEBADA", "#FB8072", "#FDB462", "#B3DE69"];
|
||||
function kindColours(steps: rugnux_live_step[]) {
|
||||
const colours: Record<string, string> = {...KIND_COLOURS};
|
||||
let spare = 0;
|
||||
for (const st of steps) {
|
||||
const tag = tagOf(st.title);
|
||||
if (!(tag in colours))
|
||||
colours[tag] = SPARE_COLOURS[spare++ % SPARE_COLOURS.length];
|
||||
}
|
||||
return colours;
|
||||
}
|
||||
// dark text on the light colours
|
||||
const inkOn = (hex: string) => {
|
||||
const [r, g, b] = [1, 3, 5].map(i => parseInt(hex.slice(i, i + 2), 16));
|
||||
return 0.299 * r + 0.587 * g + 0.114 * b > 150 ? "rgba(0,0,0,.85)" : "#fff";
|
||||
};
|
||||
|
||||
const BAR = 20;
|
||||
const GAP = 6;
|
||||
const AXIS = 16;
|
||||
@@ -85,6 +117,21 @@ function RugnuxProgress({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
: s.cut ? `cut (${s.cut})` : `${s.steps.length} steps · ${seconds(s.elapsed_s)}`;
|
||||
|
||||
const stepEnd = (i: number) => s.steps[i].duration_s === undefined ? s.elapsed_s : s.steps[i].started_s + s.steps[i].duration_s!;
|
||||
const colours = kindColours(s.steps);
|
||||
const tags = s.steps.map(st => tagOf(st.title));
|
||||
// consecutive steps of one kind (the passes) as one chip, with their time together
|
||||
const kinds: { tag: string, titles: string[], seconds: number, open: boolean }[] = [];
|
||||
s.steps.forEach((st, i) => {
|
||||
const last = kinds[kinds.length - 1];
|
||||
const open = st.duration_s === undefined;
|
||||
const duration = open ? s.elapsed_s - st.started_s : st.duration_s!;
|
||||
if (last && last.tag === tags[i]) {
|
||||
last.titles.push(st.title);
|
||||
last.seconds += duration;
|
||||
last.open = open;
|
||||
} else
|
||||
kinds.push({tag: tags[i], titles: [st.title], seconds: duration, open});
|
||||
});
|
||||
|
||||
return (
|
||||
<Box sx={{display: "grid", gridTemplateColumns: "72px minmax(0, 1fr) auto", columnGap: 2, alignItems: "start"}}>
|
||||
@@ -107,15 +154,18 @@ function RugnuxProgress({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
{s.steps.map((st, i) => {
|
||||
const open = st.duration_s === undefined;
|
||||
const width = Math.max(pct(stepEnd(i)) - pct(st.started_s), 0.3);
|
||||
const showTitle = width / 100 * laneWidth >= Math.min(st.title.length * 6.5 + 10, 60);
|
||||
const colour = colours[tags[i]];
|
||||
// a pass after a pass of the same kind: a thin divider, the tag only on the first
|
||||
const samePass = i > 0 && tags[i - 1] === tags[i];
|
||||
const showTag = !samePass && width / 100 * laneWidth >= tags[i].length * 8 + 14;
|
||||
return (
|
||||
<Tooltip key={i} arrow title={`${st.title} · ${open ? "running" : seconds(st.duration_s!)}`}>
|
||||
<Box sx={{position: "absolute", left: `${pct(st.started_s)}%`, width: `${width}%`, top: 0, bottom: 0,
|
||||
bgcolor: STEP_COLOURS[i % STEP_COLOURS.length], borderRight: "1px solid rgba(0,0,0,.5)",
|
||||
bgcolor: colour, borderLeft: samePass ? "1px solid rgba(0,0,0,.35)" : "1px solid rgba(0,0,0,.7)",
|
||||
display: "flex", alignItems: "center", px: 0.5, overflow: "hidden",
|
||||
...(open && running && runningSx)}}>
|
||||
{showTitle && <Typography noWrap sx={{fontSize: "0.72rem", fontWeight: 600, color: "#fff", position: "relative", zIndex: 1}}>
|
||||
{st.title}
|
||||
{showTag && <Typography noWrap sx={{fontSize: "0.72rem", fontWeight: 700, color: inkOn(colour), position: "relative", zIndex: 1}}>
|
||||
{tags[i]}
|
||||
</Typography>}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
@@ -154,25 +204,24 @@ function RugnuxProgress({s, deadline_s}: { s: rugnux_live_status, deadline_s: nu
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* the steps, each with its duration */}
|
||||
{s.steps.length > 0 &&
|
||||
{/* the kinds of step, each with its time */}
|
||||
{kinds.length > 0 &&
|
||||
<Box sx={{gridColumn: "2 / 4", display: "flex", flexWrap: "wrap", gap: 0.75, mt: 0.25}}>
|
||||
{s.steps.map((st, i) => {
|
||||
const open = st.duration_s === undefined;
|
||||
return (
|
||||
<Box key={i} sx={{display: "inline-flex", alignItems: "center", gap: 0.75, px: 1, height: 22, borderRadius: "11px",
|
||||
{kinds.map((k, i) => (
|
||||
<Tooltip key={i} arrow title={k.titles.join(" · ")}>
|
||||
<Box sx={{display: "inline-flex", alignItems: "center", gap: 0.75, px: 1, height: 22, borderRadius: "11px",
|
||||
bgcolor: "rgba(255,255,255,.06)", fontSize: "0.75rem", whiteSpace: "nowrap",
|
||||
...(open && running && {animation: `${pulse} 1.6s ease-in-out infinite`,
|
||||
...(k.open && running && {animation: `${pulse} 1.6s ease-in-out infinite`,
|
||||
"@media (prefers-reduced-motion: reduce)": {animation: "none"}})}}>
|
||||
<Box sx={{width: 8, height: 8, borderRadius: "2px", bgcolor: STEP_COLOURS[i % STEP_COLOURS.length]}}/>
|
||||
<Box component="span" sx={{fontWeight: 600}}>{st.title}</Box>
|
||||
<Box sx={{width: 8, height: 8, borderRadius: "2px", bgcolor: colours[k.tag]}}/>
|
||||
<Box component="span" sx={{fontWeight: 700}}>{k.tag}</Box>
|
||||
<Box component="span" sx={{color: "text.secondary", fontVariantNumeric: "tabular-nums"}}>
|
||||
{!open ? seconds(st.duration_s!)
|
||||
: running && i === s.steps.length - 1 && s.step_progress !== undefined ? `${(100 * s.step_progress).toFixed(0)} %` : ""}
|
||||
{k.open && running && i === kinds.length - 1 && s.step_progress !== undefined
|
||||
? `${(100 * s.step_progress).toFixed(0)} %` : seconds(k.seconds)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Tooltip>
|
||||
))}
|
||||
</Box>}
|
||||
</Box>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user