diff --git a/src/components/menu-bar/menu-bar.jsx b/src/components/menu-bar/menu-bar.jsx index b3b3be4dad4..824e4884d7f 100644 --- a/src/components/menu-bar/menu-bar.jsx +++ b/src/components/menu-bar/menu-bar.jsx @@ -539,16 +539,16 @@ class MenuBar extends React.Component { this.props.onArtieChangeFlowState(ARTIE_FLOW_EXERCISE_STATEMENT_STATE); } handleClickRequestEmotionalHelp (){ - // Evita múltiples solicitudes mientras está cargando + // Avoid multiple requests while help loading is in progress if (this.props.artieExercises.loadingHelp) return; this.props.onArtieShowHelpPopup(null, true); if (this.artieEmotionalPopupFeature === 'on') { - // We show the emotional popup just in the case the flag is active + // Show the emotional popup only when the feature flag is active this.props.onArtieChangeFlowState(ARTIE_FLOW_EMOTIONAL_STATE); } else { - // Activamos pantalla de carga mientras esperamos la ayuda + // Turn on loading overlay while waiting for the help response this.props.onArtieLoadingHelp(true); // Compute Split guard: if Help_Popup is OFF and the student interacts with the robot, @@ -560,8 +560,10 @@ class MenuBar extends React.Component { ); const hideByFeature = this.artieHelpPopupFeature === 'off' && interactsWithRobot; - // In case the the flag is off we just show the help popup - sendBlockArtie( + // Wrap the help request so we can enforce a client-side timeout + const HELP_TIMEOUT_MS = 15000; // 15 seconds to avoid an infinite loading overlay + + const helpRequestPromise = sendBlockArtie( this.props.artieLogin.currentStudent, this.props.sprites, this.props.artieExercises.currentExercise, @@ -573,8 +575,25 @@ class MenuBar extends React.Component { this.props.artieExercises.lastExerciseChange, null, null - ) + ); + + const timeoutPromise = new Promise((resolve, reject) => { + this.artieHelpTimeoutId = setTimeout(() => { + // Mark this as a timeout-specific error so we can handle it if needed + const timeoutError = new Error('ARTIE_HELP_TIMEOUT'); + timeoutError.code = 'ARTIE_HELP_TIMEOUT'; + reject(timeoutError); + }, HELP_TIMEOUT_MS); + }); + + Promise.race([helpRequestPromise, timeoutPromise]) .then(responseBodyObject => { + // If we got a real response, clear the timeout timer + if (this.artieHelpTimeoutId) { + clearTimeout(this.artieHelpTimeoutId); + this.artieHelpTimeoutId = null; + } + // If the response has a solution distance object if (responseBodyObject !== null && responseBodyObject.solutionDistance !== null){ this.props.onArtieHelpReceived(responseBodyObject.solutionDistance); @@ -586,12 +605,19 @@ class MenuBar extends React.Component { } }) .catch(() => { - // En caso de error, aseguramos ocultar el overlay + // If the error is a timeout or a network/server error, just ensure the overlay closes. + if (this.artieHelpTimeoutId) { + clearTimeout(this.artieHelpTimeoutId); + this.artieHelpTimeoutId = null; + } + + // TODO: Optionally dispatch a user-visible error for ARTIE help timeout }) .finally(() => { - // Stops the loading help + // Stop the loading help overlay in all paths (success, error or timeout) this.props.onArtieLoadingHelp(false); }); + if (this.props.artieExercises.secondsHelpOpen > 0) { this.props.onArtieResetSecondsHelpOpen(); } diff --git a/src/containers/artie-help.jsx b/src/containers/artie-help.jsx index d71c5a1be1c..4f803466460 100644 --- a/src/containers/artie-help.jsx +++ b/src/containers/artie-help.jsx @@ -49,13 +49,16 @@ class ArtieHelp extends React.Component { this.workspace.options.pathToMedia = 'static/blocks-media/'; - // Gets the workspace metrics + // Get workspace metrics const metrics = this.workspace.getMetrics(); - // If the help is not null and we have some blocks to add - if (this.props.help !== null && this.props.help.nextSteps !== null && this.props.help.nextSteps.addBlocks !== null) { + // If we have help data and a non-empty list of blocks to add + if (this.props.help !== null && + this.props.help.nextSteps !== null && + this.props.help.nextSteps.addBlocks !== null + ) { - // We build the block array for the elements we have to add + // Build the block array for the blocks we need to add const addBlockArray = []; let dy = 10; let dx = -1; @@ -69,7 +72,7 @@ class ArtieHelp extends React.Component { block.setDeletable(false); block.contextMenu = false; - // If we haven't set the center of the workspace + // If we have not yet computed the workspace center offset if (dx === -1) { const {x} = block.getRelativeToSurfaceXY(); @@ -79,7 +82,7 @@ class ArtieHelp extends React.Component { const midPoint = metrics.viewWidth / 2; if (x === 0) { - // If it's the first time positioning, it should always move right + // On first positioning, we always move the block to the right if (block.width < midPoint) { dx = ltrX; } else if (block.width < metrics.viewWidth) { @@ -126,13 +129,16 @@ class ArtieHelp extends React.Component { this.workspace.options.pathToMedia = 'static/blocks-media/'; - // Gets the workspace metrics + // Get workspace metrics const metrics = this.workspace.getMetrics(); - // If the help is not null and we have some blocks to delete - if (this.props.help !== null && this.props.help.nextSteps !== null && this.props.help.nextSteps.deleteBlocks !== null) { + // If we have help data and a non-empty list of blocks to delete + if (this.props.help !== null && + this.props.help.nextSteps !== null && + this.props.help.nextSteps.deleteBlocks !== null + ) { - // We build the block array for the blocks we have to delete + // Build the block array for the blocks we need to delete const delBlockArray = []; let dy = 10; let dx = -1; @@ -146,7 +152,7 @@ class ArtieHelp extends React.Component { block.setDeletable(false); block.contextMenu = false; - // If we haven't set the center of the workspace + // If we have not yet computed the workspace center offset if (dx === -1) { const {x} = block.getRelativeToSurfaceXY(); @@ -156,7 +162,7 @@ class ArtieHelp extends React.Component { const midPoint = metrics.viewWidth / 2; if (x === 0) { - // If it's the first time positioning, it should always move right + // On first positioning, we always move the block to the right if (block.width < midPoint) { dx = ltrX; } else if (block.width < metrics.viewWidth) { @@ -203,13 +209,16 @@ class ArtieHelp extends React.Component { this.workspace.options.pathToMedia = 'static/blocks-media/'; - // Gets the workspace metrics + // Get workspace metrics const metrics = this.workspace.getMetrics(); - // If the help is not null and we have some input values to replace - if (this.props.help !== null && this.props.help.nextSteps !== null && this.props.help.nextSteps.replaceInputs !== null) { + // If we have help data and a non-empty list of inputs to replace + if (this.props.help !== null && + this.props.help.nextSteps !== null && + this.props.help.nextSteps.replaceInputs !== null + ) { - // We build the block array for the elements we have to replace + // Build the block array for the blocks whose inputs we need to replace const replaceBlockArray = []; let dy = 10; let dx = -1; @@ -223,7 +232,7 @@ class ArtieHelp extends React.Component { block.setDeletable(false); block.contextMenu = false; - // If we haven't set the center of the workspace + // If we have not yet computed the workspace center offset if (dx === -1) { const {x} = block.getRelativeToSurfaceXY(); @@ -233,7 +242,7 @@ class ArtieHelp extends React.Component { const midPoint = metrics.viewWidth / 2; if (x === 0) { - // If it's the first time positioning, it should always move right + // On first positioning, we always move the block to the right if (block.width < midPoint) { dx = ltrX; } else if (block.width < metrics.viewWidth) { @@ -280,13 +289,16 @@ class ArtieHelp extends React.Component { this.workspace.options.pathToMedia = 'static/blocks-media/'; - // Gets the workspace metrics + // Get workspace metrics const metrics = this.workspace.getMetrics(); - // If the help is not null and we have some misplaced blocks - if (this.props.help !== null && this.props.help.nextSteps !== null && this.props.help.nextSteps.replacePositions !== null) { + // If we have help data and a non-empty list of misplaced blocks + if (this.props.help !== null && + this.props.help.nextSteps !== null && + this.props.help.nextSteps.replacePositions !== null + ) { - // We build the block array for the elements we have to add + // Build the block array for the misplaced blocks we need to show const misplacedBlockArray = []; let dy = 10; let dx = -1; @@ -300,7 +312,7 @@ class ArtieHelp extends React.Component { block.setDeletable(false); block.contextMenu = false; - // If we haven't set the center of the workspace + // If we have not yet computed the workspace center offset if (dx === -1) { const {x} = block.getRelativeToSurfaceXY(); @@ -310,7 +322,7 @@ class ArtieHelp extends React.Component { const midPoint = metrics.viewWidth / 2; if (x === 0) { - // If it's the first time positioning, it should always move right + // On first positioning, we always move the block to the right if (block.width < midPoint) { dx = ltrX; } else if (block.width < metrics.viewWidth) { @@ -376,8 +388,12 @@ class ArtieHelp extends React.Component { // do not render the help popup at all. Data fetching and timers can still run elsewhere, // but the visual popup must remain hidden so the robot tutor handles the help. const interactsWithRobot = Boolean( - (this.props.artieLogin && this.props.artieLogin.currentStudent && this.props.artieLogin.currentStudent.interactsWithRobot) || - (this.props.artieLogin && this.props.artieLogin.user && this.props.artieLogin.user.interactsWithRobot) + (this.props.artieLogin && + this.props.artieLogin.currentStudent && + this.props.artieLogin.currentStudent.interactsWithRobot) || + (this.props.artieLogin && + this.props.artieLogin.user && + this.props.artieLogin.user.interactsWithRobot) ); const hideByFeature = this.props.artieHelpPopupFeature === 'off' && interactsWithRobot;