diff --git a/src/components/Firebase.js b/src/components/Firebase.js index ed59bff99a8..72fd647b119 100644 --- a/src/components/Firebase.js +++ b/src/components/Firebase.js @@ -216,7 +216,8 @@ class Firebase { courseName, hintType, dynamicHint, - bioInfo + bioInfo, + keylog_data ) { if (!DO_LOG_DATA) { console.debug("Not using firebase for logging (2)"); @@ -245,9 +246,10 @@ class Firebase { hintType, dynamicHint, bioInfo, + keylog_data, }; - // return this.writeData(GPTExperimentOutput, data); - return this.writeData(problemSubmissionsOutput, data); + return this.writeData(GPTExperimentOutput, data); + // return this.writeData(problemSubmissionsOutput, data); } hintLog( @@ -288,8 +290,8 @@ class Firebase { dynamicHint, bioInfo, }; - // return this.writeData(GPTExperimentOutput, data); - return this.writeData(problemSubmissionsOutput, data); + return this.writeData(GPTExperimentOutput, data); + // return this.writeData(problemSubmissionsOutput, data); } mouseLog(payload) { diff --git a/src/components/keystroke/ActivityDetector.jsx b/src/components/keystroke/ActivityDetector.jsx new file mode 100644 index 00000000000..6db60fdf943 --- /dev/null +++ b/src/components/keystroke/ActivityDetector.jsx @@ -0,0 +1,546 @@ +export function ActivityDetector( + keylog, + startSelect, + endSelect, + ActivityCancel, + TextChangeCancel +) { + if (keylog.TextContent.length >= 1) { + // textchange //activity + let textNow = String(keylog.TextContent.slice(-2, -1)); // used to guess what happens to the text at the current event. + let change = + String(keylog.TextContent.slice(-1)).length - + String(keylog.TextContent.slice(-2, -1)).length; + + if (change === 0) { + let start = parseInt(startSelect.slice(-2, -1)); + let end = parseInt(endSelect.slice(-2, -1)); + + let start1 = parseInt(startSelect.slice(-2, -1)); + let end1 = parseInt(endSelect.slice(-2, -1)); + let start2 = parseInt(startSelect.slice(-1)); + let end2 = parseInt(endSelect.slice(-1)); + let Text1 = String(keylog.TextContent.slice(-2, -1)).slice( + start1, + end1 + ); + let Text2 = String(keylog.TextContent.slice(-1)).slice( + start2, + end2 + ); + + if (start1 < end1 && start2 < end2 && Text1 === Text2) { + // when move is detected + + if (start2 > start1 && end2 > end1) { + //front move + let movedText = String(keylog.TextContent.slice(-1)).slice( + start2, + end2 + ); + keylog.TextChange.push(movedText); + TextChangeCancel.push(movedText); + keylog.Activity.push( + `Move From [${start1}, ${end1}] To [${start2}, ${end2}]` + ); + ActivityCancel.push( + `Move From [${start1}, ${end1}] To [${start2}, ${end2}]` + ); + + textNow = + String(textNow.slice(0, start1)) + + String(textNow.slice(end1, end2)) + + movedText + + textNow.slice(end2, textNow.length); + } else if (start2 < start1 && end2 < end1) { + //back move + + let movedText = String(keylog.TextContent.slice(-1)).slice( + start2, + end2 + ); + keylog.TextChange.push(movedText); + TextChangeCancel.push(movedText); + keylog.Activity.push( + `Move From [${start1}, ${end1}] To [${start2}, ${end2}]` + ); + ActivityCancel.push( + `Move From [${start1}, ${end1}] To [${start2}, ${end2}]` + ); + textNow = + textNow.slice(0, start2) + + movedText + + textNow.slice(start2, start1) + + textNow.slice(end1, textNow.length); + } else if (start2 === start1 && end2 === end1) { + // no move in the end + + keylog.TextChange.push("NoChange"); + TextChangeCancel.push("NoChange"); + keylog.Activity.push("Nonproduction"); + ActivityCancel.push("Nonproduction"); + } else if (start2 < start1 && end2 > end1) { + // mistakenly select all the text + keylog.TextChange.push("NoChange"); + TextChangeCancel.push("NoChange"); + keylog.Activity.push("Nonproduction"); + ActivityCancel.push("Nonproduction"); + } + } else if ( + start1 === end1 && + start2 < end2 && + String(textNow) !== String(keylog.TextContent.slice(-1)) + ) { + // cancel this move + let changeN = parseInt(ActivityCancel.length); + + for (let i = 0; i <= changeN - 1; i++) { + let num = changeN - 1 - i; + let activity = String(ActivityCancel[num]); + if (activity.startsWith("Move")) { + keylog.TextChange.push(String(TextChangeCancel[num])); + TextChangeCancel.splice(num, 1); + let index1 = activity.indexOf("["); + let index2 = activity.indexOf("]"); + let index3 = activity.lastIndexOf("["); + let secondmove = activity.slice(index1, index2 + 1); + let firstmove = activity.slice(index3); + keylog.Activity.push( + `Move From ${firstmove} To ${secondmove}` + ); + ActivityCancel.splice(num, 1); + textNow = String(keylog.TextContent.slice(-1)); + i = changeN - 1; + } + + if (activity.startsWith("Replace")) { + let middleindex = String( + TextChangeCancel[num] + ).lastIndexOf(" => "); + let substitute = String(TextChangeCancel[num]).slice( + 5, + middleindex + ); + let replace = String(TextChangeCancel[num]).slice( + middleindex + 4 + ); + keylog.TextChange.push(`${replace} => ${substitute}`); + keylog.Activity.push("Replace"); + TextChangeCancel.splice(num, 1); + ActivityCancel.splice(num, 1); + textNow = String(keylog.TextContent.slice(-1)); + i = changeN - 1; + } + } + } else { + if (textNow === String(keylog.TextContent.slice(-1))) { + keylog.TextChange.push("NoChange"); + keylog.Activity.push("Nonproduction"); + } else { + // start the else condition + if (start < end) { + // replace activity: replace n chracters with n new characters + textNow = + String(keylog.TextContent.slice(-2, -1)).slice( + 0, + start + ) + + String(keylog.TextContent.slice(-1)).substr( + start, + end + change - start + ) + + String(keylog.TextContent.slice(-2, -1)).slice( + end, + String(keylog.TextContent.slice(-2, -1)).length + ); + + let replaced = String( + keylog.TextContent.slice(-2, -1) + ).substr(start, end - start); + let substitute = String( + keylog.TextContent.slice(-1) + ).substr(start, end + change - start); + if (textNow === String(keylog.TextContent.slice(-1))) { + if (replaced !== substitute) { + keylog.TextChange.push( + `${replaced} => ${substitute}` + ); + TextChangeCancel.push( + `${replaced} => ${substitute}` + ); + keylog.Activity.push("Replace"); + ActivityCancel.push("Replace"); + } else { + keylog.TextChange.push("NoChange"); + keylog.Activity.push("Nonproduction"); + } + } else { + AutoCorrectionDector(keylog); + // textNow adjustment + textNow = String(keylog.TextContent.slice(-1)); + } + } + if (start === end) { + // irregular replacement + AutoCorrectionDector(keylog); + // textNow adjustment + textNow = String(keylog.TextContent.slice(-1)); + } + } + } + } + + if (change === 1) { + let start = parseInt(startSelect.slice(-2, -1)); + let end = parseInt(endSelect.slice(-2, -1)); + + let index = parseInt(endSelect.slice(-1)); + + textNow = + textNow.slice(0, index - 1) + + String(keylog.TextContent.slice(-1))[index - 1] + + textNow.slice(index - 1); + + if (textNow === String(keylog.TextContent.slice(-1))) { + // just a common input + keylog.TextChange.push( + String(keylog.TextContent.slice(-1))[index - 1] + ); + keylog.Activity.push("Input"); + } else { + /// replace n characters with n+1 new characters + + if (start < end) { + /// regular paste activity + textNow = + String(keylog.TextContent.slice(-2, -1)).slice( + 0, + start + ) + + String(keylog.TextContent.slice(-1)).substr( + start, + end + change - start + ) + + String(keylog.TextContent.slice(-2, -1)).slice( + end, + String(keylog.TextContent.slice(-2, -1)).length + ); + + let replaced = String( + keylog.TextContent.slice(-2, -1) + ).substr(start, end - start); + let substitute = String( + keylog.TextContent.slice(-1) + ).substr(start, end + change - start); + + if (textNow === String(keylog.TextContent.slice(-1))) { + ReplaceDetector(replaced, substitute, keylog); + } else { + //irregular paste activity. + AutoCorrectionDector(keylog); + // textNow adjustment + textNow = String(keylog.TextContent.slice(-1)); + } + } + + if (start === end) { + AutoCorrectionDector(keylog); + // textNow adjustment + textNow = String(keylog.TextContent.slice(-1)); + } + } + } + + if (change > 1) { + let start = parseInt(startSelect.slice(-2, -1)); + let end = parseInt(endSelect.slice(-2, -1)); + + let rangeStart = parseInt(keylog.CursorPosition.slice(-2, -1)); // the time beyond the last time where the cursor is. + let rangeEnd = parseInt(keylog.CursorPosition.slice(-1)); // the last time where the cursor is. + let newlyAdded = String(keylog.TextContent.slice(-1)).slice( + rangeStart, + rangeEnd + ); + + textNow = + textNow.slice(0, rangeStart) + + String(keylog.TextContent.slice(-1)).slice( + rangeStart, + rangeEnd + ) + + textNow.slice(rangeStart); + + if (textNow === String(keylog.TextContent.slice(-1))) { + // Paste more than 1 character + keylog.TextChange.push(newlyAdded); + keylog.Activity.push("Paste"); + } else { + // replace activity + if (start < end) { + // regular replace activity: replace n characters with n+m characters + textNow = + String(keylog.TextContent.slice(-2, -1)).slice( + 0, + start + ) + + String(keylog.TextContent.slice(-1)).substr( + start, + end + change - start + ) + + String(keylog.TextContent.slice(-2, -1)).slice( + end, + String(keylog.TextContent.slice(-2, -1)).length + ); + + let replaced = String( + keylog.TextContent.slice(-2, -1) + ).substr(start, end - start); + let substitute = String( + keylog.TextContent.slice(-1) + ).substr(start, end + change - start); + + if (textNow === String(keylog.TextContent.slice(-1))) { + ReplaceDetector(replaced, substitute, keylog); + } else { + //irregular paste activity. + AutoCorrectionDector(keylog); + // textNow adjustment + textNow = String(keylog.TextContent.slice(-1)); + } + } + + if (start === end) { + // irregular replace activity + AutoCorrectionDector(keylog); + // textNow adjustment + textNow = String(keylog.TextContent.slice(-1)); + } + } + } + + if (change === -1) { + let start = parseInt(startSelect.slice(-2, -1)); + let end = parseInt(endSelect.slice(-2, -1)); + + let index = parseInt(keylog.CursorPosition.slice(-2, -1)); + let textinfo = String(keylog.TextContent.slice(-2, -1)); + let deleted = ""; + + if ( + String(keylog.Output.slice(-2, -1)) === "Delete" && + start === end + ) { + deleted = textinfo[index]; + textNow = textNow.slice(0, index) + textNow.slice(index + 1); + } else { + deleted = textinfo[index - 1]; // curosor position and character position are different + textNow = textNow.slice(0, index - 1) + textNow.slice(index); + } + + if (textNow === String(keylog.TextContent.slice(-1))) { + // backspace or delete to remove just one character + keylog.TextChange.push(deleted); + keylog.Activity.push("Remove/Cut"); + } else { + if (start < end) { + // regular replace activity: replace n characters with n-1 new characters + textNow = + String(keylog.TextContent.slice(-2, -1)).slice( + 0, + start + ) + + String(keylog.TextContent.slice(-1)).substr( + start, + end + change - start + ) + + String(keylog.TextContent.slice(-2, -1)).slice( + end, + String(keylog.TextContent.slice(-2, -1)).length + ); + + let replaced = String( + keylog.TextContent.slice(-2, -1) + ).substr(start, end - start); + let substitute = String( + keylog.TextContent.slice(-1) + ).substr(start, end + change - start); + + if (textNow === String(keylog.TextContent.slice(-1))) { + ReplaceDetector(replaced, substitute, keylog); + } else { + //irregular paste activity. + AutoCorrectionDector(keylog); + // textNow adjustment + textNow = String(keylog.TextContent.slice(-1)); + } + } + + if (start === end) { + AutoCorrectionDector(keylog); + // textNow adjustment + textNow = String(keylog.TextContent.slice(-1)); + } + } + } + + if (change < -1) { + let start = parseInt(startSelect.slice(-2, -1)); + let end = parseInt(endSelect.slice(-2, -1)); + + let rangeStart = parseInt(startSelect.slice(-2, -1)); + let rangeEnd = parseInt(endSelect.slice(-2, -1)); + + let textinfo = String(keylog.TextContent.slice(-2, -1)); + let deleted = textinfo.slice(rangeStart, rangeEnd); + + textNow = textNow.slice(0, rangeStart) + textNow.slice(rangeEnd); + + if (textNow === String(keylog.TextContent.slice(-1))) { + // delete more than 1 characters + keylog.TextChange.push(deleted); + keylog.Activity.push("Remove/Cut"); + } else { + if (start < end) { + // regular replace activity: replace n characters with n-m (m 0 && substitute.length > 0) { + if (replaced !== substitute) { + keylog.TextChange.push(`${replaced} => ${substitute}`); + keylog.Activity.push("AutoCorrectionReplace"); + } else { + keylog.TextChange.push("NoChange"); + keylog.Activity.push("Nonproduction"); + } + } else if (replaced.length > 0 && substitute.length === 0) { + keylog.TextChange.push(replaced); + keylog.Activity.push("AutoCorrectionRemove/Cut"); + } else if (replaced.length === 0 && substitute.length > 0) { + keylog.TextChange.push(substitute); + keylog.Activity.push("AutocorrectionPaste"); + } else { + keylog.TextChange.push("NoChange"); + keylog.Activity.push("Nonproduction"); + } + + // cursorPosition adjustment + let thisPosition = newTextChangeEnd; + keylog.CursorPosition.pop(); // remove the last value + keylog.CursorPosition.push(thisPosition); // add the new position + } else { + keylog.TextChange.push("NoChange"); + keylog.Activity.push("Nonproduction"); + } +} + +// this function is used to detect replace events in different conditions +function ReplaceDetector(replaced, substitute, keylog) { + if (replaced.length > 0 && substitute.length > 0) { + if (replaced !== substitute) { + keylog.TextChange.push(`${replaced} => ${substitute}`); + keylog.Activity.push("Replace"); + } else { + keylog.TextChange.push("NoChange"); + keylog.Activity.push("Nonproduction"); + } + } else if (replaced.length > 0 && substitute.length === 0) { + keylog.TextChange.push(replaced); + keylog.Activity.push("Remove/Cut"); + } else if (replaced.length === 0 && substitute.length > 0) { + keylog.TextChange.push(substitute); + keylog.Activity.push("Paste"); + } else { + keylog.TextChange.push("NoChange"); + keylog.Activity.push("Nonproduction"); + } +} diff --git a/src/components/problem-input/ProblemInput.js b/src/components/problem-input/ProblemInput.js index 64c1885b7a8..80c03286b29 100644 --- a/src/components/problem-input/ProblemInput.js +++ b/src/components/problem-input/ProblemInput.js @@ -7,9 +7,13 @@ import MatrixInput from "./MatrixInput"; import { renderText } from "../../platform-logic/renderText"; import clsx from "clsx"; import "mathlive"; -import './ProblemInput.css' +import "./ProblemInput.css"; import { shuffleArray } from "../../util/shuffleArray"; -import { EQUATION_EDITOR_AUTO_COMMANDS, EQUATION_EDITOR_AUTO_OPERATORS, ThemeContext } from "../../config/config"; +import { + EQUATION_EDITOR_AUTO_COMMANDS, + EQUATION_EDITOR_AUTO_OPERATORS, + ThemeContext, +} from "../../config/config"; import { stagingProp } from "../../util/addStagingProperty"; import { parseMatrixTex } from "../../util/parseMatrixTex"; @@ -19,199 +23,282 @@ class ProblemInput extends React.Component { constructor(props) { super(props); - this.equationRef = createRef() + this.equationRef = createRef(); + this.finalAnswerAreaRef = props.finalAnswerAreaRef; + + this.onEquationChange = this.onEquationChange.bind(this); - this.onEquationChange = this.onEquationChange.bind(this) - this.state = { value: "", }; } componentDidMount() { - console.debug('problem', this.props.step, 'seed', this.props.seed) + console.debug("problem", this.props.step, "seed", this.props.seed); if (this.isMatrixInput()) { - console.log('automatically determined matrix input to be the correct problem type') + console.log( + "automatically determined matrix input to be the correct problem type" + ); } - const mqDisplayArea = this.equationRef?.current?.querySelector(".mq-editable-field > .mq-root-block") + const mqDisplayArea = this.equationRef?.current?.querySelector( + ".mq-editable-field > .mq-root-block" + ); if (mqDisplayArea != null) { - mqDisplayArea.ariaHidden = true + mqDisplayArea.ariaHidden = true; } - const textareaEl = this.equationRef?.current?.querySelector(".mq-textarea > textarea") + const textareaEl = this.equationRef?.current?.querySelector( + ".mq-textarea > textarea" + ); if (textareaEl != null) { - textareaEl.ariaLabel = `Answer question number ${this.props.index} here` + textareaEl.ariaLabel = `Answer question number ${this.props.index} here`; } } isMatrixInput() { if (this.props.step?.stepAnswer) { - return this.props.step?.problemType !== "MultipleChoice" && + return ( + this.props.step?.problemType !== "MultipleChoice" && /\\begin{[a-zA-Z]?matrix}/.test(this.props.step.stepAnswer[0]) + ); } if (this.props.step?.hintAnswer) { - return this.props.step?.problemType !== "MultipleChoice" && + return ( + this.props.step?.problemType !== "MultipleChoice" && /\\begin{[a-zA-Z]?matrix}/.test(this.props.step.hintAnswer[0]) + ); } } onEquationChange(eq) { - const containerEl = this.equationRef?.current - const eqContentEl = this.equationRef?.current?.querySelector(".mq-editable-field") + const containerEl = this.equationRef?.current; + const eqContentEl = + this.equationRef?.current?.querySelector(".mq-editable-field"); - const textareaEl = this.equationRef?.current?.querySelector(".mq-textarea > textarea") + const textareaEl = this.equationRef?.current?.querySelector( + ".mq-textarea > textarea" + ); if (textareaEl != null) { // console.debug("not null!", textareaEl) - textareaEl.ariaLabel = `The current value is: ${eq}. Answer question number ${this.props.index} here.` + textareaEl.ariaLabel = `The current value is: ${eq}. Answer question number ${this.props.index} here.`; } if (containerEl != null && eqContentEl != null) { - const eqContainer = eqContentEl.querySelector("*[mathquill-block-id]") + const eqContainer = eqContentEl.querySelector( + "*[mathquill-block-id]" + ); if (eqContainer != null) { - const tallestEqElement = Math.max(...Array.from(eqContainer.childNodes.values()).map(el => el.offsetHeight)) - const newHeight = Math.max(tallestEqElement + 20, 50) + const tallestEqElement = Math.max( + ...Array.from(eqContainer.childNodes.values()).map( + (el) => el.offsetHeight + ) + ); + const newHeight = Math.max(tallestEqElement + 20, 50); containerEl.style.height = `${newHeight}px`; eqContainer.style.height = `${newHeight}px`; } } - this.props.setInputValState(eq) + this.props.setInputValState(eq); } render() { - const { classes, state, index, showCorrectness, allowRetry, variabilization } = this.props; + const { + classes, + state, + index, + showCorrectness, + allowRetry, + variabilization, + } = this.props; const { use_expanded_view, debug } = this.context; let { problemType, stepAnswer, hintAnswer, units } = this.props.step; const keepMCOrder = this.props.keepMCOrder; const keyboardType = this.props.keyboardType; - const problemAttempted = state.isCorrect != null - const correctAnswer = Array.isArray(stepAnswer) ? stepAnswer[0] : hintAnswer[0] - const disableInput = problemAttempted && !allowRetry + const problemAttempted = state.isCorrect != null; + const correctAnswer = Array.isArray(stepAnswer) + ? stepAnswer[0] + : hintAnswer[0]; + const disableInput = problemAttempted && !allowRetry; if (this.isMatrixInput()) { - problemType = "MatrixInput" + problemType = "MatrixInput"; } - + try { window.mathVirtualKeyboard.layouts = [keyboardType]; } catch { window.mathVirtualKeyboard.layouts = ["default"]; } + console.log("this problem: ", problemType, this.props.step.answerType); return ( - - + + - {(problemType === "TextBox" && this.props.step.answerType !== "string") && ( - this.props.setInputValState(evt.target.value)} - style={{"display": "block"}} - value={(use_expanded_view && debug) ? correctAnswer : state.inputVal} - onChange={this.onEquationChange} - autoCommands={EQUATION_EDITOR_AUTO_COMMANDS} - autoOperatorNames={EQUATION_EDITOR_AUTO_OPERATORS} - > - - - )} - {(problemType === "TextBox" && this.props.step.answerType === "string") && ( - this.props.editInput(evt)} - onKeyPress={(evt) => this.props.handleKey(evt)} - InputProps={{ - classes: { - notchedOutline: ((showCorrectness && state.isCorrect !== false && state.usedHints) ? classes.muiUsedHint : null) + {problemType === "TextBox" && + this.props.step.answerType !== "string" && ( + + this.props.setInputValState( + evt.target.value + ) } - }} - {...(use_expanded_view && debug) ? { - defaultValue: correctAnswer - } : {}} - > - - )} - {(problemType === "TextBox" && this.props.step.answerType === "short-essay") && ( - - )} - {(problemType === "MultipleChoice" && keepMCOrder) ? ( + ref={this.finalAnswerAreaRef} + style={{ display: "block" }} + value={ + use_expanded_view && debug + ? correctAnswer + : state.inputVal + } + onChange={this.onEquationChange} + autoCommands={EQUATION_EDITOR_AUTO_COMMANDS} + autoOperatorNames={ + EQUATION_EDITOR_AUTO_OPERATORS + } + > + )} + {problemType === "TextBox" && + this.props.step.answerType === "string" && ( + this.props.editInput(evt)} + onKeyPress={(evt) => this.props.handleKey(evt)} + InputProps={{ + classes: { + notchedOutline: + showCorrectness && + state.isCorrect !== false && + state.usedHints + ? classes.muiUsedHint + : null, + }, + }} + {...(use_expanded_view && debug + ? { + defaultValue: correctAnswer, + } + : {})} + > + )} + {problemType === "TextBox" && + this.props.step.answerType === "short-essay" && ( + + )} + {problemType === "MultipleChoice" && keepMCOrder ? ( this.props.editInput(evt)} choices={[...this.props.step.choices].reverse()} index={index} - {...(use_expanded_view && debug) ? { - defaultValue: correctAnswer - } : {}} - variabilization={variabilization} - /> - ) : - (problemType === "MultipleChoice") && ( - this.props.editInput(evt)} - choices={shuffleArray(this.props.step.choices, this.props.seed)} - index={index} - {...(use_expanded_view && debug) ? { - defaultValue: correctAnswer - } : {}} + {...(use_expanded_view && debug + ? { + defaultValue: correctAnswer, + } + : {})} variabilization={variabilization} /> + ) : ( + problemType === "MultipleChoice" && ( + this.props.editInput(evt)} + choices={shuffleArray( + this.props.step.choices, + this.props.seed + )} + index={index} + {...(use_expanded_view && debug + ? { + defaultValue: correctAnswer, + } + : {})} + variabilization={variabilization} + /> + ) )} {problemType === "GridInput" && ( this.props.setInputValState(newVal)} + onChange={(newVal) => + this.props.setInputValState(newVal) + } numRows={this.props.step.numRows} numCols={this.props.step.numCols} context={this.props.context} classes={this.props.classes} index={index} - {...(use_expanded_view && debug) ? { - defaultValue: parseMatrixTex(correctAnswer)[0] - } : {}} + {...(use_expanded_view && debug + ? { + defaultValue: + parseMatrixTex(correctAnswer)[0], + } + : {})} /> )} {problemType === "MatrixInput" && ( this.props.setInputValState(newVal)} + onChange={(newVal) => + this.props.setInputValState(newVal) + } numRows={this.props.step.numRows} numCols={this.props.step.numCols} context={this.props.context} classes={this.props.classes} index={index} - {...(use_expanded_view && debug) ? { - defaultValue: parseMatrixTex(correctAnswer)[0] - } : {}} + {...(use_expanded_view && debug + ? { + defaultValue: + parseMatrixTex(correctAnswer)[0], + } + : {})} /> )}
- {units && renderText(units, this.context.problemID, variabilization, this.context)} + {units && + renderText( + units, + this.context.problemID, + variabilization, + this.context + )}
- + - ) + ); } } -export default ProblemInput +export default ProblemInput; diff --git a/src/components/problem-layout/ProblemCard.js b/src/components/problem-layout/ProblemCard.js index e6238574f70..e9d767e7738 100644 --- a/src/components/problem-layout/ProblemCard.js +++ b/src/components/problem-layout/ProblemCard.js @@ -29,26 +29,59 @@ import { stagingProp } from "../../util/addStagingProperty"; import ErrorBoundary from "../ErrorBoundary"; import { toastNotifyCompletion, - toastNotifyCorrectness, toastNotifyEmpty + toastNotifyCorrectness, + toastNotifyEmpty, } from "./ToastNotifyCorrectness"; import { joinList } from "../../util/formListString"; import axios from "axios"; -import withTranslation from "../../util/withTranslation.js" +import withTranslation from "../../util/withTranslation.js"; +import { ActivityDetector } from "@components/keystroke/ActivityDetector.jsx"; class ProblemCard extends React.Component { static contextType = ThemeContext; + // keystroke logging class features + userId = 0; + sessionId = "XXX"; + quizId = "YYY"; + endpoint = "https://XXXX"; + + TextareaTouch = false; + taskonset = 0; // set the value as the time when the target question is loaded. + EventID = 0; + startSelect = []; + endSelect = []; + ActivityCancel = []; // to keep track of changes caused by control + z + TextChangeCancel = []; // to keep track of changes caused by control + z + sessionQuiz = this.sessionId + "-" + this.quizId; + keylog = { + //Proprieties + TaskOnSet: [], /// + TaskEnd: [], + PartitionKey: [], + RowKey: [], + EventID: [], //// + EventTime: [], //// + Output: [], //// + CursorPosition: [], //// + TextContent: [], //// + finalAnswer: [], //// + TextChange: [], //// + Activity: [], ///// + FinalProduct: [], ///// + }; + constructor(props, context) { super(props); // console.log("problem lesson props:", props); - this.translate = props.translate + this.translate = props.translate; this.step = props.step; this.index = props.index; this.giveStuFeedback = props.giveStuFeedback; this.giveStuHints = props.giveStuHints; this.unlockFirstHint = props.unlockFirstHint; - this.giveHintOnIncorrect = props.giveHintOnIncorrect + this.giveHintOnIncorrect = props.giveHintOnIncorrect; this.keepMCOrder = props.keepMCOrder; this.keyboardType = props.keyboardType; this.allowRetry = this.giveStuFeedback; @@ -98,8 +131,10 @@ class ProblemCard extends React.Component { // Bottom out hints this.hints.push({ id: this.step.id + "-h" + (this.hints.length + 1), - title: this.translate('hintsystem.answer'), - text: this.translate('hintsystem.answerIs') + this.step.stepAnswer, + title: this.translate("hintsystem.answer"), + text: + this.translate("hintsystem.answerIs") + + this.step.stepAnswer, type: "bottomOut", dependencies: Array.from(Array(this.hints.length).keys()), }); @@ -116,8 +151,10 @@ class ProblemCard extends React.Component { i + "-s" + (hint.subHints.length + 1), - title: this.translate('hintsystem.answer'), - text: this.translate('hintsystem.answerIs') + hint.hintAnswer[0], + title: this.translate("hintsystem.answer"), + text: + this.translate("hintsystem.answerIs") + + hint.hintAnswer[0], type: "bottomOut", dependencies: Array.from( Array(hint.subHints.length).keys() @@ -141,8 +178,119 @@ class ProblemCard extends React.Component { bioInfo: "", enableHintGeneration: true, }; + + // keystroke logging part + this.finalAnswerAreaRef = React.createRef(); + this.submitButtonRef = React.createRef(); } + // keystroke logging functionalities + handleCursor = (refType, keylog, startSelect, endSelect) => { + if (refType == "finalAnswerAreaRef") { + keylog.CursorPosition.push( + this.finalAnswerAreaRef.current.selectionEnd + ); + startSelect.push(this.finalAnswerAreaRef.current.selectionStart); + endSelect.push(this.finalAnswerAreaRef.current.selectionEnd); + } + }; + + logCurrentText = (e) => { + this.keylog.TextContent.push(e.target.value); + }; + + handleKeyDown = (e) => { + let d_press = new Date(); + this.keylog.EventTime.push(d_press.getTime() - this.taskonset); // start time + + this.EventID = this.EventID + 1; + this.keylog.EventID.push(this.EventID); + + // Add a unique RowKey + this.keylog.RowKey.push(this.sessionQuiz + "-" + String(this.EventID)); + + /// when logging space, it is better to use the letter space for the output column + if (e.key === " ") { + this.keylog.Output.push("Space"); + } else if (e.key === "unidentified") { + this.keylog.Output.push("VirtualKeyboardTouch"); + } else { + this.keylog.Output.push(e.key); + } + + this.logCurrentText(e); + this.handleCursor(this.keylog, this.startSelect, this.endSelect); + + // use a customized function to detect and record different activities and the according text changes these activities bring about + ActivityDetector( + this.keylog, + this.startSelect, + this.endSelect, + this.ActivityCancel, + this.TextChangeCancel + ); + // console.log(textNow); + }; + + handleTouch = () => { + this.TextareaTouch = true; + }; + + handleMouseClick = (e) => { + let mouseDown_m = new Date(); + let MouseDownTime = mouseDown_m.getTime() - this.taskonset; + + this.EventID = this.EventID + 1; + this.keylog.EventID.push(this.EventID); + + // Add a unique RowKey + this.keylog.RowKey.push(this.sessionQuiz + "-" + String(this.EventID)); + + //////Start logging for this current click down event + this.keylog.EventTime.push(MouseDownTime); // starttime + if (e.button === 0) { + if (this.TextareaTouch) { + this.keylog.Output.push("TextareaTouch"); + } else { + this.keylog.Output.push("Leftclick"); + } + } else if (e.button === 1) { + if (this.state.TextareaTouch) { + this.keylog.Output.push("TextareaTouch"); + } else { + this.keylog.Output.push("Middleclick"); + } + } else if (e.button === 2) { + if (this.state.TextareaTouch) { + this.keylog.Output.push("TextareaTouch"); + } else { + this.keylog.Output.push("Rightclick"); + } + } else { + if (this.state.TextareaTouch) { + this.keylog.Output.push("TextareaTouch"); + } else { + this.keylog.Output.push("Unknownclick"); + } + } + + this.logCurrentText(e); + // log cursor position + this.handleCursor(this.keylog, this.startSelect, this.endSelect); + /////// use a customized function to detect and record different activities and the according text changes these activities bring about + ActivityDetector( + this.keylog, + this.startSelect, + this.endSelect, + this.ActivityCancel, + this.TextChangeCancel + ); + // set TextareaTouch back as false + this.setState((prevState) => ({ + TextareaTouch: false, + })); + }; + _findHintId = (hints, targetId) => { for (var i = 0; i < hints.length; i++) { if (hints[i].id === targetId) { @@ -179,7 +327,46 @@ class ProblemCard extends React.Component { componentDidMount() { // Start an asynchronous task this.updateBioInfo(); - console.log("student show hints status: ", this.showHints); + const isFinalAnswerAreaRef = this.finalAnswerAreaRef.current; + // console.log("finalAnswer", this.finalAnswerAreaRef); + + const isSubmitButton = + this.submitButtonRef.current && + this.submitButtonRef.current.tagName === "BUTTON"; + + if (isFinalAnswerAreaRef) { + this.finalAnswerAreaRef.current.addEventListener("keydown", (e) => { + console.log("I hear a keydown event."); + if ( + e.key === "Enter" && + !e.ctrlKey && + !e.shiftKey && + !e.altKey && + !e.metaKey + ) { + this.keylog.Output.push("Enter"); + } else { + this.handleKeyDown(e); + } + }); + + this.finalAnswerAreaRef.current.addEventListener( + "touchstart", + this.handleTouch + ); + this.finalAnswerAreaRef.current.addEventListener( + "mousedown", + this.handleMouseClick + ); + } + + if (isSubmitButton) { + this.submitButtonRef.current.addEventListener("click", () => { + this.keylog.Output.push("Click Submit"); + this.EventID = this.EventID + 1; + this.keylog.EventID.push(this.EventID); + }); + } } componentDidUpdate(prevProps) { @@ -196,6 +383,50 @@ class ProblemCard extends React.Component { } } + componentWillUnmount() { + const isFinalAnswerAreaRef = + this.finalAnswerAreaRef.current && + this.finalAnswerAreaRef.current.tagName === "TEXTAREA"; + + const isSubmitButton = + this.submitButtonRef.current && + this.submitButtonRef.current.tagName === "BUTTON"; + + if (isFinalAnswerAreaRef) { + this.finalAnswerAreaRef.current.removeEventListener( + "keydown", + (e) => { + if ( + e.key === "Enter" && + !e.ctrlKey && + !e.shiftKey && + !e.altKey && + !e.metaKey + ) { + this.keylog.Output.push("Remove Enter"); + } else { + this.handleKeyDown(e); + } + } + ); + + this.finalAnswerAreaRef.current.removeEventListener( + "touchstart", + this.handleTouch + ); + this.finalAnswerAreaRef.current.removeEventListener( + "mousedown", + this.handleMouseClick + ); + } + + if (isSubmitButton) { + this.submitButtonRef.current.removeEventListener("click", () => { + this.keylog.Output.push("Remove Submit"); + }); + } + } + submit = () => { console.debug("submitting problem"); const { inputVal, hintsFinished } = this.state; @@ -211,8 +442,8 @@ class ProblemCard extends React.Component { const { seed, problemVars, problemID, courseName, answerMade, lesson } = this.props; - if (inputVal == '') { - toastNotifyEmpty(this.translate) + if (inputVal == "") { + toastNotifyEmpty(this.translate); return; } @@ -260,6 +491,114 @@ class ProblemCard extends React.Component { checkMarkOpacity: isCorrect ? "100" : "0", }); answerMade(this.index, knowledgeComponents, isCorrect); + + // keystroke logging part + if (this.EventID != 0) { + console.log("start logging"); + this.keylog.TaskOnSet.push(this.taskonset); //record task onset time + + ///// adjust the keylog data + // record current text + // this.keylog.TextContent.push( + // String(this.showYourWorkAreaRef.current.value) + // ); + + if (this.finalAnswerAreaRef && this.finalAnswerAreaRef.current) { + this.keylog.finalAnswer.push( + String(this.finalAnswerAreaRef.current.value) + ); + } + + // record the final product + this.keylog.FinalProduct = String( + this.keylog.TextContent.slice(-1) + ); + + // log cursor position + this.handleCursor(this.keylog, this.startSelect, this.endSelect); + /////// use a customized function to detect and record different activities and the according text changes these activities bring about + ActivityDetector( + this.keylog, + this.startSelect, + this.endSelect, + this.ActivityCancel, + this.TextChangeCancel + ); + + //Add PartitionKey + this.keylog.PartitionKey.push(this.userId); + + //Textchange and Activity adjustment + this.keylog.TextChange.shift(); + this.keylog.Activity.shift(); + + // cursor information adjustment + this.keylog.CursorPosition.shift(); + + let d_end = new Date(); + let taskend = d_end.getTime(); + this.keylog.TaskEnd.push(taskend); //record task end time + + //post the data to the serve and lead to the next page + let keylog_data = { + PartitionKey: this.keylog.PartitionKey.toString(), + RowKey: this.keylog.RowKey.join(), + EventID: this.keylog.EventID.join(), + EventTime: this.keylog.EventTime.join(), + Output: this.keylog.Output.join("<=@=>"), + CursorPosition: this.keylog.CursorPosition.join(), + TextChange: this.keylog.TextChange.join("<=@=>"), + Activity: this.keylog.Activity.join("<=@=>"), + }; + + // console.log("keylog_eedi: ", keylog_data); + + this.context.firebase.log( + parsed, + problemID, + this.step, + null, + isCorrect, + hintsFinished, + "answerStep", + chooseVariables( + Object.assign({}, problemVars, variabilization), + seed + ), + lesson, + courseName, + this.state.displayHintType, + this.state.dynamicHint, + this.state.studentProgress, + keylog_data + ); + + //empty keylog + this.keylog.PartitionKey = []; + this.keylog.RowKey = []; + this.keylog.EventID = []; + this.keylog.EventTime = []; + this.keylog.FinalProduct = []; + this.keylog.CursorPosition = []; + this.keylog.Output = []; + this.keylog.TaskEnd = []; + this.keylog.TaskOnSet = []; + this.keylog.TextChange = []; + this.keylog.Activity = []; + this.keylog.TextContent = []; + this.EventID = 0; + } + + // reset variables + this.EventID = 0; + this.startSelect = []; + this.endSelect = []; + this.ActivityCancel = []; // to keep track of changes caused by control + z + this.TextChangeCancel = []; // to keep track of changes caused by control + z + + console.log( + `Inner submit! e.g. submit the keystroke logging data... (your code) for ${this.sessionId}` + ); }; editInput = (event) => { @@ -552,7 +891,9 @@ class ProblemCard extends React.Component { descriptor={"hint"} > - {translate('problem.Submit')} + {translate("problem.Submit")}