diff --git a/package-lock.json b/package-lock.json
index 0e600a3c4..0fdc8372b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -46,7 +46,7 @@
"nmr-processing": "^22.1.0",
"numeral": "^2.0.6",
"openchemlib": "^9.18.2",
- "openchemlib-utils": "^8.9.0",
+ "openchemlib-utils": "^8.12.1",
"papaparse": "^5.5.3",
"react-d3-utils": "^3.1.2",
"react-dropzone": "^14.3.8",
@@ -55,7 +55,7 @@
"react-icons": "^5.5.0",
"react-inspector": "^9.0.0",
"react-mf": "^3.1.1",
- "react-ocl": "^8.4.0",
+ "react-ocl": "^8.5.0",
"react-ocl-nmr": "^4.1.1",
"react-plot": "^3.1.2",
"react-rnd": "^10.5.2",
@@ -9621,9 +9621,9 @@
"peer": true
},
"node_modules/openchemlib-utils": {
- "version": "8.9.0",
- "resolved": "https://registry.npmjs.org/openchemlib-utils/-/openchemlib-utils-8.9.0.tgz",
- "integrity": "sha512-eSKIXOK/ybOf1EoWEUaKeFxARimjd2BOBuiw7P+NaZFuPa0Xvg6HZ57DcJby0g/pJxF5f0m5ChPz4MGM/AVRwg==",
+ "version": "8.12.1",
+ "resolved": "https://registry.npmjs.org/openchemlib-utils/-/openchemlib-utils-8.12.1.tgz",
+ "integrity": "sha512-aQbEzLxunuRqwEVw6I98J9NL+Mxo3uASLbFjhsFe4ZuJZWN7G+s8tgmXO43+9kaNNeD8lAjcG6+EHNhwX+TV3w==",
"license": "MIT",
"dependencies": {
"atom-sorter": "^2.2.1",
@@ -10478,10 +10478,13 @@
}
},
"node_modules/react-ocl": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/react-ocl/-/react-ocl-8.4.0.tgz",
- "integrity": "sha512-HrKWK78u9XOeW9U6Qh869Z+ee1Kv0DsayrQeurkh+mTIkf4Gy/oeY31Q4WkzHV9Hcar44XH8d2jjEYyFCJVcJw==",
+ "version": "8.5.0",
+ "resolved": "https://registry.npmjs.org/react-ocl/-/react-ocl-8.5.0.tgz",
+ "integrity": "sha512-IOgPuJCcGs36iiiWjVz1EuHzutlmFQak7H+YXEy0cjEodZeL7HWAqrjWzd1/uyKIpDL+Hkqzk8RNVyWt2j18xg==",
"license": "MIT",
+ "dependencies": {
+ "@emotion/styled": "^11.14.1"
+ },
"peerDependencies": {
"openchemlib": ">=8",
"react": ">=18",
diff --git a/package.json b/package.json
index 487fe4ddc..fbff20b23 100644
--- a/package.json
+++ b/package.json
@@ -100,7 +100,7 @@
"nmr-processing": "^22.1.0",
"numeral": "^2.0.6",
"openchemlib": "^9.18.2",
- "openchemlib-utils": "^8.9.0",
+ "openchemlib-utils": "^8.12.1",
"papaparse": "^5.5.3",
"react-d3-utils": "^3.1.2",
"react-dropzone": "^14.3.8",
@@ -109,7 +109,7 @@
"react-icons": "^5.5.0",
"react-inspector": "^9.0.0",
"react-mf": "^3.1.1",
- "react-ocl": "^8.4.0",
+ "react-ocl": "^8.5.0",
"react-ocl-nmr": "^4.1.1",
"react-plot": "^3.1.2",
"react-rnd": "^10.5.2",
diff --git a/src/component/modal/MoleculeAutoLabelsDatabaseModal.tsx b/src/component/modal/MoleculeAutoLabelsDatabaseModal.tsx
new file mode 100644
index 000000000..835c34ae1
--- /dev/null
+++ b/src/component/modal/MoleculeAutoLabelsDatabaseModal.tsx
@@ -0,0 +1,219 @@
+import { Dialog, InputGroup } from '@blueprintjs/core';
+import styled from '@emotion/styled';
+import { Molecule } from 'openchemlib';
+import { autoLabelDatabase } from 'openchemlib-utils';
+import { useState } from 'react';
+import { MF } from 'react-mf';
+import { IdcodeSvgRenderer } from 'react-ocl';
+import { Button } from 'react-science/ui';
+import { filter } from 'smart-array-filter';
+
+import { useDispatch } from '../context/DispatchContext.tsx';
+import { StyledDialogBody } from '../elements/StyledDialogBody.js';
+
+interface LabelDatabaseItem {
+ idCode: string;
+ coordinates: string;
+ mf: string;
+ mw: number;
+ label: string;
+}
+
+const ChemicalGrid = styled.div`
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1.25rem;
+ justify-content: flex-start;
+ padding: 0.5rem 1rem;
+ flex: 1;
+`;
+
+const AddButton = styled(Button)`
+ position: absolute;
+ top: 0;
+ left: 0;
+`;
+
+const ChemicalCard = styled.div`
+ position: relative;
+ flex: 0 0 calc((100% - 2.5rem) / 3);
+ min-width: 200px;
+ border-radius: 8px;
+ overflow: hidden;
+ background: #fff;
+ box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
+ transition: all 0.25s ease;
+ /* stylelint-disable nesting-selector-no-missing-scoping-root */
+ &:hover {
+ box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
+ transform: translateY(-2px);
+ }
+`;
+
+const CardLabel = styled.div`
+ padding: 0.75rem 1rem;
+ background: linear-gradient(180deg, #fff 0%, #f0f2f5 40%, #e2e6eb 100%);
+ border-bottom: 1px solid #d1d5db;
+ box-shadow: inset 0 1px 0 rgb(255 255 255 / 80%);
+
+ h3 {
+ margin: 0;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: #374151;
+ text-align: center;
+ text-transform: capitalize;
+ letter-spacing: 0.02em;
+ text-shadow: 0 1px 0 rgb(255 255 255 / 50%);
+ }
+`;
+
+const StructureContainer = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.5rem;
+ background: #fafafa;
+ border-bottom: 1px solid #eee;
+`;
+
+const CardInfo = styled.div`
+ padding: 0.75rem 1rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ background: #fff;
+`;
+
+const MolecularFormula = styled.div<{ color?: string }>`
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: ${({ color }) => color ?? '#374151'};
+`;
+
+const SearchContainer = styled.div`
+ padding: 1.2rem;
+ margin-bottom: 1.5rem;
+ background-color: white;
+ border-bottom: 1px solid #e0e0e0;
+ position: sticky;
+ top: 0;
+ z-index: 1;
+`;
+
+const SearchInput = styled(InputGroup)`
+ input {
+ background: #eceff7;
+
+ &:focus {
+ background: #fff;
+ box-shadow: 0 1px 4px rgb(0 0 0 / 15%);
+ }
+ }
+`;
+
+const NoResults = styled.p`
+ text-align: center;
+ color: #6b7280;
+ font-size: 1rem;
+ padding: 3rem;
+`;
+interface MoleculeAutoLabelsDatabaseModalProps {
+ onClose?: (element?: string) => void;
+}
+
+export function MoleculeAutoLabelsDatabaseModal({
+ onClose = () => null,
+}: MoleculeAutoLabelsDatabaseModalProps) {
+ const dispatch = useDispatch();
+ const [keywords, setSearch] = useState('');
+
+ function handleAddMolecule(options: LabelDatabaseItem) {
+ const { idCode, coordinates, label } = options;
+ const molecule = Molecule.fromIDCode(idCode, coordinates);
+
+ if (molecule.getAllAtoms() <= 0) {
+ return;
+ }
+
+ dispatch({
+ type: 'ADD_MOLECULE',
+ payload: { molfile: molecule.toMolfileV3(), label },
+ });
+
+ onClose();
+ }
+
+ const filteredLabelDatabase = filter(autoLabelDatabase, { keywords });
+
+ return (
+
+ );
+}
diff --git a/src/component/panels/MoleculesPanel/MoleculePanelHeader.tsx b/src/component/panels/MoleculesPanel/MoleculePanelHeader.tsx
index 2cc37f02f..ef9b76da0 100644
--- a/src/component/panels/MoleculesPanel/MoleculePanelHeader.tsx
+++ b/src/component/panels/MoleculesPanel/MoleculePanelHeader.tsx
@@ -1,8 +1,10 @@
import { Molecule } from 'openchemlib';
+import { autoLabel } from 'openchemlib-utils';
import type { ReactNode } from 'react';
import { useCallback } from 'react';
import {
FaCopy,
+ FaDatabase,
FaDownload,
FaFileExport,
FaFileImage,
@@ -13,7 +15,7 @@ import {
} from 'react-icons/fa';
import { FaMaximize, FaMinimize } from 'react-icons/fa6';
import { IoOpenOutline } from 'react-icons/io5';
-import { MdNumbers, MdOutlineLabelOff } from 'react-icons/md';
+import { MdFlashAuto, MdNumbers, MdOutlineLabelOff } from 'react-icons/md';
import { PanelHeader, Toolbar } from 'react-science/ui';
import type {
@@ -33,7 +35,9 @@ import { useToaster } from '../../context/ToasterContext.js';
import { useTopicMolecule } from '../../context/TopicMoleculeContext.js';
import type { ToolbarPopoverMenuItem } from '../../elements/ToolbarPopoverItem.js';
import { ToolbarPopoverItem } from '../../elements/ToolbarPopoverItem.js';
+import { useDialogToggle } from '../../hooks/useDialogToggle.ts';
import AboutPredictionModal from '../../modal/AboutPredictionModal.js';
+import { MoleculeAutoLabelsDatabaseModal } from '../../modal/MoleculeAutoLabelsDatabaseModal.tsx';
import PredictSpectraModal from '../../modal/PredictSpectraModal.js';
import { booleanToString } from '../../utility/booleanToString.ts';
import {
@@ -67,30 +71,30 @@ const MOL_EXPORT_MENU: Array> = [
},
{
icon: ,
- text: 'Copy as molfile V3',
+ text: 'Copy as molfile V2',
data: {
- id: 'CopyAsMolfileV3',
+ id: 'CopyAsMolfileV2',
},
},
{
icon: ,
- text: 'Copy as molfile V2',
+ text: 'Copy as molfile V3',
data: {
- id: 'CopyAsMolfileV2',
+ id: 'CopyAsMolfileV3',
},
},
{
icon: ,
- text: 'Save as molfile V3',
+ text: 'Save as molfile V2',
data: {
- id: 'SaveAsMolfileV3',
+ id: 'SaveAsMolfileV2',
},
},
{
icon: ,
- text: 'Save as molfile V2',
+ text: 'Save as molfile V3',
data: {
- id: 'SaveAsMolfileV2',
+ id: 'SaveAsMolfileV3',
},
},
{
@@ -139,6 +143,9 @@ export default function MoleculePanelHeader(props: MoleculePanelHeaderProps) {
current: { defaultMoleculeSettings },
} = usePreferences();
const moleculeKey = molecules?.[currentIndex]?.id;
+ const { dialog, openDialog, closeDialog } = useDialogToggle({
+ autoLabelDatabaseDialog: false,
+ });
const saveAsSVGHandler = useCallback(() => {
if (!rootRef) return;
exportAsSVG(`molSVG${currentIndex}`, {
@@ -312,6 +319,20 @@ export default function MoleculePanelHeader(props: MoleculePanelHeaderProps) {
});
}
+ function autoLabels() {
+ const currentMolecule = molecules[currentIndex];
+
+ if (!currentMolecule) return;
+ const { id, label, molfile } = currentMolecule;
+
+ const molecule = Molecule.fromMolfile(molfile);
+ autoLabel(molecule);
+ dispatch({
+ type: 'SET_MOLECULE',
+ payload: { id, label, molfile: molecule.toMolfileV3() },
+ });
+ }
+
const hasMolecules = molecules && molecules.length > 0;
const showCounter = hasMolecules && renderSource !== 'predictionPanel';
const moreMenu: ToolbarPopoverMenuItem[] = [
@@ -333,6 +354,23 @@ export default function MoleculePanelHeader(props: MoleculePanelHeaderProps) {
disabled: !hasMolecules,
onClick: () => clearCustomAtomLabels(),
},
+ {
+ icon: ,
+ text: 'Auto label atoms',
+ disabled: !hasMolecules,
+ tooltip: {
+ title: 'Auto label atoms',
+ description:
+ 'Atoms are automatically labeled according to a predefined template database',
+ link: 'https://docs.nmrium.org/help/structure-labelling',
+ },
+ onClick: () => autoLabels(),
+ },
+ {
+ icon: ,
+ text: 'Template database',
+ onClick: () => openDialog('autoLabelDatabaseDialog'),
+ },
];
const { handleChangeAtomAnnotation, isAnnotation } =
@@ -343,6 +381,9 @@ export default function MoleculePanelHeader(props: MoleculePanelHeaderProps) {
current={showCounter ? currentIndex + 1 : undefined}
total={showCounter ? molecules.length : undefined}
>
+ {dialog.autoLabelDatabaseDialog && (
+
+ )}
{renderSource === 'predictionPanel' && }
{renderSource === 'moleculePanel' && (
diff --git a/src/component/reducer/actions/MoleculeActions.ts b/src/component/reducer/actions/MoleculeActions.ts
index 79d34a891..c3b89ca16 100644
--- a/src/component/reducer/actions/MoleculeActions.ts
+++ b/src/component/reducer/actions/MoleculeActions.ts
@@ -33,6 +33,7 @@ interface AddMoleculeProps {
id?: string;
floatMoleculeOnSave?: boolean;
defaultMoleculeSettings?: MoleculeView;
+ label?: string;
}
type AddMoleculeAction = ActionType<'ADD_MOLECULE', AddMoleculeProps>;
type AddMoleculesAction = ActionType<
@@ -105,9 +106,10 @@ export type MoleculeActions =
| ToggleMoleculeLabelAction;
function addMolecule(draft: Draft, props: AddMoleculeProps) {
- const { molfile, id, floatMoleculeOnSave, defaultMoleculeSettings } = props;
+ const { molfile, id, label, floatMoleculeOnSave, defaultMoleculeSettings } =
+ props;
const isEmpty = draft.molecules.length === 0;
- MoleculeManager.addMolfile(draft.molecules, molfile, id);
+ MoleculeManager.addMolfile(draft.molecules, molfile, { id, label });
/**
* if it's the first creation of a molecule after the molecules list was empty,
diff --git a/src/data/molecules/MoleculeManager.ts b/src/data/molecules/MoleculeManager.ts
index 7237d1f45..47c6dcf12 100644
--- a/src/data/molecules/MoleculeManager.ts
+++ b/src/data/molecules/MoleculeManager.ts
@@ -35,11 +35,16 @@ export function fromJSON(
return molecules;
}
+interface AddMolfileOptions {
+ id?: string;
+ label?: string;
+}
export function addMolfile(
molecules: StateMoleculeExtended[],
molfile: string,
- id?: string,
+ options: AddMolfileOptions,
) {
+ const { id, label } = options;
const reservedNumbers = extractLabelsNumbers(molecules);
// try to parse molfile
@@ -48,7 +53,7 @@ export function addMolfile(
molecules.push(
initMolecule({
molfile: molecule.toMolfileV3(),
- label: `P${getLabelNumber(reservedNumbers)}`,
+ label: label ?? `P${getLabelNumber(reservedNumbers)}`,
id,
}),
);