Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
2e101af
fix(biochem): query only parent docs on the nested Solr schema
VibhavSetlur Aug 12, 2026
2663575
feat(biochem): expose Solr 9 nested thermodynamics records and atom m…
VibhavSetlur Aug 12, 2026
1ebeca7
feat(biochem): render all thermodynamics records and atom mappings on…
VibhavSetlur Aug 12, 2026
717823e
docs(changelog): note Solr 9 thermodynamics and atom mapping UI support
VibhavSetlur Aug 12, 2026
6fcd75e
feat(biochem): visualize atom mappings as a reactant-to-product flow …
VibhavSetlur Aug 19, 2026
0ac6f48
feat(biochem): report three-state reaction direction agreement from s…
VibhavSetlur Aug 19, 2026
104af90
docs(changelog): record three-state direction wording and the atom-fl…
VibhavSetlur Aug 19, 2026
e1ccbd9
feat(biochem): parse symmetry-grouped atom mappings
VibhavSetlur Aug 20, 2026
03e558b
fix(biochem): read the live atom_mapping_data Solr field with legacy …
VibhavSetlur Aug 20, 2026
28bd563
feat(biochem): state symmetry-equivalent atom groups in the mapping UI
VibhavSetlur Aug 20, 2026
b601971
docs(changelog): record symmetry-group atom mapping support
VibhavSetlur Aug 20, 2026
7e35408
feat(biochem): colour reaction atom mappings by element block
VibhavSetlur Aug 20, 2026
cf36596
feat(biochem): draw reactions on an open structure canvas
VibhavSetlur Aug 20, 2026
8766d08
refactor(biochem): wire mapping colours into the reaction page and dr…
VibhavSetlur Aug 20, 2026
8c259f3
feat(release): prepare 3.2.0 release
VibhavSetlur Aug 20, 2026
758bdb5
fix(biochem): tighten the reaction canvas after UI review
VibhavSetlur Aug 20, 2026
b514e5f
feat(biochem): colour merged atom-mapping groups instead of discardin…
VibhavSetlur Aug 20, 2026
37e115b
fix(biochem): draw every reaction participant that has a structure
VibhavSetlur Aug 20, 2026
732c73c
docs(changelog): describe the structure-first reaction canvas and mer…
VibhavSetlur Aug 20, 2026
968243f
feat(biochem): let researchers interrogate atom-mapping groups
VibhavSetlur Aug 21, 2026
30674a3
docs(biochem): document the atom-mapping data contract
VibhavSetlur Aug 21, 2026
e8269fd
feat(api): add a structures Solr client that serves raw InChI
VibhavSetlur Aug 21, 2026
ebc5084
feat(biochem): derive local atom identity from raw InChI canonical order
VibhavSetlur Aug 21, 2026
59857cc
feat(biochem): colour reaction structures from InChI-derived atom orbits
VibhavSetlur Aug 21, 2026
b97b8fa
feat(biochem): state atom-mapping precision honestly in the reaction …
VibhavSetlur Aug 21, 2026
cf4b5e0
fix(biochem): reject out-of-range mapping refs and unsafe structure ids
VibhavSetlur Aug 21, 2026
3eced37
docs(biochem): document InChI-derived atom mapping and release 3.3.0
VibhavSetlur Aug 21, 2026
3843d2c
feat(api): make Solr endpoints configurable per corpus
VibhavSetlur Aug 21, 2026
5f44957
feat(ui): streamline atom mapping colors and participant metadata
VibhavSetlur Aug 22, 2026
d6317fd
feat(ui): label every carbon in mapped reaction structures
VibhavSetlur Aug 24, 2026
927a026
fix(ui): show reaction atom mapping only in SVGs
VibhavSetlur Aug 25, 2026
46fd8b2
fix(a11y): use a color-blind-safe atom mapping palette
VibhavSetlur Aug 25, 2026
b58514d
docs(solr): document the full Solr env surface and endpoint switching…
VibhavSetlur Aug 26, 2026
5754243
chore: merge staging (3.1.1 security fix) into solr-upgrade
VibhavSetlur Aug 26, 2026
768b422
test(e2e): cover Solr 9 nested-schema and legacy reaction rendering
VibhavSetlur Aug 26, 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
41 changes: 35 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -118,23 +118,52 @@ NEXT_PUBLIC_SOLR_BASE_URL=
NEXT_PUBLIC_SOLR_BASE_URL_STAGING=https://staging.modelseed.org/solr/
NEXT_PUBLIC_SOLR_BASE_URL_PRODUCTION=https://modelseed.org/solr/

