From f9818235a573a7d3defb736b7a2d7817ef410a88 Mon Sep 17 00:00:00 2001 From: adi-herwana-nus Date: Thu, 4 Jul 2024 17:19:13 +0800 Subject: [PATCH] feature(codaveri-live-feedback): frontend tweaks - tweaked live feedback component styling - live feedback drawer now closes only when all feedback is deleted - added toast messages for successful feedback / no feedback generated --- .../submission/actions/answers/index.js | 45 +-- .../components/answers/Programming/index.jsx | 273 +++++++++++------- .../assessment/submission/translations.ts | 8 + client/locales/en.json | 24 +- client/locales/zh.json | 24 +- 5 files changed, 232 insertions(+), 142 deletions(-) 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..a3ceaa728ae 100644 --- a/client/app/bundles/course/assessment/submission/actions/answers/index.js +++ b/client/app/bundles/course/assessment/submission/actions/answers/index.js @@ -230,20 +230,38 @@ export function initializeLiveFeedback(questionId) { }); } +// if status returned 200, populate feedback array if has feedback, otherwise return error +const handleFeedbackOKResponse = (dispatch, response, answerId, questionId) => { + 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(translations.liveFeedbackSuccess)); + } else { + dispatch({ + type: actionTypes.LIVE_FEEDBACK_FAILURE, + payload: { + questionId, + }, + }); + dispatch(setNotification(translations.liveFeedbackNoneGenerated)); + } +}; + export function generateLiveFeedback(submissionId, answerId, questionId) { 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); } else { // 201, save feedback signed token dispatch({ @@ -267,7 +285,6 @@ export function generateLiveFeedback(submissionId, answerId, questionId) { }); } -// TODO should each answer/question store its own feedback array? export function fetchLiveFeedback( answerId, questionId, @@ -278,16 +295,8 @@ export function fetchLiveFeedback( 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); } }) .catch(() => { 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/translations.ts b/client/app/bundles/course/assessment/submission/translations.ts index 96c8f2e6e3e..91a798f89ca 100644 --- a/client/app/bundles/course/assessment/submission/translations.ts +++ b/client/app/bundles/course/assessment/submission/translations.ts @@ -355,6 +355,14 @@ 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: 'No feedback generated for this code answer.', + }, + liveFeedbackSuccess: { + id: 'course.assessment.submission.liveFeedbackSuccess', + defaultMessage: 'Live code 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..534067c340d 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": "No code feedback generated for this answer." + }, + "course.assessment.submission.liveFeedbackSuccess": { + "defaultMessage": "Live code 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..35c41f11407 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": "TODO" + }, + "course.assessment.submission.liveFeedbackSuccess": { + "defaultMessage": "TODO" + }, "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": "继续" },