diff --git a/src/ProblemLayout/ProblemInput/ProblemInput.js b/src/ProblemLayout/ProblemInput/ProblemInput.js index b1904247d77..078588bacf8 100644 --- a/src/ProblemLayout/ProblemInput/ProblemInput.js +++ b/src/ProblemLayout/ProblemInput/ProblemInput.js @@ -5,6 +5,7 @@ import TextField from "@material-ui/core/TextField"; import MultipleChoice from "./MultipleChoice"; import GridInput from "./GridInput"; import MatrixInput from "./MatrixInput"; +import TableInput from "./TableInput"; import { renderText } from "../../ProblemLogic/renderText"; import clsx from "clsx"; import './ProblemInput.css' @@ -12,6 +13,7 @@ import { shuffleArray } from "../../util/shuffleArray"; import { EQUATION_EDITOR_AUTO_COMMANDS, EQUATION_EDITOR_AUTO_OPERATORS, ThemeContext } from "../../config/config"; import { stagingProp } from "../../util/addStagingProperty"; import { parseMatrixTex } from "../../util/parseMatrixTex"; +import { parseTableTex } from "../../util/parseTableTex"; class ProblemInput extends React.Component { static contextType = ThemeContext; @@ -22,6 +24,8 @@ class ProblemInput extends React.Component { this.equationRef = createRef() this.onEquationChange = this.onEquationChange.bind(this) + + this.questionAnswer = this.props.stepAnswer } componentDidMount() { @@ -30,6 +34,11 @@ class ProblemInput extends React.Component { console.log('automatically determined matrix input to be the correct problem type') } + console.debug('problem', this.props.step, 'seed', this.props.seed) + if (this.isTableInput()) { + console.log('automatically determined table input to be the correct problem type') + } + const mqDisplayArea = this.equationRef?.current?.querySelector(".mq-editable-field > .mq-root-block") if (mqDisplayArea != null) { mqDisplayArea.ariaHidden = true @@ -52,6 +61,17 @@ class ProblemInput extends React.Component { } } + isTableInput() { + if (this.props.step?.stepAnswer) { + return this.props.step?.problemType !== "MultipleChoice" && + /\\begin{[a-zA-Z]?tabular}/.test(this.props.step.stepAnswer[0]) + } + if (this.props.step?.hintAnswer) { + return this.props.step?.problemType !== "MultipleChoice" && + /\\begin{[a-zA-Z]?tabular}/.test(this.props.step.hintAnswer[0]) + } + } + onEquationChange(eq) { const containerEl = this.equationRef?.current const eqContentEl = this.equationRef?.current?.querySelector(".mq-editable-field") @@ -75,6 +95,17 @@ class ProblemInput extends React.Component { } this.props.setInputValState(eq) } + + + getDimAndHeaders(correctAnswer) { + const temp = correctAnswer; + const TotalRows = correctAnswer.split("\\\\").length; + const numCols = correctAnswer.split("\\\\").join("&").split("&").length / TotalRows; + const headers = temp.match(/([a-zA-z]+)/g).slice(2,2+numCols) + const numRows = TotalRows - 1; + return {numRows, numCols, headers} + } + render() { const { classes, state, index } = this.props; @@ -87,8 +118,15 @@ class ProblemInput extends React.Component { problemType = "MatrixInput" } + if (this.isTableInput()) { + problemType = "TableInput" + } + + return ( + {/* {console.log("tbl",parseTableTex(correctAnswer))} */} + {/* {console.log("mx", parseMatrixTex(correctAnswer))} */} {(problemType === "TextBox" && this.props.step.answerType !== "string") && ( @@ -171,6 +209,22 @@ class ProblemInput extends React.Component { } : {}} /> )} + {problemType === "TableInput" && ( + this.props.setInputValState(newVal)} + numRows={this.getDimAndHeaders(correctAnswer).numRows} + numCols={this.getDimAndHeaders(correctAnswer).numCols} + headers= {this.getDimAndHeaders(correctAnswer).headers} + context={this.props.context} + classes={this.props.classes} + index={index} + {...(use_expanded_view && debug) ? { + defaultValue: parseTableTex(correctAnswer)[0] + } : {}} + /> + + + )}
@@ -178,6 +232,7 @@ class ProblemInput extends React.Component {
+ ) } diff --git a/src/ProblemLayout/ProblemInput/TableInput.js b/src/ProblemLayout/ProblemInput/TableInput.js new file mode 100644 index 00000000000..baf913988c2 --- /dev/null +++ b/src/ProblemLayout/ProblemInput/TableInput.js @@ -0,0 +1,145 @@ +import React, { createRef } from "react"; +import './GridInput.css' +import Button from "@material-ui/core/Button"; +import clsx from 'clsx'; +import EquationEditor from "equation-editor-react"; +import { Box,TextField } from "@material-ui/core"; +import { EQUATION_EDITOR_AUTO_COMMANDS, EQUATION_EDITOR_AUTO_OPERATORS } from "../../config/config"; +import { stagingProp } from "../../util/addStagingProperty"; + +class TableInput extends React.Component { + + constructor(props) { + + super(props); + this.state = { + gridState: props.defaultValue || this.genEmptyGrid(props.numRows, props.numCols), + fer: Math.random() + }; + this.gridRef = createRef() + this.clearCells = this.clearCells.bind(this) + this.numRows = props.numRows; + this.numCols = props.numCols; + this.headers = props.headers; + this.renderTabular = props.renderTabular; + } + + + genEmptyGrid = (numRows, numCols) => new Array(numRows) + .fill(0) + .map(_ => new Array(numCols).fill("")) + + cellFieldChange(str, idx, jdx) { + const gridState = this.state.gridState + gridState[idx][jdx] = str; + + this.setState({ + gridState + }, () => { + this.props.onChange(JSON.stringify(this.state.gridState)) + }) + } + + clearCells(evt) { + if (evt != null && evt.type === 'submit') { + evt.preventDefault() + } + + this.setState({ + gridState: this.genEmptyGrid(this.numRows, this.numCols), + fer: Math.random() + }, () => { + this.props.onChange(JSON.stringify(this.state.gridState)) + }) + + } + + + render() { + const { classes, index } = this.props; + + const { gridState, fer } = this.state; + + const revealClearButton = gridState.reduce((acc, cur, _) => + acc + cur.reduce((_acc, _cur, __) => + _acc + _cur.length, 0 + ), 0 + ) > 0; // only reveal the clear button if there is at least something in a cell + + return ( +
+ + {console.log(gridState[0])} + { + gridState[0].map((row, idx) => { + return ( + + ) + }) + } + { + gridState.map((row, idx) => + row.map((val, jdx) => { + return ( +
+ this.cellFieldChange(str, idx, jdx)} + style={{ width: "100%" }} + autoCommands={EQUATION_EDITOR_AUTO_COMMANDS} + autoOperatorNames={EQUATION_EDITOR_AUTO_OPERATORS} + /> + +
+ + ) + }) + ) + } + +
+ + + +
+ ) + } + +} + + +export default TableInput; diff --git a/src/ProblemLayout/commonStyles.js b/src/ProblemLayout/commonStyles.js index 2af2301ee2e..836dcce79a7 100644 --- a/src/ProblemLayout/commonStyles.js +++ b/src/ProblemLayout/commonStyles.js @@ -145,6 +145,27 @@ const styles = theme => ({ paddingBottom: 5 } }, + textTblLatex: { + borderRadius: "0px", + outline: "1px solid #c4c4c4", + '&:hover': { + border: "1px solid #000000", + }, + '&:focus-within': { + border: "2px solid #3f51b5", + }, + height: 50, + width: '100%', + '& > .mq-editable-field': { + display: 'table', + tableLayout: 'fixed' + }, + '& > * > *[mathquill-block-id]': { + height: 50, + display: 'table-cell', + paddingBottom: 5 + } + }, textBoxLatexIncorrect: { boxShadow: "0 0 0.75pt 0.75pt red", '&:focus-within': { diff --git a/src/ProblemLogic/checkAnswer.js b/src/ProblemLogic/checkAnswer.js index 23ebfd48350..d4e1607aeb3 100644 --- a/src/ProblemLogic/checkAnswer.js +++ b/src/ProblemLogic/checkAnswer.js @@ -2,6 +2,7 @@ import { variabilize } from './variabilize.js'; import insert from "../util/strInsert"; import { parseMatrixTex } from "../util/parseMatrixTex"; import { IS_DEVELOPMENT, IS_STAGING_OR_DEVELOPMENT } from "../util/getBuildType"; +import { parseTableTex } from '../util/parseTableTex.js'; const KAS = require('../kas.js'); @@ -63,6 +64,8 @@ function parse(_string) { function checkAnswer(attempt, actual, answerType, precision, variabilization) { let parsed = attempt.replace(/\s+/g, ''); + console.debug(`attempt: ${attempt} vs. actual:`, actual) + console.log(parsed) if (variabilization) { actual = actual.map((actualAns) => variabilize(actualAns, variabilization)); } @@ -93,6 +96,26 @@ function checkAnswer(attempt, actual, answerType, precision, variabilization) { }) return [attempt, correctAnswer] + } else if (/\\begin{[a-zA-Z]?tabular}/.test(actual)){ + console.debug(`attempt: ${attempt} vs. actual:`, actual) + const studentMatrix = JSON.parse(attempt) + const solutionMatrices = parseTableTex(actual); + + console.debug('solutions: ', solutionMatrices) + correctAnswer = solutionMatrices.some(matrix => { + return matrix.reduce((acc, row, idx) => acc && row.reduce((_acc, cell, jdx) => { + const _studentRow = studentMatrix[idx] || [] + const _studentCell = _studentRow[jdx] || "" + const _studentExpr = parse(_studentCell).expr + + const _solExpr = parse(cell).expr + + return _acc && KAS.compare(_studentExpr, _solExpr).equal + }, true), true) + }) + + return [attempt, correctAnswer] + } else { if (IS_STAGING_OR_DEVELOPMENT) { console.debug("Using KAS to compare answer with solution", attempt, actual) diff --git a/src/ProblemLogic/renderText.js b/src/ProblemLogic/renderText.js index caacddf5dcc..05d4302b4b2 100644 --- a/src/ProblemLogic/renderText.js +++ b/src/ProblemLogic/renderText.js @@ -5,6 +5,8 @@ import { variabilize, chooseVariables } from './variabilize.js'; import Spacer from "../Components/_General/Spacer"; import ErrorBoundary from "../Components/_General/ErrorBoundary"; import RenderMedia from "../Components/_General/RenderMedia"; +import { Box,TextField } from "@material-ui/core"; +import { parseTableTex, parseTableHeaders } from '../util/parseTableTex.js'; function renderText(text, problemID, variabilization) { if (typeof text !== 'string') { @@ -18,9 +20,80 @@ function renderText(text, problemID, variabilization) { if (variabilization) { result = variabilize(text, variabilization); } - const lines = result.split("\\n"); return lines.map((line, idx) => { + if (text.includes('tabular')) { + const headers = parseTableHeaders(text) + const answers = parseTableTex(text)[0] + const parsedResult = headers.concat(answers) + const numCols = headers[0].length + // const numRows = parsedResult.length + // const EmptyGrid = new Array(numRows - 1).fill(0).map(_ => new Array(numCols).fill("")) + return
+ + { + parsedResult[0].map((row, idx) => { + return ( + + ) + }) + } + + + { + parsedResult.slice(1).map((row, idx) => + row.map((val, jdx) => { + return ( + + + ) + }) + ) + } + +
+ } /** * If line has LaTeX, split by the "&&" delimiter to separate plain text from LaTeX * @type {(string | JSX.Element)[]} @@ -56,6 +129,7 @@ function renderText(text, problemID, variabilization) { } return lineParts; }) + } /** diff --git a/src/ProblemPool/agtesttbl1/agtesttbl1.json b/src/ProblemPool/agtesttbl1/agtesttbl1.json new file mode 100644 index 00000000000..73d29c0a979 --- /dev/null +++ b/src/ProblemPool/agtesttbl1/agtesttbl1.json @@ -0,0 +1,9 @@ +{ + "id": "agtesttbl1", + "title": "Find two way table", + "body": "Find two way table", + "variabilization": {}, + "oer": "https://openstax.org/", + "lesson": "7.5 Matrices and Matrix Operations", + "courseName": "Openstax: College Algebra" +} \ No newline at end of file diff --git a/src/ProblemPool/agtesttbl1/steps/agtesttbl1a/agtesttbl1a.json b/src/ProblemPool/agtesttbl1/steps/agtesttbl1a/agtesttbl1a.json new file mode 100644 index 00000000000..2684c1a75c6 --- /dev/null +++ b/src/ProblemPool/agtesttbl1/steps/agtesttbl1a/agtesttbl1a.json @@ -0,0 +1,13 @@ +{ + "id": "agtesttbl1a", + "stepAnswer": [ + "$$\\begin{tabular} apple & sugar \\\\ -6 & -4 \\\\ 1 & 3 \\end{tabular}$$" + ], + "problemType": "TextBox", + "stepTitle": "enter table with -6, -4, 1, 3", + "stepBody": "", + "answerType": "arithmetic", + "variabilization": {}, + "answerLatex": "enter table with -6, -4, 1, 3" +} + diff --git a/src/ProblemPool/agtesttbl1/steps/agtesttbl1a/tutoring/agtesttbl1aDefaultPathway.json b/src/ProblemPool/agtesttbl1/steps/agtesttbl1a/tutoring/agtesttbl1aDefaultPathway.json new file mode 100644 index 00000000000..65d08b4e65e --- /dev/null +++ b/src/ProblemPool/agtesttbl1/steps/agtesttbl1a/tutoring/agtesttbl1aDefaultPathway.json @@ -0,0 +1,10 @@ +[ + { + "id": "agtesttbl1a-h1", + "type": "hint", + "dependencies": [], + "title": "Multiplying a Matrix by a Scalar", + "text": "To multiply a matrix A by a scaler C, multiply each entry in A by C. $$\\begin{tabular} apple & sugar \\\\ -4 & \\\\ 1 & 3 \\end{tabular}$$", + "variabilization": {} + } +] \ No newline at end of file diff --git a/src/ProblemPool/agtesttbl2/agtesttbl2.json b/src/ProblemPool/agtesttbl2/agtesttbl2.json new file mode 100644 index 00000000000..2869ea1b8e0 --- /dev/null +++ b/src/ProblemPool/agtesttbl2/agtesttbl2.json @@ -0,0 +1,9 @@ +{ + "id": "agtesttbl2", + "title": "Find two way table", + "body": "Find two way table", + "variabilization": {}, + "oer": "https://openstax.org/", + "lesson": "7.5 Matrices and Matrix Operations", + "courseName": "Openstax: College Algebra" +} \ No newline at end of file diff --git a/src/ProblemPool/agtesttbl2/steps/agtesttbl2a/agtesttbl2a.json b/src/ProblemPool/agtesttbl2/steps/agtesttbl2a/agtesttbl2a.json new file mode 100644 index 00000000000..8a24f06883d --- /dev/null +++ b/src/ProblemPool/agtesttbl2/steps/agtesttbl2a/agtesttbl2a.json @@ -0,0 +1,12 @@ +{ + "id": "agtesttbl2a", + "stepAnswer": [ + "$$\\begin{tabular} apple & sugarandhoneyand & candy \\\\ -6 & -4 & 3\\\\ 1 & 3 & 2\\end{tabular}$$" + ], + "problemType": "TextBox", + "stepTitle": "enter table with -6, -4, 3, 1, 3, 2", + "stepBody": "", + "answerType": "arithmetic", + "variabilization": {}, + "answerLatex": "enter table with -6, -4, 3, 1, 3, 2" +} diff --git a/src/ProblemPool/agtesttbl2/steps/agtesttbl2a/tutoring/agtesttbl2aDefaultPathway.json b/src/ProblemPool/agtesttbl2/steps/agtesttbl2a/tutoring/agtesttbl2aDefaultPathway.json new file mode 100644 index 00000000000..dfdf0ee8580 --- /dev/null +++ b/src/ProblemPool/agtesttbl2/steps/agtesttbl2a/tutoring/agtesttbl2aDefaultPathway.json @@ -0,0 +1,10 @@ +[ + { + "id": "agtesttbl2a-h1", + "type": "hint", + "dependencies": [], + "title": "Multiplying a Matrix by a Scalar", + "text": "To multiply a matrix A by a scaler C, multiply each entry in A by C.", + "variabilization": {} + } +] \ No newline at end of file diff --git a/src/ProblemPool/stepfiles.txt b/src/ProblemPool/stepfiles.txt index a12abefa12b..bf40bfe5dd3 100644 --- a/src/ProblemPool/stepfiles.txt +++ b/src/ProblemPool/stepfiles.txt @@ -1,3 +1,5 @@ + agtesttbl1a: ["matrices_and_matrix_operations"], + agtesttbl2a: ["matrices_and_matrix_operations"], a6f9727real1a: ["realnumber"], a6f9727real1b: ["realnumber"], a6f9727real1c: ["realnumber"], diff --git a/src/config/skillModel.js b/src/config/skillModel.js index f553f2acca1..3875ba7189e 100644 --- a/src/config/skillModel.js +++ b/src/config/skillModel.js @@ -1,5 +1,6 @@ const skillModel = { - + agtesttbl1a: [`matrices_and_matrix_operations`], + agtesttbl2a: [`matrices_and_matrix_operations`], a6f9727real1a: [`classifying_a_real_number`], a6f9727real1b: [`classifying_a_real_number`], a6f9727real1c: [`classifying_a_real_number`], diff --git a/src/tools/preprocessProblemPool.js b/src/tools/preprocessProblemPool.js index ac3b894dadc..67cb337d0bb 100644 --- a/src/tools/preprocessProblemPool.js +++ b/src/tools/preprocessProblemPool.js @@ -110,8 +110,9 @@ const staticFiguresPath = path.join(__dirname, '..', '..', 'public', 'static', ' } problem.steps = await Promise.all( - stepDirs.map( - async stepDir => { + stepDirs + .filter(s => !(s.toString().startsWith("_") || s.toString().startsWith("."))) // ignore directories that start with _ or . + .map(async stepDir => { const stepName = stepDir.toString() const stepPath = path.join(problemPath, 'steps', stepName) diff --git a/src/util/parseTableTex.js b/src/util/parseTableTex.js new file mode 100644 index 00000000000..c54f9e3b015 --- /dev/null +++ b/src/util/parseTableTex.js @@ -0,0 +1,33 @@ +function parseTableTex(solTex) { + const solutionTable = [] + + ;(Array.isArray(solTex) ? solTex : [solTex]).forEach(sol => { + const _start = sol.indexOf("tabular} ") + "tabular} ".length + const _end = sol.indexOf("\\end{") + let _solutionTable = sol + .substring(_start, _end) + .trim() + .split("\\\\") + .map(row => row.split("&").map(val => val.trim())) + solutionTable.push(_solutionTable) + }) + return Array(solutionTable[0].slice(1)); +} + +function parseTableHeaders(solTex) { + const solutionTable = [] + + ;(Array.isArray(solTex) ? solTex : [solTex]).forEach(sol => { + const _start = sol.indexOf("tabular} ") + "tabular} ".length + const _end = sol.indexOf("\\end{") + let _solutionTable = sol + .substring(_start, _end) + .trim() + .split("\\\\") + .map(row => row.split("&").map(val => val.trim())) + solutionTable.push(_solutionTable) + }) + return Array(solutionTable[0][0]); +} + +export { parseTableTex, parseTableHeaders } \ No newline at end of file