|
| 1 | +// Renders a reaction rule's scheme -- reactant(s) -> product(s) -- as an SVG |
| 2 | +// rendered server-side by RDKit (see fetchReactionSchemeSvg / routes/rules.py's |
| 3 | +// reaction_scheme_svg). This replaced an earlier client-side attempt built on |
| 4 | +// smiles-drawer's ReactionDrawer/parseReaction: that API parses with the same |
| 5 | +// strict, SMILES-only grammar `SmilesDrawer.parse` uses, so a single SMARTS-only |
| 6 | +// token anywhere in the reaction (an OR-list like "[C,c]", a boolean primitive like |
| 7 | +// "[O&D2]"/"[C;!R]") aborted the whole parse -- most of this ruleset's reaction |
| 8 | +// rules use at least one such token. RDKit's own reaction drawer understands full |
| 9 | +// SMARTS query syntax directly, so every rule renders, not just the plain-enough |
| 10 | +// handful a SMILES grammar could parse. |
| 11 | +import React from "react"; |
| 12 | +import Alert from "@mui/material/Alert"; |
| 13 | +import Box from "@mui/material/Box"; |
| 14 | +import CircularProgress from "@mui/material/CircularProgress"; |
| 15 | +import DOMPurify from "dompurify"; |
| 16 | +import { useColorScheme } from "@mui/material/styles"; |
| 17 | +import { useQuery } from "@tanstack/react-query"; |
| 18 | +import { fetchReactionSchemeSvg } from "../../features/rules/api"; |
| 19 | + |
| 20 | +export function ReactionScheme({ id, smarts }: { id: string; smarts: string }) { |
| 21 | + const { mode, systemMode } = useColorScheme(); |
| 22 | + const theme = (systemMode !== undefined ? systemMode : mode) === "dark" ? "dark" : "light"; |
| 23 | + |
| 24 | + const svgQuery = useQuery({ |
| 25 | + queryKey: ["reactionSchemeSvg", id, theme], |
| 26 | + queryFn: ({ signal }) => fetchReactionSchemeSvg(id, theme, signal), |
| 27 | + staleTime: Infinity, |
| 28 | + gcTime: Infinity, |
| 29 | + }); |
| 30 | + |
| 31 | + // Server-rendered SVG markup gets injected raw via dangerouslySetInnerHTML, so it |
| 32 | + // must be sanitized first -- same rationale/profile as SvgViewer. |
| 33 | + const sanitizedSvg = React.useMemo( |
| 34 | + () => (svgQuery.data ? DOMPurify.sanitize(svgQuery.data, { USE_PROFILES: { svg: true, svgFilters: true } }) : null), |
| 35 | + [svgQuery.data] |
| 36 | + ); |
| 37 | + |
| 38 | + if (svgQuery.isLoading) { |
| 39 | + return ( |
| 40 | + <Box sx={{ display: "flex", alignItems: "center", justifyContent: "center", height: 100 }}> |
| 41 | + <CircularProgress size={20} /> |
| 42 | + </Box> |
| 43 | + ); |
| 44 | + } |
| 45 | + |
| 46 | + if (svgQuery.error || !sanitizedSvg) { |
| 47 | + return ( |
| 48 | + <Alert severity="warning" variant="outlined" sx={{ py: 0 }}> |
| 49 | + Could not render this reaction ({smarts}). |
| 50 | + </Alert> |
| 51 | + ); |
| 52 | + } |
| 53 | + |
| 54 | + return ( |
| 55 | + <Box |
| 56 | + sx={{ "& svg": { display: "block", maxWidth: "100%", height: "auto" } }} |
| 57 | + dangerouslySetInnerHTML={{ __html: sanitizedSvg }} |
| 58 | + /> |
| 59 | + ); |
| 60 | +} |
0 commit comments