# Per-corpus base precedence: explicit override → mode default → shared
# NEXT_PUBLIC_SOLR_BASE_URL resolution. Leave empty to retain shared-base behavior.
# A relative value such as /solr/ is served through the SOLR_PROXY_UPSTREAM rewrite.
NEXT_PUBLIC_SOLR_REACTIONS_BASE_URL=
NEXT_PUBLIC_SOLR_REACTIONS_BASE_URL_STAGING=
NEXT_PUBLIC_SOLR_REACTIONS_BASE_URL_PRODUCTION=
NEXT_PUBLIC_SOLR_COMPOUNDS_BASE_URL=
NEXT_PUBLIC_SOLR_COMPOUNDS_BASE_URL_STAGING=
NEXT_PUBLIC_SOLR_COMPOUNDS_BASE_URL_PRODUCTION=
NEXT_PUBLIC_SOLR_STRUCTURES_BASE_URL=
NEXT_PUBLIC_SOLR_STRUCTURES_BASE_URL_STAGING=
NEXT_PUBLIC_SOLR_STRUCTURES_BASE_URL_PRODUCTION=

# Server-side only; for dev/internal use. http://poplar:8983/solr is an internal
# host and must never be used as a public production value.
SOLR_PROXY_UPSTREAM=

# =============================================================================
# SOLR COLLECTION / CORE NAMES
# =============================================================================
# The Solr core names for the reactions and compounds collections.
# The Solr core names for the reactions, compounds, and structures collections.
# These must match the names configured in your Solr instance.
#
# Override: Required in manual mode, otherwise optional (mode default used)
# Mode default: staging=reactions_staging / compounds_staging
# production=reactions / compounds
# Fallback: staging: "reactions_staging" / "compounds_staging"
# production: "reactions" / "compounds"
# Override: reactions/compounds required in manual mode; structures optional (falls back to structures)
# Mode default: staging=reactions_staging / compounds_staging / structures_staging
# production=reactions / compounds / structures
# Fallback: staging: "reactions_staging" / "compounds_staging" / "structures_staging"
# production: "reactions" / "compounds" / "structures"
NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION=
NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION=
NEXT_PUBLIC_SOLR_STRUCTURES_COLLECTION=
NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_STAGING=reactions_staging
NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_STAGING=compounds_staging
NEXT_PUBLIC_SOLR_STRUCTURES_COLLECTION_STAGING=structures_staging
NEXT_PUBLIC_SOLR_REACTIONS_COLLECTION_PRODUCTION=reactions
NEXT_PUBLIC_SOLR_COMPOUNDS_COLLECTION_PRODUCTION=compounds
NEXT_PUBLIC_SOLR_STRUCTURES_COLLECTION_PRODUCTION=structures

# =============================================================================
# SOLR NESTED SCHEMA OVERRIDE
# =============================================================================
# Whether the Solr reactions/compounds collections use the Solr-9 nested-
# document schema (parent docs plus [child] rows). Unset = auto-detect via a
# one-time probe query per collection. true/1 = force nested-schema queries
# (parent docs only). false/0 = force legacy (flat) behavior.
NEXT_PUBLIC_SOLR_NESTED_SCHEMA=

# =============================================================================
# FEATURE FLAGS
Expand Down
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [3.4.0] - 2026-08-21

