diff --git a/client/app/bundles/course/assessment/submission/actions/answers/index.js b/client/app/bundles/course/assessment/submission/actions/answers/index.js
index 44331bb351f..991a48886f7 100644
--- a/client/app/bundles/course/assessment/submission/actions/answers/index.js
+++ b/client/app/bundles/course/assessment/submission/actions/answers/index.js
@@ -230,19 +230,57 @@ export function initializeLiveFeedback(questionId) {
});
}
-export function generateLiveFeedback(submissionId, answerId, questionId) {
+// if status returned 200, populate feedback array if has feedback, otherwise return error
+const handleFeedbackOKResponse = ({
+ dispatch,
+ response,
+ answerId,
+ questionId,
+ successMessage,
+ noFeedbackMessage,
+}) => {
+ const feedbackFiles = response.data?.data?.feedbackFiles ?? [];
+ const success = response.data?.success;
+ if (success && feedbackFiles.length) {
+ dispatch({
+ type: actionTypes.LIVE_FEEDBACK_SUCCESS,
+ payload: {
+ questionId,
+ answerId,
+ feedbackFiles,
+ },
+ });
+ dispatch(setNotification(successMessage));
+ } else {
+ dispatch({
+ type: actionTypes.LIVE_FEEDBACK_FAILURE,
+ payload: {
+ questionId,
+ },
+ });
+ dispatch(setNotification(noFeedbackMessage));
+ }
+};
+
+export function generateLiveFeedback({
+ submissionId,
+ answerId,
+ questionId,
+ successMessage,
+ noFeedbackMessage,
+}) {
return (dispatch) =>
CourseAPI.assessment.submissions
.generateLiveFeedback(submissionId, { answer_id: answerId })
.then((response) => {
if (response.status === 200) {
- dispatch({
- type: actionTypes.LIVE_FEEDBACK_SUCCESS,
- payload: {
- questionId,
- answerId,
- feedbackFiles: response.data?.data?.feedbackFiles ?? {},
- },
+ handleFeedbackOKResponse({
+ dispatch,
+ response,
+ answerId,
+ questionId,
+ successMessage,
+ noFeedbackMessage,
});
} else {
// 201, save feedback signed token
@@ -267,26 +305,26 @@ export function generateLiveFeedback(submissionId, answerId, questionId) {
});
}
-// TODO should each answer/question store its own feedback array?
-export function fetchLiveFeedback(
+export function fetchLiveFeedback({
answerId,
questionId,
feedbackUrl,
feedbackToken,
-) {
+ successMessage,
+ noFeedbackMessage,
+}) {
return (dispatch) =>
CourseAPI.assessment.submissions
.fetchLiveFeedback(feedbackUrl, feedbackToken)
.then((response) => {
- // if 200, go straight to LIVE_FEEDBACK_SUCCESS
if (response.status === 200) {
- dispatch({
- type: actionTypes.LIVE_FEEDBACK_SUCCESS,
- payload: {
- questionId,
- answerId,
- feedbackFiles: response.data?.data?.feedbackFiles ?? {},
- },
+ handleFeedbackOKResponse({
+ dispatch,
+ response,
+ answerId,
+ questionId,
+ successMessage,
+ noFeedbackMessage,
});
}
})
diff --git a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx
index 2c3319c626c..b3508607794 100644
--- a/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx
+++ b/client/app/bundles/course/assessment/submission/components/answers/Programming/index.jsx
@@ -1,5 +1,6 @@
import { useRef, useState } from 'react';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
+import { defineMessages } from 'react-intl';
import { Close, ThumbDown, ThumbUp } from '@mui/icons-material';
import {
Box,
@@ -8,6 +9,7 @@ import {
CardContent,
Drawer,
IconButton,
+ Tooltip,
Typography,
} from '@mui/material';
import { green, grey, orange, red, yellow } from '@mui/material/colors';
@@ -32,19 +34,25 @@ const styles = {
card: {
marginBottom: 1,
borderStyle: 'solid',
- borderWidth: 0.2,
+ borderWidth: 1.0,
borderColor: grey[400],
borderRadius: 2,
+ boxShadow: 'none',
minWidth: '300px',
maxWidth: '300px',
},
- header: {
+ cardActions: {
+ px: 0,
+ paddingTop: 0.5,
+ paddingBottom: 0,
display: 'flex',
- backgroundColor: orange[100],
- borderRadius: 2,
- padding: 1,
- borderBottomLeftRadius: 0,
- borderBottomRightRadius: 0,
+ },
+ cardContent: {
+ px: 1,
+ paddingTop: 0,
+ '&:last-child': {
+ paddingBottom: 1,
+ },
},
cardSelected: {
backgroundColor: yellow[100],
@@ -53,15 +61,53 @@ const styles = {
backgroundColor: orange.A100,
},
cardResolved: {
- opacity: 0.6,
- backgroundColor: green['100'],
+ borderColor: '#cecece',
+ backgroundColor: green[100],
+ color: grey[600],
},
cardDismissed: {
- opacity: 0.6,
- backgroundColor: red['100'],
+ borderColor: '#cecece',
+ backgroundColor: red[100],
+ color: grey[600],
+ },
+ cardActionButton: {
+ opacity: 1.0,
+ marginX: -0.5,
+ padding: 0.4,
+ transform: 'scale(0.86)',
+ transformOrigin: 'right',
+ },
+ cardActionButtonHighlightOnResolve: {
+ '&:disabled': {
+ color: green.A700,
+ },
+ },
+ cardActionButtonHighlightOnDismiss: {
+ '&:disabled': {
+ color: red.A700,
+ },
},
};
+const translations = defineMessages({
+ lineHeader: {
+ id: 'course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLineHeading',
+ defaultMessage: 'Line {linenum}',
+ },
+ likeItem: {
+ id: 'course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLike',
+ defaultMessage: 'Like',
+ },
+ dislikeItem: {
+ id: 'course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDislike',
+ defaultMessage: 'Dislike',
+ },
+ deleteItem: {
+ id: 'course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDelete',
+ defaultMessage: 'Dismiss',
+ },
+});
+
const ProgrammingFiles = ({
readOnly,
questionId,
@@ -95,32 +141,6 @@ const ProgrammingFiles = ({
}
};
- // const editorKeyboardHandler = {
- // handleKeyboard: (data, hash, keyString) => {
- // const selectedRow = editorRef.current?.editor?.selection?.cursor?.row;
- // const lastRow =
- // (editorRef.current?.editor?.session?.getLength() ?? 1) - 1;
- // if (selectedRow || selectedRow === 0) {
- // if (keyString === 'up') {
- // setSelectedLine(Math.max(selectedRow - 1, 0) + 1);
- // } else if (keyString === 'down') {
- // setSelectedLine(Math.min(selectedRow + 1, lastRow) + 1);
- // }
- // }
- // },
- // };
-
- // useEffect(() => {
- // editorRef.current?.editor?.keyBinding?.addKeyboardHandler(
- // editorKeyboardHandler,
- // );
- // return () => {
- // editorRef.current?.editor?.keyBinding?.removeKeyboardHandler(
- // editorKeyboardHandler,
- // );
- // };
- // });
-
const renderFeedbackCard = (feedbackItem) => {
let cardStyle = styles.card;
if (feedbackItem.state === 'resolved') {
@@ -131,6 +151,22 @@ const ProgrammingFiles = ({
cardStyle = { ...styles.card, ...styles.cardSelected };
}
+ const feedbackTooltipProps = {
+ placement: 'top',
+ slotProps: {
+ popper: {
+ modifiers: [
+ {
+ name: 'offset',
+ options: {
+ offset: [0, -12],
+ },
+ },
+ ],
+ },
+ },
+ };
+
const focusEditorOnFeedbackLine = () => {
editorRef.current?.editor?.gotoLine(feedbackItem.linenum, 0);
editorRef.current?.editor?.selection?.setAnchor(
@@ -144,85 +180,94 @@ const ProgrammingFiles = ({
editorRef.current?.editor?.focus();
};
+ const renderLikeButton = () => (
+
+ {
+ dispatch({
+ type: actionTypes.LIVE_FEEDBACK_ITEM_MARK_RESOLVED,
+ payload: {
+ questionId,
+ path: 'main.py',
+ lineId: feedbackItem.id,
+ },
+ });
+ }}
+ sx={{
+ ...styles.cardActionButton,
+ ...styles.cardActionButtonHighlightOnResolve,
+ }}
+ >
+
+
+
+ );
+
+ const renderDislikeButton = () => (
+
+ {
+ dispatch({
+ type: actionTypes.LIVE_FEEDBACK_ITEM_MARK_DISMISSED,
+ payload: {
+ questionId,
+ path: 'main.py',
+ lineId: feedbackItem.id,
+ },
+ });
+ }}
+ sx={{
+ ...styles.cardActionButton,
+ ...styles.cardActionButtonHighlightOnDismiss,
+ }}
+ >
+
+
+
+ );
+
+ const renderDeleteButton = () => (
+
+ {
+ dispatch({
+ type: actionTypes.LIVE_FEEDBACK_ITEM_DELETE,
+ payload: {
+ questionId,
+ path: 'main.py',
+ lineId: feedbackItem.id,
+ },
+ });
+ }}
+ sx={{ ...styles.cardActionButton, marginRight: 1 }}
+ >
+
+
+
+ );
+
return (
-
- {feedbackItem.feedback}
-
- L{feedbackItem.linenum}
+ {t(translations.lineHeader, { linenum: feedbackItem.linenum })}
- {feedbackItem.state === 'resolved' && (
-
- {t({
- id: 'course.assessment.submission.answers.Programming.liveFeedbackItemResolved',
- defaultMessage: 'Item resolved.',
- })}
-
- )}
- {feedbackItem.state === 'dismissed' && (
-
- {t({
- id: 'course.assessment.submission.answers.Programming.liveFeedbackItemDismissed',
- defaultMessage: 'Item dismissed.',
- })}
-
- )}
- {
- // TODO: expose BE route to Codaveri feedback rating endpoint and call here
- dispatch({
- type: actionTypes.LIVE_FEEDBACK_ITEM_MARK_RESOLVED,
- payload: {
- questionId,
- path: 'main.py',
- lineId: feedbackItem.id,
- },
- });
- }}
- size="small"
- >
-
-
- {
- dispatch({
- type: actionTypes.LIVE_FEEDBACK_ITEM_MARK_DISMISSED,
- payload: {
- questionId,
- path: 'main.py',
- lineId: feedbackItem.id,
- },
- });
- }}
- size="small"
- >
-
-
- {
- dispatch({
- type: actionTypes.LIVE_FEEDBACK_ITEM_DELETE,
- payload: {
- questionId,
- path: 'main.py',
- lineId: feedbackItem.id,
- },
- });
- }}
- size="small"
- >
-
-
+ {renderLikeButton()}
+ {renderDislikeButton()}
+ {renderDeleteButton()}
+
+ {feedbackItem.feedback}
+
);
};
@@ -249,9 +294,7 @@ const ProgrammingFiles = ({
annotations = feedbackFiles['main.py'] ?? [];
}
const keyString = `editor-container-${index}`;
- const shouldOpenDrawer = annotations?.some(
- (feedbackItem) => feedbackItem.state === 'pending',
- );
+ const shouldOpenDrawer = annotations.length > 0;
return (
@@ -275,7 +318,14 @@ const ProgrammingFiles = ({
style: { alignContent: 'start', position: 'absolute' },
}}
open={shouldOpenDrawer}
- PaperProps={{ style: { position: 'absolute' } }}
+ PaperProps={{
+ style: {
+ position: 'absolute',
+ width: '315px',
+ alignContent: 'start',
+ border: 0,
+ },
+ }}
variant="persistent"
>
{annotations.map(renderFeedbackCard)}
@@ -322,7 +372,6 @@ const Programming = (props) => {
saveAnswerAndUpdateClientVersion={saveAnswerAndUpdateClientVersion}
/>
)}
- {/*
*/}
);
diff --git a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/SubmissionEditStepForm.jsx b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/SubmissionEditStepForm.jsx
index a2a845caf73..a13dc2312ad 100644
--- a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/SubmissionEditStepForm.jsx
+++ b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/SubmissionEditStepForm.jsx
@@ -577,20 +577,6 @@ const SubmissionEditStepForm = (props) => {
/>
);
- const renderProgrammingQuestionActions = () => {
- const id = questionIds[stepIndex];
- const question = questions[id];
-
- return (
-
-
- {isCodaveriEnabled &&
- question.isCodaveri &&
- renderGetLiveFeedbackButton()}
-
- );
- };
-
const renderStepQuestion = () => {
const id = questionIds[stepIndex];
const question = questions[id];
@@ -613,15 +599,17 @@ const SubmissionEditStepForm = (props) => {
}}
/>
{attempting && (
- <>
+
{renderResetButton()}
{renderSubmitButton()}
{renderContinueButton()}
- >
+
+ {question.type === questionTypes.Programming &&
+ isCodaveriEnabled &&
+ question.isCodaveri &&
+ renderGetLiveFeedbackButton()}
+
)}
- {attempting &&
- question.type === questionTypes.Programming &&
- renderProgrammingQuestionActions()}
{renderAutogradingErrorPanel(id)}
{renderExplanationPanel(question)}
diff --git a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx
index a4376f89009..fbcf31af419 100644
--- a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx
+++ b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx
@@ -1,5 +1,5 @@
import { Component } from 'react';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, injectIntl } from 'react-intl';
import { connect } from 'react-redux';
import InsertDriveFile from '@mui/icons-material/InsertDriveFile';
import {
@@ -204,17 +204,33 @@ class VisibleSubmissionEditIndex extends Component {
};
onFetchLiveFeedback = (answerId, questionId) => {
- const { dispatch, liveFeedback } = this.props;
+ const {
+ intl,
+ dispatch,
+ liveFeedback,
+ assessment: { questionIds },
+ } = this.props;
- const feedbackRequestToken =
+ const feedbackToken =
liveFeedback?.feedbackByQuestion?.[questionId].pendingFeedbackToken;
+ const questionIndex = questionIds.findIndex((id) => id === questionId) + 1;
+ const successMessage = intl.formatMessage(
+ translations.liveFeedbackSuccess,
+ { questionIndex },
+ );
+ const noFeedbackMessage = intl.formatMessage(
+ translations.liveFeedbackNoneGenerated,
+ { questionIndex },
+ );
dispatch(
- fetchLiveFeedback(
+ fetchLiveFeedback({
answerId,
questionId,
- liveFeedback?.feedbackUrl,
- feedbackRequestToken,
- ),
+ feedbackUrl: liveFeedback?.feedbackUrl,
+ feedbackToken,
+ successMessage,
+ noFeedbackMessage,
+ }),
);
};
@@ -229,10 +245,30 @@ class VisibleSubmissionEditIndex extends Component {
onGenerateLiveFeedback = (answerId, questionId) => {
const {
dispatch,
+ intl,
+ assessment: { questionIds },
match: { params },
} = this.props;
+ const questionIndex = questionIds.findIndex((id) => id === questionId) + 1;
+ const successMessage = intl.formatMessage(
+ translations.liveFeedbackSuccess,
+ { questionIndex },
+ );
+ const noFeedbackMessage = intl.formatMessage(
+ translations.liveFeedbackNoneGenerated,
+ { questionIndex },
+ );
+
dispatch(initializeLiveFeedback(questionId));
- dispatch(generateLiveFeedback(params.submissionId, answerId, questionId));
+ dispatch(
+ generateLiveFeedback({
+ submissionId: params.submissionId,
+ answerId,
+ questionId,
+ successMessage,
+ noFeedbackMessage,
+ }),
+ );
};
allConsideredCorrect() {
@@ -495,6 +531,7 @@ VisibleSubmissionEditIndex.propTypes = {
questions: PropTypes.objectOf(questionShape),
historyAnswers: PropTypes.objectOf(answerShape),
historyQuestions: PropTypes.objectOf(historyQuestionShape),
+ intl: PropTypes.object.isRequired,
questionsFlags: PropTypes.objectOf(questionFlagsShape),
submission: submissionShape,
topics: PropTypes.objectOf(topicShape),
@@ -539,7 +576,9 @@ function mapStateToProps({ assessments: { submission } }) {
const handle = assessmentsTranslations.attempt;
const SubmissionEditIndex = withRouter(
- withHeartbeatWorker(connect(mapStateToProps)(VisibleSubmissionEditIndex)),
+ withHeartbeatWorker(
+ connect(mapStateToProps)(injectIntl(VisibleSubmissionEditIndex)),
+ ),
);
export default Object.assign(SubmissionEditIndex, { handle });
diff --git a/client/app/bundles/course/assessment/submission/translations.ts b/client/app/bundles/course/assessment/submission/translations.ts
index 96c8f2e6e3e..33badc10173 100644
--- a/client/app/bundles/course/assessment/submission/translations.ts
+++ b/client/app/bundles/course/assessment/submission/translations.ts
@@ -355,6 +355,15 @@ const translations = defineMessages({
Try submitting your code again in a couple of minutes \
or check the error message in the network response.',
},
+ liveFeedbackNoneGenerated: {
+ id: 'course.assessment.submission.liveFeedbackNoneGenerated',
+ defaultMessage: 'Question {questionIndex}: No feedback generated.',
+ },
+ liveFeedbackSuccess: {
+ id: 'course.assessment.submission.liveFeedbackSuccess',
+ defaultMessage:
+ 'Question {questionIndex}: Feedback successfully generated.',
+ },
autogradeSubmissionSuccess: {
id: 'course.assessment.submission.autogradeSubmissionSuccess',
defaultMessage: 'All answers have been evaluated.',
diff --git a/client/locales/en.json b/client/locales/en.json
index fc63af7882f..38a944114a1 100644
--- a/client/locales/en.json
+++ b/client/locales/en.json
@@ -2795,12 +2795,6 @@
"course.assessment.submission.answers.ForumPostResponse.TopicCard.viewTopicInNewTab": {
"defaultMessage": "View Topic"
},
- "course.assessment.submission.answers.Programming.liveFeedbackItemDismissed": {
- "defaultMessage": "Item dismissed."
- },
- "course.assessment.submission.answers.Programming.liveFeedbackItemResolved": {
- "defaultMessage": "Item resolved."
- },
"course.assessment.submission.answers.Programming.ProgrammingFile.downloadFile": {
"defaultMessage": "Download File"
},
@@ -2831,6 +2825,12 @@
"course.assessment.submission.codaveriAutogradeFailure": {
"defaultMessage": "There is an error while evaluating your code in Codaveri. Try submitting your code again in a couple of minutes or check the error message in the network response."
},
+ "course.assessment.submission.liveFeedbackNoneGenerated": {
+ "defaultMessage": "Question {questionIndex}: No feedback generated."
+ },
+ "course.assessment.submission.liveFeedbackSuccess": {
+ "defaultMessage": "Question {questionIndex}: Feedback successfully generated."
+ },
"course.assessment.submission.comment.CodaveriCommentCard.finalise": {
"defaultMessage": "Finalise and Post Feedback"
},
@@ -2873,6 +2873,18 @@
"course.assessment.submission.comments": {
"defaultMessage": "Comments"
},
+ "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDelete": {
+ "defaultMessage": "Dismiss"
+ },
+ "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDislike": {
+ "defaultMessage": "Dislike"
+ },
+ "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLike": {
+ "defaultMessage": "Like"
+ },
+ "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLineHeading": {
+ "defaultMessage": "Line {linenum}"
+ },
"course.assessment.submission.continue": {
"defaultMessage": "Continue"
},
diff --git a/client/locales/zh.json b/client/locales/zh.json
index 444517bf3fa..0673adafd48 100644
--- a/client/locales/zh.json
+++ b/client/locales/zh.json
@@ -2768,12 +2768,6 @@
"course.assessment.submission.answers.ForumPostResponse.TopicCard.viewTopicInNewTab": {
"defaultMessage": "查看主题"
},
- "course.assessment.submission.answers.Programming.liveFeedbackItemDismissed": {
- "defaultMessage": "项目已忽略。"
- },
- "course.assessment.submission.answers.Programming.liveFeedbackItemResolved": {
- "defaultMessage": "项目已解决。"
- },
"course.assessment.submission.answers.Programming.ProgrammingFile.downloadFile": {
"defaultMessage": "下载文件"
},
@@ -2804,6 +2798,12 @@
"course.assessment.submission.codaveriAutogradeFailure": {
"defaultMessage": "(T_T) 抱歉,codaveri 自动打分气罢工了。尝试在几分钟后再次提交你的代码或检查网络响应中的错误消息。"
},
+ "course.assessment.submission.liveFeedbackNoneGenerated": {
+ "defaultMessage": "问题 {questionIndex}:未生成反馈。"
+ },
+ "course.assessment.submission.liveFeedbackSuccess": {
+ "defaultMessage": "问题 {questionIndex}:反馈生成成功。"
+ },
"course.assessment.submission.comment.CodaveriCommentCard.finalise": {
"defaultMessage": "完成并发布反馈"
},
@@ -2846,6 +2846,18 @@
"course.assessment.submission.comments": {
"defaultMessage": "注释"
},
+ "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDelete": {
+ "defaultMessage": "忽略"
+ },
+ "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemDislike": {
+ "defaultMessage": "不喜欢"
+ },
+ "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLike": {
+ "defaultMessage": "喜欢"
+ },
+ "course.assessment.submission.answers.Programming.ProgrammingFiles.liveFeedbackItemLineHeading": {
+ "defaultMessage": "线 {linenum}"
+ },
"course.assessment.submission.continue": {
"defaultMessage": "继续"
},