Skip to content

Commit

Permalink
refactor(chat): support live chat for multi-file
Browse files Browse the repository at this point in the history
- include filename inside the chat header (filename.java:10, for example)
- clicking chat will also change state of the filename
- relocate GetHelpChatPage to be defined in index.jsx instead of only ProgrammingFiles.jsx
  • Loading branch information
bivanalhar authored and cysjonathan committed Dec 20, 2024
1 parent 58137b5 commit 24bf9f3
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 81 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { FC } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { Typography } from '@mui/material';

import LoadingEllipsis from 'lib/components/core/LoadingEllipsis';
Expand All @@ -11,13 +12,21 @@ import translations from '../../translations';
import { ChatSender } from '../../types';

interface ConversationAreaProps {
onFeedbackClick: (linenum: number) => void;
onFeedbackClick: (linenum: number, filename?: string) => void;
answerId: number;
}

const ConversationArea: FC<ConversationAreaProps> = (props) => {
const { onFeedbackClick, answerId } = props;

const { control } = useFormContext();
const currentAnswer = useWatch({ control });

const files = currentAnswer[answerId]
? currentAnswer[answerId].files_attributes ||
currentAnswer[`${answerId}`].files_attributes
: [];

const dispatch = useAppDispatch();
const liveFeedbackChats = useAppSelector((state) =>
getLiveFeedbackChatsForAnswerId(state, answerId),
Expand Down Expand Up @@ -78,9 +87,14 @@ const ConversationArea: FC<ConversationAreaProps> = (props) => {
className="flex flex-col whitespace-pre-wrap ml-1"
variant="body2"
>
{t(translations.lineNumber, {
lineNumber: chat.lineNumber,
})}
{files.length === 1
? t(translations.lineNumber, {
lineNumber: chat.lineNumber,
})
: t(translations.fileNameAndLineNumber, {
filename: chat.filename ?? '',
lineNumber: chat.lineNumber,
})}
</Typography>
<Typography
className="flex flex-col whitespace-pre-wrap ml-1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Header from './Header';
import SuggestionChips from './SuggestionChips';

interface GetHelpChatPageProps {
onFeedbackClick: (linenum: number) => void;
onFeedbackClick: (linenum: number, filename?: string) => void;
answerId: number | null;
questionId: number;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useRef } from 'react';
import { useRef, useState } from 'react';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import { Box } from '@mui/material';
import PropTypes from 'prop-types';

import { workflowStates } from 'course/assessment/submission/constants';
Expand All @@ -23,21 +22,12 @@ import ProgrammingFile from './ProgrammingFile';
const ProgrammingFiles = ({
readOnly,
answerId,
questionId,
editorRef,
language,
saveAnswerAndUpdateClientVersion,
}) => {
const { control } = useFormContext();

const liveFeedbackChatForAnswer = useAppSelector((state) =>
getLiveFeedbackChatsForAnswerId(state, answerId),
);
const submission = useAppSelector(getSubmission);
const isAttempting = submission.workflowState === workflowStates.Attempting;

const isLiveFeedbackChatOpen =
liveFeedbackChatForAnswer?.isLiveFeedbackChatOpen;

const { fields } = useFieldArray({
control,
name: `${answerId}.files_attributes`,
Expand All @@ -48,15 +38,6 @@ const ProgrammingFiles = ({
name: `${answerId}.files_attributes`,
});

const editorRef = useRef(null);

const focusEditorOnFeedbackLine = (linenum) => {
editorRef.current?.editor?.gotoLine(linenum, 0);
editorRef.current?.editor?.selection?.setAnchor(linenum - 1, 0);
editorRef.current?.editor?.selection?.moveCursorTo(linenum - 1, 0);
editorRef.current?.editor?.focus();
};

const controlledProgrammingFields = fields.map((field, index) => ({
...field,
...currentField[index],
Expand All @@ -73,34 +54,17 @@ const ProgrammingFiles = ({
const keyString = `editor-container-${index}`;

return (
<div
key={keyString}
className="flex w-full relative gap-3 mb-1 max-h-full"
id={keyString}
>
<Box
className={`${isLiveFeedbackChatOpen && isAttempting ? 'w-1/2' : 'w-full'}`}
>
<ProgrammingFile
key={field.id}
answerId={answerId}
editorRef={editorRef}
fieldName={`${answerId}.files_attributes.${index}.content`}
file={file}
language={language}
readOnly={readOnly}
saveAnswerAndUpdateClientVersion={saveAnswerAndUpdateClientVersion}
/>
</Box>
{isLiveFeedbackChatOpen && isAttempting && (
<div className="absolute h-full flex w-1/2 whitespace-nowrap right-0">
<GetHelpChatPage
answerId={answerId}
onFeedbackClick={focusEditorOnFeedbackLine}
questionId={questionId}
/>
</div>
)}
<div key={keyString} id={keyString}>
<ProgrammingFile
key={field.id}
answerId={answerId}
editorRef={editorRef}
fieldName={`${answerId}.files_attributes.${index}.content`}
file={file}
language={language}
readOnly={readOnly}
saveAnswerAndUpdateClientVersion={saveAnswerAndUpdateClientVersion}
/>
</div>
);
});
Expand All @@ -109,11 +73,45 @@ const ProgrammingFiles = ({
const Programming = (props) => {
const { question, readOnly, answerId, saveAnswerAndUpdateClientVersion } =
props;

const { control } = useFormContext();
const currentAnswer = useWatch({ control });

const liveFeedbackChatForAnswer = useAppSelector((state) =>
getLiveFeedbackChatsForAnswerId(state, answerId),
);
const submission = useAppSelector(getSubmission);
const isAttempting = submission.workflowState === workflowStates.Attempting;

const isLiveFeedbackChatOpen =
liveFeedbackChatForAnswer?.isLiveFeedbackChatOpen;
const fileSubmission = question.fileSubmission;
const isSavingAnswer = useAppSelector((state) =>
getIsSavingAnswer(state, answerId),
);

const files = currentAnswer[answerId]
? currentAnswer[answerId].files_attributes ||
currentAnswer[`${answerId}`].files_attributes
: null;

const [displayFileName, setDisplayFileName] = useState(
files && files.length > 0 ? files[0].filename : '',
);

const editorRef = useRef(null);

const focusEditorOnFeedbackLine = (linenum, filename) => {
if (filename) {
setDisplayFileName(filename);
}

editorRef.current?.editor?.gotoLine(linenum, 0);
editorRef.current?.editor?.selection?.setAnchor(linenum - 1, 0);
editorRef.current?.editor?.selection?.moveCursorTo(linenum - 1, 0);
editorRef.current?.editor?.focus();
};

const feedbackFiles = useAppSelector(
(state) =>
state.assessments.submission.liveFeedback?.feedbackByQuestion?.[
Expand All @@ -122,29 +120,51 @@ const Programming = (props) => {
);

return (
<div className="mt-5">
{fileSubmission ? (
<ProgrammingImportEditor
key={question.id}
answerId={answerId}
isSavingAnswer={isSavingAnswer}
question={question}
readOnly={readOnly}
saveAnswerAndUpdateClientVersion={saveAnswerAndUpdateClientVersion}
/>
) : (
<ProgrammingFiles
key={question.id}
answerId={answerId}
feedbackFiles={feedbackFiles}
language={parseLanguages(question.language)}
questionId={question.id}
readOnly={readOnly}
saveAnswerAndUpdateClientVersion={saveAnswerAndUpdateClientVersion}
/>
)}
<>
<div className="mt-5 flex w-full relative gap-3 mb-1 max-h-[100%]">
<div
className={`${isLiveFeedbackChatOpen && isAttempting ? 'w-1/2' : 'w-full'}`}
>
{fileSubmission ? (
<ProgrammingImportEditor
key={question.id}
answerId={answerId}
displayFileName={displayFileName}
editorRef={editorRef}
isSavingAnswer={isSavingAnswer}
question={question}
readOnly={readOnly}
saveAnswerAndUpdateClientVersion={
saveAnswerAndUpdateClientVersion
}
setDisplayFileName={setDisplayFileName}
/>
) : (
<ProgrammingFiles
key={question.id}
answerId={answerId}
editorRef={editorRef}
feedbackFiles={feedbackFiles}
language={parseLanguages(question.language)}
readOnly={readOnly}
saveAnswerAndUpdateClientVersion={
saveAnswerAndUpdateClientVersion
}
/>
)}
</div>
{isLiveFeedbackChatOpen && isAttempting && (
<div className="absolute h-[100%] flex w-1/2 whitespace-nowrap right-0">
<GetHelpChatPage
answerId={answerId}
onFeedbackClick={focusEditorOnFeedbackLine}
questionId={question.id}
/>
</div>
)}
</div>
<CodaveriFeedbackStatus answerId={answerId} questionId={question.id} />
</div>
</>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react';
import { Component } from 'react';
import { useFieldArray, useFormContext, useWatch } from 'react-hook-form';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
Expand All @@ -17,6 +17,7 @@ import ImportedFileView from './ImportedFileView';

const SelectProgrammingFileEditor = ({
answerId,
editorRef,
readOnly,
language,
displayFileName,
Expand Down Expand Up @@ -54,6 +55,7 @@ const SelectProgrammingFileEditor = ({
return (
<Editor
key={file.id}
editorRef={editorRef}
fieldName={`${answerId}.files_attributes.${index}.content`}
file={file}
language={language}
Expand All @@ -75,6 +77,10 @@ SelectProgrammingFileEditor.propTypes = {
language: PropTypes.string,
displayFileName: PropTypes.string,
saveAnswerAndUpdateClientVersion: PropTypes.func,
editorRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Component) }),
]),
};

const renderProgrammingHistoryEditor = (answer, displayFileName) => {
Expand Down Expand Up @@ -119,11 +125,14 @@ const handleStageFiles = async (filesToImport) => {
const VisibleProgrammingImportEditor = (props) => {
const {
answerId,
editorRef,
disabled,
dispatch,
historyAnswers,
question,
readOnly,
displayFileName,
setDisplayFileName,
saveAnswerAndUpdateClientVersion,
viewHistory,
} = props;
Expand All @@ -136,10 +145,6 @@ const VisibleProgrammingImportEditor = (props) => {
answers[`${answerId}`].files_attributes
: null;

const [displayFileName, setDisplayFileName] = useState(
files && files.length > 0 ? files[0].filename : '',
);

// When an assessment is submitted/unsubmitted,
// the form is somehow not reset yet and the answers for the new answerId
// can't be found.
Expand Down Expand Up @@ -198,6 +203,7 @@ const VisibleProgrammingImportEditor = (props) => {
<SelectProgrammingFileEditor
{...{
answerId,
editorRef,
readOnly,
question,
displayFileName,
Expand Down Expand Up @@ -238,7 +244,13 @@ VisibleProgrammingImportEditor.propTypes = {
questionId: PropTypes.number,
files_attributes: PropTypes.arrayOf(fileShape),
}),
displayFileName: PropTypes.string,
setDisplayFileName: PropTypes.func,
saveAnswerAndUpdateClientVersion: PropTypes.func,
editorRef: PropTypes.oneOfType([
PropTypes.func,
PropTypes.shape({ current: PropTypes.instanceOf(Component) }),
]),
};

function mapStateToProps(state, ownProps) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ export const liveFeedbackChatSlice = createSlice({
const newChats: ChatShape[] = sortedAndCombinedFeedbacks.map((line) => {
return {
sender: ChatSender.codaveri,
filename: line.path,
lineNumber: line.line,
lineContent: answerLines[line.path][line.line - 1].trim() ?? null,
message: line.content,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,10 @@ const translations = defineMessages({
id: 'course.assessment.submission.GetHelpChatPage.ConversationArea.lineNumber',
defaultMessage: 'Line {lineNumber}',
},
fileNameAndLineNumber: {
id: 'course.assessment.submission.GetHelpChatPage.ConversationArea.fileNameAndLineNumber',
defaultMessage: '{filename}:{lineNumber}',
},
threadExpired: {
id: 'course.assessment.submission.GetHelpChatPage.ConversationArea.threadExpired',
defaultMessage: 'The chat above has ended. Start a new chat?',
Expand Down
1 change: 1 addition & 0 deletions client/app/bundles/course/assessment/submission/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export enum ChatSender {

export interface ChatShape {
sender: ChatSender;
filename?: string;
lineNumber: number | null;
lineContent: string | null;
message: string[];
Expand Down

0 comments on commit 24bf9f3

Please sign in to comment.