### Added
- Solr reaction, compound and structure lookups can now each use their own endpoint and core through separate environment variables, while retaining the shared Solr base when no per-corpus value is set
- An optional server-side proxy lets a deployment or local checkout serve Solr from its own origin

### Documentation
- Documented the full Solr environment surface and endpoint switching scenarios for legacy, Solr 9, temporary, and proxied instances

### Fixed
- Structure-core environment overrides now reach browser lookups instead of silently falling back to the shared endpoint

---

## [3.3.0] - 2026-08-21

### Added
- Reaction structure equations now resolve atom-mapping colours from raw InChI canonical order through the structures Solr core, rather than treating canonical `#N` references as SMILES or renderer positions
- Each mapped participant and the legend now disclose whether a highlight is an exact atom, a symmetry-equivalent orbit, a whole-element block, or unresolved, so researchers can see precisely what the mapping supports

### Fixed
- Atom-mapping highlights no longer assign chemically false colours when InChI canonical order diverges from stored-SMILES order

---

## [3.2.0] - 2026-08-20

### Added
- Reaction structure equations now use an open, continuous canvas with prominent common names, secondary IDs, formulas and charges; plain `+` and direction operators; and compound-page links
- Every reaction participant that has a structure is now drawn, including small species such as water, CO2 and ammonium; only heavy-atom-free species such as H+ stay textual, and they keep their name, formula, charge and compound link
- Reaction atom mappings now colour atoms and bonds by mapped group across reactants and products, with a legend that discloses mappings that cannot safely be coloured
- Atom mappings in which several compounds contribute the same element to one product are now coloured as one merged group, and the legend states plainly that individual atom pairing is not determined by the data; group members that are only partially covered are named as uncoloured rather than dropped
- Each participant shows a labelled colour-dot row naming its mapped elements, so colour is never the only carrier of meaning
- Mapping colours are applied only to fully covered, mutually mapped compound-element blocks, never by treating InChI canonical-order `#N` indices as renderer atom indices
- Compound and reaction detail pages now list every thermodynamics record returned by the upgraded Solr schema, one row per source with energy, error and (for reactions) direction operator
- Compound detail page now shows all pKa and pKb values instead of only the first
- Reaction detail page now shows an atom-mapping summary with per-compound element counts, a confidence indicator and an expandable raw list
- Reaction atom mappings now disclose symmetry-equivalent groups without claiming a specific atom correspondence
- All of the above is feature-detected, so pages render exactly as before against the current production Solr

### Fixed
- Reaction detail pages now read the live Solr `atom_mapping_data` field while retaining legacy `atom_mapping` fallback

### Changed
- Replaced the flat reaction atom-flow diagram with the structure canvas; the raw mapping list remains available as secondary detail
- Reaction thermodynamics direction agreement is now derived from the per-source direction operators rather than a single server flag, and reports three states: "Sources agree on direction" (all operators identical), "Sources could agree on direction" (only one angle-bracket direction, optionally mixed with `=`) and "Sources disagree on direction" (both `>` and `<` present)

### Known Issues
- RAST MS FBA not working
- PATRIC-only model submission
- Workspace write operations limited

### Expected Behaviors
- Models/Media differ between RAST and PATRIC (intentional system design)

---

