Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
65ccaaf
ENH: bump major version
davidmeijer Aug 7, 2026
f5c458d
DEL: remove uninformative stats on dashboard
davidmeijer Aug 7, 2026
e1b7c33
UPD: new query defaults
davidmeijer Aug 7, 2026
9aeb5aa
ADD: tooltip info to coverage scores
davidmeijer Aug 7, 2026
c03da7a
ENH: edit name of workspace item
davidmeijer Aug 7, 2026
c0c6d39
UPD: tooltips for workspace items
davidmeijer Aug 7, 2026
3d97a2c
FIX: add dev suffix to version for now
davidmeijer Aug 7, 2026
e9e7970
STY: update styling and user experience primary sequence editing
davidmeijer Aug 7, 2026
f61388c
STY: make dialog windows have lighter backgrounds
davidmeijer Aug 7, 2026
39caa26
STY: add some padding at bottom import gene cluster dialog
davidmeijer Aug 7, 2026
465846a
ENH: improve visualization RetroMol results in compound view
davidmeijer Aug 8, 2026
505fbe1
ENH: download compound view as png
davidmeijer Aug 8, 2026
df58a9f
ENH: server up time on homepage
davidmeijer Aug 8, 2026
26ca7b6
FIX: bring package-lock.json up to date
davidmeijer Aug 8, 2026
ad3b21a
STY: change button appearances compound view
davidmeijer Aug 8, 2026
c98be70
STY: make download buttons look similar across different tabs
davidmeijer Aug 8, 2026
8b27835
WIP: styling discovery tab
davidmeijer Aug 8, 2026
2a3b257
STY: update user experience query settings
davidmeijer Aug 8, 2026
e4555db
UPD: query results list user experience
davidmeijer Aug 9, 2026
f0a9cb4
UPD: description query window
davidmeijer Aug 9, 2026
7f1c7ce
ENH: add motif hover cards
davidmeijer Aug 9, 2026
04b46f0
STY: change styling result dialog
davidmeijer Aug 9, 2026
c302327
STY: write out fp in alignmen grid
davidmeijer Aug 9, 2026
3d1d739
FIX: remove unused import
davidmeijer Aug 9, 2026
779ebb7
STY: update styling and user experience of result dialog
davidmeijer Aug 9, 2026
14532ec
FIX: removed unused import
davidmeijer Aug 9, 2026
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
47 changes: 27 additions & 20 deletions gui/src/client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions gui/src/client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "retromol-gui",
"version": "0.1.0",
"version": "1.0.0-dev",
"private": true,
"dependencies": {
"@dnd-kit/core": "^6.3.1",
Expand All @@ -19,6 +19,7 @@
"dayjs": "^1.11.19",
"dompurify": "^3.2.4",
"framer-motion": "^12.6.3",
"html-to-image": "^1.11.13",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-router-dom": "^7.9.5",
Expand Down Expand Up @@ -63,4 +64,4 @@
]
},
"proxy": "http://localhost:4000"
}
}
48 changes: 48 additions & 0 deletions gui/src/client/src/components/ExportImageButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React from "react";
import CircularProgress from "@mui/material/CircularProgress";
import DownloadIcon from "@mui/icons-material/Download";
import { useNotifications } from "./NotificationProvider";
import { exportElementAsPng } from "./exportImage";
import Button from "@mui/material/Button";

// Small "export this view as a PNG" affordance. Captures whatever is currently
// rendered inside `targetRef`, including any live highlight state, exactly as
// shown on screen.
export function ExportImageButton({
targetRef,
filename,
label = "Download this view as a PNG",
}: {
targetRef: React.RefObject<HTMLElement>;
filename: string;
label?: string;
}) {
const { pushNotification } = useNotifications();
const [exporting, setExporting] = React.useState(false);

const handleExport = async () => {
const node = targetRef.current;
if (!node) return;

setExporting(true);
try {
await exportElementAsPng(node, filename);
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
pushNotification(`Failed to export image: ${msg}`, "error");
} finally {
setExporting(false);
}
};

return (
<Button
size="small"
variant="text"
startIcon={exporting ? <CircularProgress size={18} /> : <DownloadIcon fontSize="small" />}
onClick={handleExport}
>
Download PNG
</Button>
);
}
2 changes: 2 additions & 0 deletions gui/src/client/src/components/Hero.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Box from "@mui/material/Box";
import Container from "@mui/material/Container";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { ServerUptime } from "./ServerUptime";

