Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add front-end warnings and Inequality integration for new Chemistry options #1165

Merged
merged 13 commits into from
Oct 25, 2024
88 changes: 52 additions & 36 deletions src/app/components/content/IsaacSymbolicChemistryQuestion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { v4 as uuid_v4 } from "uuid";
import { Inequality, makeInequality } from "inequality";
import { parseInequalityChemistryExpression, parseInequalityNuclearExpression, ParsingError } from "inequality-grammar";
import { AppState, useAppSelector } from "../../state";
import { CHEMICAL_ELEMENTS, CHEMICAL_PARTICLES, CHEMICAL_STATES } from "../elements/modals/inequality/constants";

const InequalityModal = lazy(() => import("../elements/modals/inequality/InequalityModal"));

Expand Down Expand Up @@ -48,40 +49,6 @@ function isError(p: ParsingError | any[]): p is ParsingError {
return p.hasOwnProperty("error");
}

export const symbolicInputValidator = (input: string) => {
const openRoundBracketsCount = input.split("(").length - 1;
const closeRoundBracketsCount = input.split(")").length - 1;
const openSquareBracketsCount = input.split("[").length - 1;
const closeSquareBracketsCount = input.split("]").length - 1;
const openCurlyBracketsCount = input.split("{").length - 1;
const closeCurlyBracketsCount = input.split("}").length - 1;
const regexStr = /[^ 0-9A-Za-z()[\]{}*+,-./<=>^_\\]+/;
const badCharacters = new RegExp(regexStr);
const errors = [];
if (badCharacters.test(input)) {
const usedBadChars: string[] = [];
for(let i = 0; i < input.length; i++) {
const char = input.charAt(i);
if (badCharacters.test(char)) {
if (!usedBadChars.includes(char)) {
usedBadChars.push(char);
}
}
}
errors.push('Some of the characters you are using are not allowed: ' + usedBadChars.join(" "));
}
if (openRoundBracketsCount !== closeRoundBracketsCount
|| openSquareBracketsCount !== closeSquareBracketsCount
|| openCurlyBracketsCount !== closeCurlyBracketsCount) {
// Rather than a long message about which brackets need closing
errors.push('You are missing some brackets.');
}
if (/\.[0-9]/.test(input)) {
errors.push('Please convert decimal numbers to fractions.');
}
return errors;
};