## [3.1.1] - 2026-08-26

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Key configuration constants live in `lib/api/config.ts`:
- `MODELSEED_API_URL` – base URL for Poplar (currently `http://poplar.cels.anl.gov:8000` in development).
- `USE_MODELSEED_API` – when `true`, user data flows (My Models, My Media, jobs) use `modelseed-api`.
- `USE_NEW_PROXY` – when `true`, workspace calls route through the REST proxy at `${MODELSEED_API_URL}/api/workspace`.
- `SOLR_BASE` / `SOLR_REACTIONS_COLLECTION` / `SOLR_COMPOUNDS_COLLECTION` – control Solr endpoint and core selection for biochem pages.
- `NEXT_PUBLIC_SOLR_BASE_URL`, per-corpus `NEXT_PUBLIC_SOLR_*_BASE_URL` overrides, and `NEXT_PUBLIC_SOLR_*_COLLECTION` control Solr endpoints and core selection for biochem pages; see [Solr configuration](docs/DEPLOYMENT.md#solr-configuration).

## Running the App Locally

Expand Down
2 changes: 1 addition & 1 deletion VERSION.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.1.1
3.4.0
44 changes: 33 additions & 11 deletions app/(reference-data)/biochem/compounds/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '@/lib/api/biochem';
import { formatFormula } from '@/components/utils/formatFormula';
import { formatEquation } from '@/components/utils/formatEquation';
import ThermodynamicsTable from '@/components/ui/ThermodynamicsTable';

/* ─── Helpers ────────────────────────────────────────────────── */

Expand Down Expand Up @@ -365,12 +366,19 @@ export default function CompoundDetailPage() {
: [];
const aliasesWithoutName = cpd.aliases?.filter((a) => !a.startsWith('Name:')) ?? [];

const pkaDisplay = cpd.pka?.[0]?.replace(/"/g, '') ?? null;
const pkbDisplay = cpd.pkb?.[0]?.replace(/"/g, '') ?? null;
const pkaValues = (Array.isArray(cpd.pka_value) ? cpd.pka_value : Array.isArray(cpd.pka) ? cpd.pka : [])
.map((v) => String(v).replace(/"/g, ''));
const pkbValues = (Array.isArray(cpd.pkb_value) ? cpd.pkb_value : Array.isArray(cpd.pkb) ? cpd.pkb : [])
.map((v) => String(v).replace(/"/g, ''));

const deltaGDisplay = cpd.deltag === 10000000 ? 'unspecified' : String(cpd.deltag);
const deltaGerrDisplay = cpd.deltagerr === 10000000 ? 'unspecified' : String(cpd.deltagerr);

const thermoRecords = cpd.thermodynamics ?? [];
const thermoLabel = cpd.n_sources_thermodynamics && cpd.n_sources_thermodynamics > 0
? `Thermodynamics (${cpd.n_sources_thermodynamics} sources)`
: 'Thermodynamics';

return (
<Box sx={{ px: 3, py: 2, maxWidth: 1200, mx: 'auto' }}>
{/* ── Title ── */}
Expand Down Expand Up @@ -419,19 +427,33 @@ export default function CompoundDetailPage() {

{/* Properties */}
<Box sx={{ flex: 1, minWidth: 300 }}>
<DetailRow label="ΔG">
<Typography variant="body2">
{deltaGDisplay === 'unspecified' ? 'N/A' : `${deltaGDisplay}${deltaGerrDisplay !== 'unspecified' ? ` ± ${deltaGerrDisplay}` : ''} kcal/mol`}
</Typography>
</DetailRow>
{pkaDisplay && (
{thermoRecords.length > 0 ? (
<DetailRow label={thermoLabel}>
<ThermodynamicsTable records={thermoRecords} />
</DetailRow>
) : (
<DetailRow label="ΔG">
<Typography variant="body2">
{deltaGDisplay === 'unspecified' ? 'N/A' : `${deltaGDisplay}${deltaGerrDisplay !== 'unspecified' ? ` ± ${deltaGerrDisplay}` : ''} kcal/mol`}
</Typography>
</DetailRow>
)}
{pkaValues.length > 0 && (
<DetailRow label="pKa">
<PKaDisplay value={pkaDisplay} />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.3 }}>
{pkaValues.map((v, i) => (
<PKaDisplay value={v} key={`pka-${i}`} />
))}
</Box>
</DetailRow>
)}
{pkbDisplay && (
{pkbValues.length > 0 && (
<DetailRow label="pKb">
<PKaDisplay value={pkbDisplay} />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.3 }}>
{pkbValues.map((v, i) => (
<PKaDisplay value={v} key={`pkb-${i}`} />
))}
</Box>
</DetailRow>
)}
<DetailRow label="Weight">
Expand Down
64 changes: 59 additions & 5 deletions app/(reference-data)/biochem/reactions/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { useMemo } from 'react';
import { useParams } from 'next/navigation';
import { useQuery } from '@tanstack/react-query';
import Box from '@mui/material/Box';
Expand All @@ -13,6 +14,13 @@ import Link from 'next/link';
import { getReactionById, EXTERNAL_DBS } from '@/lib/api/biochem';
import ChemicalEquation from '@/components/ui/ChemicalEquation';
import ReactionStructureEquation from '@/components/ui/ReactionStructureEquation';
import ThermodynamicsTable from '@/components/ui/ThermodynamicsTable';
import { normalizeAtomMapping, parseAtomMappings } from '@/lib/utils/atomMapping';
import {
directionAgreementFromRecords,
DIRECTION_AGREEMENT_COLOR,
DIRECTION_AGREEMENT_LABEL,
} from '@/lib/utils/reactionDirection';

function extractCompoundIds(equation: string): string[] {
if (!equation) return [];
Expand Down Expand Up @@ -308,6 +316,9 @@ export default function ReactionDetailPage() {
enabled: !!id,
});

const atomMapping = useMemo(() => normalizeAtomMapping(rxn), [rxn]);
const atomPairs = useMemo(() => parseAtomMappings(atomMapping.entries), [atomMapping.entries]);

if (isLoading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
Expand Down Expand Up @@ -335,6 +346,9 @@ export default function ReactionDetailPage() {

const compoundIds = extractCompoundIds(rxn.equation || rxn.definition);

const thermoRecords = rxn.thermodynamics ?? [];
const agreement = directionAgreementFromRecords(thermoRecords);

const dg = Number(rxn.deltag);
const err = Number(rxn.deltagerr);
const deltaGLabel = Number.isNaN(dg)
Expand Down Expand Up @@ -364,6 +378,9 @@ export default function ReactionDetailPage() {
<ReactionStructureEquation
equation={rxn.equation ?? rxn.definition}
reversibility={rxn.reversibility}
atomMappingPairs={atomPairs}
atomMappingConfidence={atomMapping.confidence}
atomMappingHasSymmetryGroups={atomMapping.hasSymmetryGroups}
/>
)}
</Box>
Expand All @@ -381,11 +398,47 @@ export default function ReactionDetailPage() {
<ChemicalEquation equation={rxn.equation} />
</DetailRow>

<DetailRow label="Gibbs free energy change (ΔG)">
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{deltaGLabel}
</Typography>
</DetailRow>
{thermoRecords.length > 0 ? (
<DetailRow
label={
typeof rxn.n_sources_thermodynamics === 'number' &&
rxn.n_sources_thermodynamics > 0
? `Thermodynamics (${rxn.n_sources_thermodynamics} sources)`
: 'Thermodynamics'
}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{agreement !== null ? (
<Chip
size="small"
label={DIRECTION_AGREEMENT_LABEL[agreement]}
color={DIRECTION_AGREEMENT_COLOR[agreement]}
sx={{ alignSelf: 'flex-start' }}
/>
) : (
typeof rxn.sources_agree_direction === 'boolean' && (
<Chip
size="small"
label={
rxn.sources_agree_direction
? 'Sources agree on direction'
: 'Sources disagree on direction'
}
color={rxn.sources_agree_direction ? 'success' : 'warning'}
sx={{ alignSelf: 'flex-start' }}
/>
)
)}
<ThermodynamicsTable records={thermoRecords} showOperator />
</Box>
</DetailRow>
) : (
<DetailRow label="Gibbs free energy change (ΔG)">
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{deltaGLabel}
</Typography>
</DetailRow>
)}

<DetailRow label="EC numbers">
{ecNumbers.length ? (
Expand Down Expand Up @@ -454,6 +507,7 @@ export default function ReactionDetailPage() {
</Typography>
</DetailRow>
)}

</CardContent>
</Card>
</Box>
Expand Down
Loading
Loading