export default function Hero() {
return (
Expand Down Expand Up @@ -74,6 +75,7 @@ export default function Hero() {
Perform cross-modal retrieval between natural product compounds and
BGCs.
</Typography>
<ServerUptime />
</Stack>
</Container>
</Box>
Expand Down
43 changes: 43 additions & 0 deletions gui/src/client/src/components/MinimalIconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from "react";
import IconButton, { IconButtonProps } from "@mui/material/IconButton";

// A borderless, paddingless IconButton for inline "icon next to text" affordances
// (e.g. a rename pencil sitting right next to a label) where a normal IconButton's
// padding/hover halo would look out of place. Forwards its ref so it still works
// as the direct child of a Tooltip.
export const MinimalIconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
({ sx, size = "small", ...props }, ref) => (
<IconButton
ref={ref}
size={size}
sx={[
{
p: 0,
m: 0,
minWidth: 0,
minHeight: 0,
width: "auto",
height: "auto",
border: "none !important",
borderRadius: 0,
outline: "none",
background: "transparent !important",
boxShadow: "none !important",

"&:hover": {
background: "transparent !important",
},

"&:focus": {
background: "transparent !important",
outline: "none",
},
},
...(Array.isArray(sx) ? sx : [sx]),
]}
{...props}
/>
)
);

MinimalIconButton.displayName = "MinimalIconButton";
115 changes: 115 additions & 0 deletions gui/src/client/src/components/MotifHoverCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React from "react";
import Box from "@mui/material/Box";
import CircularProgress from "@mui/material/CircularProgress";
import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography";
import { useQuery } from "@tanstack/react-query";
import { fetchMotifStructures } from "../features/motifs/api";
import { MotifName } from "./MotifName";
import SmilesDrawerContainer from "./SmilesDrawerContainer.js";

const DRAWING_SIZE = 100;

// Content of the popup -- a separate component (rather than inlined in the
// Tooltip's title) so its useQuery only actually runs once the Tooltip mounts it,
// which MUI's Popper does lazily on open. queryKey is shared across every
// instance on the page, so the name -> SMILES map is fetched once and cached.
function MotifHoverContent({ name, hint }: { name: string; hint?: string }) {
const structuresQuery = useQuery({
queryKey: ["motifStructures"],
queryFn: ({ signal }) => fetchMotifStructures(signal),
staleTime: Infinity,
gcTime: Infinity,
});

const smiles = structuresQuery.data?.[name];

// Must be unique per *mounted instance*, not derived from `name` alone --
// SmilesDrawerContainer draws into a DOM node it looks up by this id, and the
// same motif name commonly appears in many cells at once (a whole column of
// an MSA, a run of identical residues). Two same-named hover cards can end up
// mounted at the same moment -- e.g. dragging the pointer straight down a
// repeated column, where the outgoing tooltip's exit transition briefly
// overlaps the incoming one's mount -- and a shared id means the lookup can
// resolve to the wrong (closing) node, leaving the one you're actually
// hovering empty until it flashes into view as the other one unmounts.
const reactId = React.useId();

return (
<Box sx={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 0.5, maxWidth: 200, py: 0.5 }}>
{smiles ? (
<SmilesDrawerContainer identifier={`motif-hover-${reactId}`} smiles={smiles} size={DRAWING_SIZE} />
) : (
<Box
sx={{
width: DRAWING_SIZE,
height: DRAWING_SIZE,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{structuresQuery.isLoading ? (
<CircularProgress size={20} />
) : (
<Typography variant="caption" color="text.secondary" sx={{ textAlign: "center" }}>
No structure available
</Typography>
)}
</Box>
)}
<Typography variant="caption" fontWeight={600}>
<MotifName name={name} />
</Typography>
{hint && (
<Typography variant="caption" color="text.secondary" sx={{ textAlign: "center" }}>
{hint}
</Typography>
)}
</Box>
);
}

// Wraps a motif chip/cell (primary sequence chips, the sequence editor's blocks,
// and the shared pairwise/MSA AlignmentGrid all render a bare motif name) so
// hovering it shows a small structure preview, its name, and optionally whatever
// contextual hint the caller already showed as a plain-text tooltip.
export function MotifHoverCard({
name,
hint,
children,
}: {
name: string;
hint?: string;
children: React.ReactElement;
}) {
return (
<Tooltip
title={<MotifHoverContent name={name} hint={hint} />}
arrow
enterDelay={400}
slotProps={{
tooltip: {
sx: {
bgcolor: "background.paper",
color: "text.primary",
border: "1px solid",
borderColor: "divider",
boxShadow: 3,
},
},
arrow: {
sx: {
color: "background.paper",
"&::before": {
border: "1px solid",
borderColor: "divider",
},
},
},
}}
>
{children}
</Tooltip>
);
}
Loading
Loading