const IsaacSymbolicChemistryQuestion = ({doc, questionId, readonly}: IsaacQuestionProps<IsaacSymbolicChemistryQuestionDTO>) => {

const { currentAttempt, dispatchSetCurrentAttempt } = useCurrentQuestionAttempt<ChemicalFormulaDTO>(questionId);
Expand All @@ -98,6 +65,46 @@ const IsaacSymbolicChemistryQuestion = ({doc, questionId, readonly}: IsaacQuesti
currentAttemptValue = jsonHelper.parseOrDefault(currentAttempt.value, {result: {tex: '\\textrm{PLACEHOLDER HERE}'}});
}

const hasMetaSymbols = !doc.availableSymbols?.every(symbol => CHEMICAL_ELEMENTS.includes(symbol.trim()) || CHEMICAL_PARTICLES.hasOwnProperty(symbol.trim()));

const symbolicInputValidator = (input: string) => {
const openRoundBracketsCount = input.split("(").length - 1;
const closeRoundBracketsCount = input.split(")").length - 1;
const openSquareBracketsCount = input.split("[").length - 1;
const closeSquareBracketsCount = input.split("]").length - 1;
const openCurlyBracketsCount = input.split("{").length - 1;
const closeCurlyBracketsCount = input.split("}").length - 1;
const regexStr = /[^ 0-9A-Za-z()[\]{}*+,-./<=>^_\\]+/;
const badCharacters = new RegExp(regexStr);
const errors = [];
if (badCharacters.test(input)) {
const usedBadChars: string[] = [];
for(let i = 0; i < input.length; i++) {
const char = input.charAt(i);
if (badCharacters.test(char)) {
if (!usedBadChars.includes(char)) {
usedBadChars.push(char);
}
}
}
errors.push('Some of the characters you are using are not allowed: ' + usedBadChars.join(" "));
}

if (openRoundBracketsCount !== closeRoundBracketsCount
|| openSquareBracketsCount !== closeSquareBracketsCount
|| openCurlyBracketsCount !== closeCurlyBracketsCount) {
// Rather than a long message about which brackets need closing
errors.push('You are missing some brackets.');
}
if (/\.[0-9]/.test(input)) {
errors.push('Please convert decimal numbers to fractions.');
}
if (/\(s\)|\(aq\)|\(l\)|\(g\)/.test(input) && hasMetaSymbols && !doc.availableSymbols?.some(symbol => CHEMICAL_STATES.includes(symbol))) {
errors.push('This question does not require state symbols.');
}
return errors;
};

function currentAttemptMhchemExpression(): string {
return (currentAttemptValue?.result && currentAttemptValue.result.mhchem) || "";
}
Expand Down Expand Up @@ -212,7 +219,16 @@ const IsaacSymbolicChemistryQuestion = ({doc, questionId, readonly}: IsaacQuesti
};

const helpTooltipId = useMemo(() => `eqn-editor-help-${uuid_v4()}`, []);
let symbolList = parsePseudoSymbolicAvailableSymbols(doc.availableSymbols)?.map(str => str.trim().replace(/;/g, ',') ).sort().join(", ");

// Automatically filters out state symbols/brackets/etc from Nuclear Physics questions
const modifiedAvailableSymbols = doc.availableSymbols ? doc.availableSymbols : [];
if (doc.isNuclear && !hasMetaSymbols) {
modifiedAvailableSymbols.push("_plus", "_minus", "_fraction", "_right_arrow");
}

// We need these symbols available to do processing with, but don't want to display them to the user as available.
const removedSymbols = ["+","-","/","->","<=>","()","[]","."];
let symbolList = parsePseudoSymbolicAvailableSymbols(modifiedAvailableSymbols)?.filter(str => !removedSymbols.includes(str)).map(str => str.trim().replace(/;/g, ',') ).sort().join(", ");

symbolList = symbolList?.replace('electron', 'e').replace('alpha', '\\alphaparticle').replace('beta', '\\betaparticle').replace('gamma', '\\gammaray').replace('neutron', '\\neutron')//
.replace('proton', '\\proton').replace('neutrino', '\\neutrino').replace('antineutrino', '\\antineutrino');
Expand Down Expand Up @@ -272,7 +288,7 @@ const IsaacSymbolicChemistryQuestion = ({doc, questionId, readonly}: IsaacQuesti
dispatchSetCurrentAttempt({ type: 'chemicalFormula', value: JSON.stringify(state), mhchemExpression: (state && state.result && state.result.mhchem) || "" });
initialEditorSymbols.current = state.symbols;
}}
availableSymbols={doc.availableSymbols}
availableSymbols={modifiedAvailableSymbols}
initialEditorSymbols={initialEditorSymbols.current}
editorSeed={editorSeed}
editorMode={doc.isNuclear ? "nuclear" : "chemistry"}
Expand Down
29 changes: 20 additions & 9 deletions src/app/components/elements/modals/inequality/InequalityModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import classNames from "classnames";
import {Markup} from "../../markup";
import {
CHEMICAL_ELEMENTS,
CHEMICAL_PARTICLES,
EditorMode,
LogicSyntax,
MenuItemProps,
Expand Down Expand Up @@ -111,6 +112,20 @@ const MathOtherFunctionsMenu = ({defaultMenu, menuItems, activeSubMenu}: {defaul
}
};

const ChemistryOtherFunctionsMenu = ({defaultMenu, menuItems}: {defaultMenu: boolean; menuItems: MenuItems}) => {
if (defaultMenu || (menuItems.otherChemistryFunctions.length === 0)) {
return <div className="top-menu chemistry operations">
<ul className="sub-menu operations">{menuItems.chemicalOperations.map(buildIndexedMenuItem)}</ul>
</div>;
} else {
return <div className="top-menu chemistry operations">
<ul className="sub-menu operations">
{menuItems.otherChemistryFunctions.map(buildIndexedMenuItem)}
</ul>
</div>;
}
};

const LettersMenu = ({defaultMenu, menuItems, editorMode, activeSubMenu}: {defaultMenu: boolean; menuItems: MenuItems, editorMode: EditorMode, activeSubMenu: InequalityMenuSubMenuTabType}) => {
if (defaultMenu) {
return <div className="top-menu letters">
Expand Down Expand Up @@ -227,8 +242,8 @@ const InequalityMenu = React.forwardRef<HTMLDivElement, InequalityMenuProps>(({o
</div>}
{editorMode === "maths" && activeMenu === "mathsOtherFunctions" && <MathOtherFunctionsMenu activeSubMenu={activeSubMenu} menuItems={menuItems} defaultMenu={defaultMenu}/>}

{["chemistry", "nuclear"].includes(editorMode) && <>
{activeMenu === "elements" && (isDefined(availableSymbols) && availableSymbols.length > 0
{["chemistry", "nuclear"].includes(editorMode) && <>
{activeMenu === "elements" && (isDefined(availableSymbols) && availableSymbols.some(symbol => CHEMICAL_ELEMENTS.includes(symbol) || CHEMICAL_PARTICLES.hasOwnProperty(symbol))
? <div className="top-menu chemistry elements">
<ul className="sub-menu elements">
{menuItems.chemicalElements.map(buildIndexedMenuItem)}
Expand All @@ -251,16 +266,12 @@ const InequalityMenu = React.forwardRef<HTMLDivElement, InequalityMenuProps>(({o
{menuItems.chemicalParticles.map(buildIndexedMenuItem)}
</ul>
</div>}
{activeMenu === "states" && <div className="top-menu chemistry states">
{activeMenu === "states" && (menuItems.otherChemicalStates.length > 0 || menuItems.otherChemistryFunctions.length == 0) && <div className="top-menu chemistry states">
<ul className="sub-menu states">
{menuItems.chemicalStates.map(buildIndexedMenuItem)}
</ul>
</div>}
{["chemistry", "nuclear"].includes(editorMode) && activeMenu === "operations" && <div className="top-menu chemistry operations">
<ul className="sub-menu operations">
{menuItems.chemicalOperations.map(buildIndexedMenuItem)}
</ul>
</div>}
{["chemistry", "nuclear"].includes(editorMode) && activeMenu === "operations" && <ChemistryOtherFunctionsMenu menuItems={menuItems} defaultMenu={defaultMenu}/>}
</>}
</div>
<div id="inequality-menu-tabs" className="menu-tabs">
Expand All @@ -274,7 +285,7 @@ const InequalityMenu = React.forwardRef<HTMLDivElement, InequalityMenuProps>(({o
{["chemistry", "nuclear"].includes(editorMode) && <>
<InequalityMenuTab menu={"elements"} latexTitle={isDefined(availableSymbols) && availableSymbols.length > 0 && menuItems.chemicalElements.map(i => i.type).includes("Particle") ? "\\text{He Li}\\ \\alpha" : "\\text{H He Li}"}/>
{menuItems.chemicalParticles.length > 0 && <InequalityMenuTab menu={"particles"} latexTitle={"\\alpha\\ \\gamma\\ \\text{e}"}/>}
{editorMode === "chemistry" && <InequalityMenuTab menu={"states"} latexTitle={"(aq)\\, (g)\\, (l)"}/>}
{editorMode === "chemistry" && (menuItems.otherChemicalStates.length > 0 || menuItems.otherChemistryFunctions.length == 0) && <InequalityMenuTab menu={"states"} latexTitle={"(aq)\\, (g)\\, (l)"}/>}
<InequalityMenuTab menu={"operations"} latexTitle={"\\rightarrow\\, \\rightleftharpoons\\, +"}/>
</>}
</ul>
Expand Down
3 changes: 3 additions & 0 deletions src/app/components/elements/modals/inequality/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface MenuItems {
// The following are reduced versions in case there are available symbols and should replace their respective sub-sub-menus.
letters: MenuItemProps[];
otherFunctions: MenuItemProps[];
otherChemistryFunctions: MenuItemProps[];
otherChemicalStates: MenuItemProps[];
}

export const CHEMICAL_ELEMENTS = ["H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl", "Ar", "K", "Ca", "Sc", "Ti", "V", "Cr", "Mn", "Fe", "Co", "Ni", "Cu", "Zn", "Ga", "Ge", "As", "Se", "Br", "Kr", "Rb", "Sr", "Y", "Zr", "Nb", "Mo", "Tc", "Ru", "Rh", "Pd", "Ag", "Cd", "In", "Sn", "Sb", "Te", "I", "Xe", "Cs", "Ba", "La", "Ce", "Pr", "Nd", "Pm", "Sm", "Eu", "Gd", "Tb", "Dy", "Ho", "Er", "Tm", "Yb", "Lu", "Hf", "Ta", "W", "Re", "Os", "Ir", "Pt", "Au", "Hg", "Tl", "Pb", "Bi", "Po", "At", "Rn", "Fr", "Ra", "Ac", "Th", "Pa", "U", "Np", "Pu", "Am", "Cm", "Bk", "Cf", "Es", "Fm", "Md", "No", "Lr", "Rf", "Db", "Sg", "Bh", "Hs", "Mt", "Ds", "Rg", "Cn", "Nh", "Fl", "Mc", "Lv", "Ts", "Og"];
Expand Down Expand Up @@ -73,6 +75,7 @@ export const CHEMICAL_PARTICLES: {[key: string]: MenuItemProps} = {
properties: { particle: 'e', type: 'electron' }
}
};
export const CHEMICAL_STATES = ["(g)", "(l)", "(aq)", "(s)"];

export const LOWER_CASE_GREEK_LETTERS = ["alpha", "beta", "gamma", "delta", "varepsilon", "zeta", "eta", "theta", "iota", "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi", "rho", "sigma", "tau", "upsilon", "phi", "chi", "psi", "omega"];
export const UPPER_CASE_GREEK_LETTERS = ["Gamma", "Delta", "Theta", "Lambda", "Xi", "Pi", "Sigma", "Upsilon", "Phi", "Psi", "Omega"];
Expand Down
46 changes: 41 additions & 5 deletions src/app/components/elements/modals/inequality/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
LogicSyntax,
MenuItemProps,
MenuItems,
EditorMode
EditorMode,
CHEMICAL_STATES
} from "./constants";
import {GREEK_LETTERS_MAP, isDefined, sanitiseInequalityState} from "../../../../services";
import React from "react";
Expand Down Expand Up @@ -344,6 +345,24 @@ export function generateChemicalElementMenuItem(symbol: string): MenuItemProps |
return undefined;
}

export function generateChemicalOperationsMenuItem(symbol: string): MenuItemProps | undefined {
switch(symbol) {
case "+": return {type: 'BinaryOperation', properties: { operation: '+' }, menu: { label: '+', texLabel: true, className: 'chemical-operations plus' }};
case "-": return {type: 'BinaryOperation', properties: { operation: '-' }, menu: { label: '-', texLabel: true, className: 'chemical-operations minus' }};
case "/": return {type: 'Fraction', properties: { }, menu: { label: '\\frac{a}{b}', texLabel: true, className: 'chemical-operations fraction' }};
case "->": return {type: 'Relation', properties: { relation: 'rightarrow' }, menu: { label: '\\rightarrow', texLabel: true, className: 'chemical-operations rightarrow' }};
case "<=>": return {type: 'Relation', properties: { relation: 'equilibrium' }, menu: { label: '\\rightleftharpoons', texLabel: true, className: 'chemical-operations equilibrium' }};
case "[]": return {type: 'Brackets', properties: { type: 'square', mode: 'chemistry' }, menu: { label: '[x]', texLabel: true, className: 'chemical-operations brackets square' }};
case "()": return {type: 'Brackets', properties: { type: 'round', mode: 'chemistry' }, menu: { label: '(x)', texLabel: true, className: 'chemical-operations brackets round' }};
case ".": return {type: 'Relation', properties: { relation: '.' }, menu: { label: '\\cdot', texLabel: true, className: 'chemical-operations dot' }};
default: return undefined;
};
}

export function generateChemicalStateMenuItem(symbol: string): MenuItemProps | undefined {
return generateChemicalStatesMenuItems().find((item) => item.menu.label === `\\text{${symbol}}`);
}

export const generateDefaultMenuItems = (parsedAvailableSymbols: string[], logicSyntax?: LogicSyntax): MenuItems => ({
upperCaseLetters: [],
lowerCaseLetters: [],
Expand All @@ -360,6 +379,8 @@ export const generateDefaultMenuItems = (parsedAvailableSymbols: string[], logic
// The following are reduced versions in case there are available symbols and should replace their respective sub-sub-menus.
letters: [],
otherFunctions: [],
otherChemistryFunctions: [],
otherChemicalStates: [],
chemicalElements: [],
chemicalParticles: [],
parsedChemicalElements: []
Expand Down Expand Up @@ -440,11 +461,13 @@ export function generateMenuItems({editorMode, logicSyntax, parsedAvailableSymbo
if (parsedAvailableSymbols.length > 0) {
// ~~~ Assuming these are only letters... might become more complicated in the future.
// THE FUTURE IS HERE! Sorry.
const customMenuItems: {mathsDerivatives: MenuItemProps[]; letters: MenuItemProps[]; otherFunctions: MenuItemProps[]; chemicalElements: MenuItemProps[]} = {
const customMenuItems: {mathsDerivatives: MenuItemProps[]; letters: MenuItemProps[]; otherFunctions: MenuItemProps[]; otherChemistryFunctions: MenuItemProps[]; chemicalElements: MenuItemProps[]; otherChemicalStates: MenuItemProps[]} = {
mathsDerivatives: new Array<MenuItemProps>(),
letters: new Array<MenuItemProps>(),
otherFunctions: new Array<MenuItemProps>(),
otherChemistryFunctions: new Array<MenuItemProps>(),
chemicalElements: new Array<MenuItemProps>(),
otherChemicalStates: new Array<MenuItemProps>(),
};

parsedAvailableSymbols.forEach((l) => {
Expand Down Expand Up @@ -486,9 +509,21 @@ export function generateMenuItems({editorMode, logicSyntax, parsedAvailableSymbo
// Everything else is a letter, unless we are doing chemistry
if (["chemistry", "nuclear"].includes(editorMode)) {
// Available chemical elements
const item = generateChemicalElementMenuItem(availableSymbol);
if (item) {
customMenuItems.chemicalElements.push(item);
if (CHEMICAL_ELEMENTS.includes(availableSymbol) || CHEMICAL_PARTICLES.hasOwnProperty(availableSymbol)) {
const item = generateChemicalElementMenuItem(availableSymbol);
if (item) {
customMenuItems.chemicalElements.push(item);
}
} else if (CHEMICAL_STATES.includes(availableSymbol)) {
const item = generateChemicalStateMenuItem(availableSymbol);
if (item) {
customMenuItems.otherChemicalStates.push(item);
}
} else {
const item = generateChemicalOperationsMenuItem(availableSymbol);
if (item) {
customMenuItems.otherChemistryFunctions.push(item);
}
}
} else {
const item = generateLetterMenuItem(availableSymbol);
Expand Down Expand Up @@ -519,6 +554,7 @@ export function generateMenuItems({editorMode, logicSyntax, parsedAvailableSymbo
})*/,
otherFunctions: [ ...baseItems.otherFunctions, ...customMenuItems.otherFunctions ],
chemicalElements: [ ...baseItems.chemicalElements, ...customMenuItems.chemicalElements ],
otherChemistryFunctions: [ ...baseItems.otherChemistryFunctions, ...customMenuItems.otherChemistryFunctions ],
}, false] as [MenuItems, boolean];
} else {
if (editorMode === "logic") {
Expand Down
Loading