-
Notifications
You must be signed in to change notification settings - Fork 78
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
(feat) Live feedback history display on the stats page
- Loading branch information
Showing
14 changed files
with
494 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
app/views/course/statistics/assessments/_live_feedback_history_details.json.jbuilder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# frozen_string_literal: true | ||
json.files @live_feedback_details_hash[live_feedback_id].each do |live_feedback_details| | ||
json.id live_feedback_details[:code][:id] | ||
json.filename live_feedback_details[:code][:filename] | ||
json.content live_feedback_details[:code][:content] | ||
json.language @question.specific.language[:name] | ||
json.comments live_feedback_details[:comments].map do |comment| | ||
json.lineNumber comment[:line_number] | ||
json.comment comment[:comment] | ||
end | ||
end |
14 changes: 14 additions & 0 deletions
14
app/views/course/statistics/assessments/live_feedback_history.json.jbuilder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# frozen_string_literal: true | ||
json.liveFeedbackHistory do | ||
json.array! @live_feedbacks.map do |live_feedback| | ||
json.id live_feedback.id | ||
json.createdAt live_feedback.created_at&.iso8601 | ||
json.partial! 'live_feedback_history_details', live_feedback_id: live_feedback.id | ||
end | ||
end | ||
|
||
json.question do | ||
json.id @question.id | ||
json.title @question.title | ||
json.description format_ckeditor_rich_text(@question.description) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
client/app/bundles/course/assessment/operations/liveFeedback.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { AxiosError } from 'axios'; | ||
import { dispatch } from 'store'; | ||
|
||
import CourseAPI from 'api/course'; | ||
|
||
import { liveFeedbackActions as actions } from '../reducers/liveFeedback'; | ||
|
||
export const fetchLiveFeedbackHistory = async ( | ||
assessmentId: number, | ||
questionId: number, | ||
courseUserId: number, | ||
): Promise<void> => { | ||
try { | ||
const response = | ||
await CourseAPI.statistics.assessment.fetchLiveFeedbackHistory( | ||
assessmentId, | ||
questionId, | ||
courseUserId, | ||
); | ||
|
||
const data = response.data; | ||
dispatch( | ||
actions.initialize({ | ||
liveFeedbackHistory: data.liveFeedbackHistory, | ||
question: data.question, | ||
}), | ||
); | ||
} catch (error) { | ||
if (error instanceof AxiosError) throw error.response?.data?.errors; | ||
throw error; | ||
} | ||
}; |
129 changes: 129 additions & 0 deletions
129
.../course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackDetails.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import { FC, useRef, useState } from 'react'; | ||
import ReactAce from 'react-ace'; | ||
import { defineMessages } from 'react-intl'; | ||
import { Box, Card, CardContent, Drawer, Typography } from '@mui/material'; | ||
import { LiveFeedbackCodeAndComments } from 'types/course/assessment/submission/liveFeedback'; | ||
|
||
import EditorField from 'lib/components/core/fields/EditorField'; | ||
import useTranslation from 'lib/hooks/useTranslation'; | ||
|
||
const translations = defineMessages({ | ||
liveFeedbackName: { | ||
id: 'course.assessment.liveFeedback.comments', | ||
defaultMessage: 'Live Feedback', | ||
}, | ||
comments: { | ||
id: 'course.assessment.liveFeedback.comments', | ||
defaultMessage: 'Comments', | ||
}, | ||
lineHeader: { | ||
id: 'course.assessment.liveFeedback.lineHeader', | ||
defaultMessage: 'Line {lineNumber}', | ||
}, | ||
}); | ||
|
||
interface Props { | ||
file: LiveFeedbackCodeAndComments; | ||
} | ||
|
||
const LiveFeedbackDetails: FC<Props> = (props) => { | ||
const { t } = useTranslation(); | ||
const { file } = props; | ||
|
||
const languageMap = { | ||
JavaScript: 'javascript', | ||
'Python 2.7': 'python', | ||
'Python 3.4': 'python', | ||
'Python 3.5': 'python', | ||
'Python 3.6': 'python', | ||
'C/C++': 'c_cpp', | ||
'Python 3.7': 'python', | ||
'Java 8': 'java', | ||
'Java 11': 'java', | ||
'Python 3.9': 'python', | ||
'Python 3.10': 'python', | ||
'Python 3.12': 'python', | ||
'Java 17': 'java', | ||
}; | ||
|
||
const startingLineNum = Math.min( | ||
...file.comments.map((comment) => comment.lineNumber), | ||
); | ||
|
||
const [selectedLine, setSelectedLine] = useState<number>(startingLineNum); | ||
const editorRef = useRef<ReactAce | null>(null); | ||
|
||
const handleCursorChange = (selection): void => { | ||
const currentLine = selection.getCursor().row + 1; // Ace editor uses 0-index, so add 1 | ||
setSelectedLine(currentLine); | ||
}; | ||
|
||
const handleCommentClick = (lineNumber: number): void => { | ||
setSelectedLine(lineNumber); | ||
if (editorRef.current) { | ||
editorRef.current.editor.focus(); | ||
editorRef.current.editor.gotoLine(lineNumber, 0, true); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="relative" id={`file-${file.id}`}> | ||
<Box marginRight="315px"> | ||
<EditorField | ||
ref={editorRef} | ||
cursorStart={startingLineNum - 1} | ||
disabled | ||
// This height matches the prompt height exactly so there is no awkward scroll bar | ||
// and the prompt does not expand weirdly when description is opened | ||
focus | ||
height="482px" | ||
language={languageMap[file.language]} | ||
onCursorChange={handleCursorChange} | ||
value={file.content} | ||
/> | ||
</Box> | ||
<Drawer | ||
anchor="right" | ||
open | ||
PaperProps={{ | ||
style: { | ||
position: 'absolute', | ||
width: '315px', | ||
border: 0, | ||
backgroundColor: 'transparent', | ||
}, | ||
}} | ||
variant="persistent" | ||
> | ||
<div className="p-2"> | ||
{file.comments.map((comment) => ( | ||
<Card | ||
key={`file-${file.id}-comment-${comment.lineNumber}`} | ||
className={`mb-1 border border-solid border-gray-400 rounded-lg shadow-none cursor-pointer ${ | ||
selectedLine === comment.lineNumber ? 'bg-yellow-100' : '' | ||
}`} | ||
onClick={() => { | ||
handleCommentClick(comment.lineNumber); | ||
}} | ||
> | ||
<Typography | ||
className="ml-1" | ||
fontWeight="bold" | ||
variant="subtitle1" | ||
> | ||
{t(translations.lineHeader, { | ||
lineNumber: comment.lineNumber, | ||
})} | ||
</Typography> | ||
<CardContent className="px-1 pt-0 last:pb-1"> | ||
<Typography variant="body2">{comment.comment}</Typography> | ||
</CardContent> | ||
</Card> | ||
))} | ||
</div> | ||
</Drawer> | ||
</div> | ||
); | ||
}; | ||
|
||
export default LiveFeedbackDetails; |
95 changes: 95 additions & 0 deletions
95
...rse/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/LiveFeedbackHistoryPage.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { FC, useState } from 'react'; | ||
import { defineMessages } from 'react-intl'; | ||
import { Slider, Typography } from '@mui/material'; | ||
|
||
import Accordion from 'lib/components/core/layouts/Accordion'; | ||
import { useAppSelector } from 'lib/hooks/store'; | ||
import useTranslation from 'lib/hooks/useTranslation'; | ||
import { formatLongDateTime } from 'lib/moment'; | ||
|
||
import { | ||
getLiveFeedbackHistory, | ||
getLiveFeedbadkQuestionInfo, | ||
} from '../selectors'; | ||
|
||
import LiveFeedbackDetails from './LiveFeedbackDetails'; | ||
|
||
const translations = defineMessages({ | ||
questionTitle: { | ||
id: 'course.assessment.liveFeedback.questionTitle', | ||
defaultMessage: 'Question {index}', | ||
}, | ||
}); | ||
|
||
interface Props { | ||
questionNumber: number; | ||
} | ||
|
||
const LiveFeedbackHistoryPage: FC<Props> = (props) => { | ||
const { t } = useTranslation(); | ||
const { questionNumber } = props; | ||
const allLiveFeedbackHistory = useAppSelector(getLiveFeedbackHistory).filter( | ||
(liveFeedbackHistory) => { | ||
// Remove live feedbacks that have no comments | ||
return liveFeedbackHistory.files.some((file) => file.comments.length > 0); | ||
}, | ||
); | ||
const question = useAppSelector(getLiveFeedbadkQuestionInfo); | ||
|
||
const [displayedIndex, setDisplayedIndex] = useState( | ||
allLiveFeedbackHistory.length - 1, | ||
); | ||
const sliderMarks = allLiveFeedbackHistory.map((liveFeedbackHistory, idx) => { | ||
return { | ||
value: idx, | ||
label: | ||
idx === 0 || idx === allLiveFeedbackHistory.length - 1 | ||
? formatLongDateTime(liveFeedbackHistory.createdAt) | ||
: '', | ||
}; | ||
}); | ||
|
||
return ( | ||
<> | ||
<div className="pb-2"> | ||
<Accordion | ||
defaultExpanded={false} | ||
title={t(translations.questionTitle, { | ||
index: questionNumber, | ||
})} | ||
> | ||
<div className="ml-4 mt-4"> | ||
<Typography variant="body1">{question.title}</Typography> | ||
<Typography | ||
dangerouslySetInnerHTML={{ | ||
__html: question.description, | ||
}} | ||
variant="body2" | ||
/> | ||
</div> | ||
</Accordion> | ||
</div> | ||
|
||
{allLiveFeedbackHistory.length > 1 && ( | ||
<div className="w-[calc(100%_-_17rem)] mx-auto"> | ||
<Slider | ||
defaultValue={allLiveFeedbackHistory.length - 1} | ||
marks={sliderMarks} | ||
max={allLiveFeedbackHistory.length - 1} | ||
min={0} | ||
onChange={(_, value) => { | ||
setDisplayedIndex(Array.isArray(value) ? value[0] : value); | ||
}} | ||
step={null} | ||
valueLabelDisplay="off" | ||
/> | ||
</div> | ||
)} | ||
{allLiveFeedbackHistory[displayedIndex].files.map((file) => { | ||
return <LiveFeedbackDetails key={file.id} file={file} />; | ||
})} | ||
</> | ||
); | ||
}; | ||
|
||
export default LiveFeedbackHistoryPage; |
36 changes: 36 additions & 0 deletions
36
...nt/app/bundles/course/assessment/pages/AssessmentStatistics/LiveFeedbackHistory/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import { FC } from 'react'; | ||
import { useParams } from 'react-router-dom'; | ||
|
||
import { fetchLiveFeedbackHistory } from 'course/assessment/operations/liveFeedback'; | ||
import LoadingIndicator from 'lib/components/core/LoadingIndicator'; | ||
import Preload from 'lib/components/wrappers/Preload'; | ||
|
||
import LiveFeedbackHistoryPage from './LiveFeedbackHistoryPage'; | ||
|
||
interface Props { | ||
questionNumber: number; | ||
questionId: number; | ||
courseUserId: number; | ||
} | ||
|
||
const LiveFeedbackHistoryIndex: FC<Props> = (props): JSX.Element => { | ||
const { questionNumber, questionId, courseUserId } = props; | ||
const { assessmentId } = useParams(); | ||
const parsedAssessmentId = parseInt(assessmentId!, 10); | ||
|
||
const fetchLiveFeedbackHistoryDetails = (): Promise<void> => | ||
fetchLiveFeedbackHistory(parsedAssessmentId, questionId, courseUserId); | ||
|
||
return ( | ||
<Preload | ||
render={<LoadingIndicator />} | ||
while={fetchLiveFeedbackHistoryDetails} | ||
> | ||
{(): JSX.Element => ( | ||
<LiveFeedbackHistoryPage questionNumber={questionNumber} /> | ||
)} | ||
</Preload> | ||
); | ||
}; | ||
|
||
export default LiveFeedbackHistoryIndex; |
Oops, something went wrong.