diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index 20d4bb4402..08388daf0f 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -39,6 +39,8 @@ export type SessionState = { readonly enableAchievements?: boolean; readonly enableSourcecast?: boolean; readonly enableStories?: boolean; + readonly enableLlmGrading?: boolean; + readonly llmApiKey?: string; readonly sourceChapter?: Chapter; readonly sourceVariant?: Variant; readonly moduleHelpText?: string; @@ -105,10 +107,12 @@ export type CourseConfiguration = { enableAchievements: boolean; enableSourcecast: boolean; enableStories: boolean; + enableLlmGrading?: boolean; sourceChapter: Chapter; sourceVariant: Variant; moduleHelpText: string; assetsPrefix: string; + llmApiKey?: string; }; export type AdminPanelCourseRegistration = { diff --git a/src/commons/assessment/AssessmentTypes.ts b/src/commons/assessment/AssessmentTypes.ts index 2bd726f9b3..c8b36042e2 100644 --- a/src/commons/assessment/AssessmentTypes.ts +++ b/src/commons/assessment/AssessmentTypes.ts @@ -121,6 +121,7 @@ export interface IProgrammingQuestion extends BaseQuestion { prepend: string; postpend: string; solutionTemplate: string; + llm_prompt?: string | null; testcases: Testcase[]; testcasesPrivate?: Testcase[]; // For mission control type: 'programming'; @@ -279,6 +280,7 @@ export const programmingTemplate = (): IProgrammingQuestion => { prepend: '', solutionTemplate: '//This is a mock solution template', postpend: '', + llm_prompt: null, testcases: [], testcasesPrivate: [], type: 'programming', diff --git a/src/commons/dropdown/DropdownCreateCourse.tsx b/src/commons/dropdown/DropdownCreateCourse.tsx index 2d37ce7eb9..88d5018b52 100644 --- a/src/commons/dropdown/DropdownCreateCourse.tsx +++ b/src/commons/dropdown/DropdownCreateCourse.tsx @@ -40,9 +40,11 @@ const DropdownCreateCourse: React.FC = props => { enableAchievements: true, enableSourcecast: true, enableStories: false, + enableLlmGrading: false, sourceChapter: Chapter.SOURCE_1, sourceVariant: Variant.DEFAULT, - moduleHelpText: '' + moduleHelpText: '', + llmApiKey: '' }); const [courseHelpTextSelectedTab, setCourseHelpTextSelectedTab] = @@ -222,7 +224,8 @@ const DropdownCreateCourse: React.FC = props => { }) } /> - + +
= props => { }) } /> + + + setCourseConfig({ + ...courseConfig, + enableLlmGrading: (e.target as HTMLInputElement).checked + }) + } + />
@@ -273,6 +288,24 @@ const DropdownCreateCourse: React.FC = props => { fill /> + + + setCourseConfig({ + ...courseConfig, + llmApiKey: e.target.value + }) + } + /> +
{!isMobileBreakpoint && }
@@ -186,6 +206,16 @@ const CourseConfigPanel: React.FC = props => { }) } /> + + props.setCourseConfiguration({ + ...props.courseConfiguration, + enableLlmGrading: (e.target as HTMLInputElement).checked + }) + } + />
diff --git a/src/pages/academy/grading/subcomponents/GradingCommentSelector.tsx b/src/pages/academy/grading/subcomponents/GradingCommentSelector.tsx new file mode 100644 index 0000000000..6c22964a79 --- /dev/null +++ b/src/pages/academy/grading/subcomponents/GradingCommentSelector.tsx @@ -0,0 +1,38 @@ +import { NonIdealState, Spinner } from '@blueprintjs/core'; +import React from 'react'; + +type Props = { + comments: string[]; + isLoading: boolean; + onSelect: (comment: string) => void; +}; + +const GradingCommentSelector: React.FC = props => { + return ( +
+
Comment Suggestions:
+ + {props.isLoading ? ( + } /> + ) : ( +
+ {' '} + {props.comments.map(el => { + return ( +
{ + props.onSelect(el); + }} + > + {el} +
+ ); + })}{' '} +
+ )} +
+ ); +}; + +export default GradingCommentSelector; diff --git a/src/pages/academy/grading/subcomponents/GradingEditor.tsx b/src/pages/academy/grading/subcomponents/GradingEditor.tsx index 3c162606dd..542ab7ab8b 100644 --- a/src/pages/academy/grading/subcomponents/GradingEditor.tsx +++ b/src/pages/academy/grading/subcomponents/GradingEditor.tsx @@ -13,11 +13,15 @@ import { IconNames } from '@blueprintjs/icons'; import React, { useEffect, useMemo, useState } from 'react'; import ReactMde, { ReactMdeProps } from 'react-mde'; import { useDispatch } from 'react-redux'; +import { useTokens } from 'src/commons/utils/Hooks'; import SessionActions from '../../../../commons/application/actions/SessionActions'; import ControlButton from '../../../../commons/ControlButton'; import Markdown from '../../../../commons/Markdown'; import { Prompt } from '../../../../commons/ReactRouterPrompt'; +import { postGenerateComments } from '../../../../commons/sagas/RequestsSaga'; +import { saveFinalComment } from '../../../../commons/sagas/RequestsSaga'; +import { saveChosenComments } from '../../../../commons/sagas/RequestsSaga'; import { getPrettyDate } from '../../../../commons/utils/DateHelper'; import { showSimpleConfirmDialog } from '../../../../commons/utils/DialogHelper'; import { @@ -25,6 +29,7 @@ import { showWarningMessage } from '../../../../commons/utils/notifications/NotificationsHelper'; import { convertParamToInt } from '../../../../commons/utils/ParamParseHelper'; +import GradingCommentSelector from './GradingCommentSelector'; type GradingSaveFunction = ( submissionId: number, @@ -42,6 +47,7 @@ type Props = { maxXp: number; studentNames: string[]; studentUsernames: string[]; + is_llm: boolean; comments: string; graderName?: string; gradedAt?: string; @@ -51,6 +57,7 @@ const gradingEditorButtonClass = 'grading-editor-button'; const GradingEditor: React.FC = props => { const dispatch = useDispatch(); + const tokens = useTokens(); const { handleGradingSave, handleGradingSaveAndContinue, handleReautogradeAnswer } = useMemo( () => ({ @@ -101,6 +108,34 @@ const GradingEditor: React.FC = props => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.submissionId, props.questionId]); + const getCommentSuggestions = async () => { + const resp = await postGenerateComments(tokens, props.submissionId, props.questionId); + return resp; + }; + + const onSelectGeneratedComments = (comment: string) => { + if (!selectedSuggestions.includes(comment)) { + setSelectedSuggestions([comment, ...selectedSuggestions]); + } + + setEditorValue(editorValue + comment); + }; + + const postSaveFinalComment = async (comment: string) => { + const resp = await saveFinalComment(tokens, props.submissionId, props.questionId, comment); + return resp; + }; + + const postSaveChosenComments = async (comments: string[]) => { + const resp = await saveChosenComments(tokens, props.submissionId, props.questionId, comments); + + return resp; + }; + + const [suggestions, setSuggestions] = useState([]); + const [selectedSuggestions, setSelectedSuggestions] = useState([]); + const [hasClickedGenerate, setHasClickedGenerate] = useState(false); + const makeInitialState = () => { setXpAdjustmentInput(props.xpAdjustment.toString()); setEditorValue(props.comments); @@ -129,6 +164,8 @@ const GradingEditor: React.FC = props => { () => { const newXpAdjustmentInput = convertParamToInt(xpAdjustmentInput || undefined) || undefined; const xp = props.initialXp + (newXpAdjustmentInput || 0); + postSaveFinalComment(editorValue); + postSaveChosenComments(selectedSuggestions); if (xp < 0 || xp > props.maxXp) { showWarningMessage( `XP ${xp.toString()} is out of bounds. Maximum xp is ${props.maxXp.toString()}.` @@ -304,6 +341,26 @@ const GradingEditor: React.FC = props => { + {props.is_llm && ( +
+ + +
+ )} +
= props => { const grading = useTypedSelector(state => state.session.gradings[props.submissionId]); const courseId = useTypedSelector(state => state.session.courseId); + const llm_grading = useTypedSelector(state => state.session.enableLlmGrading); const { autogradingResults, isFolderModeEnabled, @@ -304,6 +305,7 @@ const GradingWorkspace: React.FC = props => { ? [grading!.answers[questionId].student.username] : grading!.answers[questionId].team!.map(member => member.username) } + is_llm={!!llm_grading && grading!.answers[questionId].question.type == 'programming'} comments={grading!.answers[questionId].grade.comments ?? ''} graderName={ grading!.answers[questionId].grade.grader diff --git a/src/pages/localStorage.ts b/src/pages/localStorage.ts index 0f9cd6d2d3..66363b269c 100644 --- a/src/pages/localStorage.ts +++ b/src/pages/localStorage.ts @@ -69,6 +69,8 @@ export const saveState = (state: OverallState) => { enableAchievements: state.session.enableAchievements, enableSourcecast: state.session.enableSourcecast, enableStories: state.session.enableStories, + enableLlmGrading: state.session.enableLlmGrading, + llmApiKey: state.session.llmApiKey, moduleHelpText: state.session.moduleHelpText, assetsPrefix: state.session.assetsPrefix, assessmentConfigurations: state.session.assessmentConfigurations, diff --git a/src/styles/_workspace.scss b/src/styles/_workspace.scss old mode 100755 new mode 100644 index df4cbddc40..67739eb6fd --- a/src/styles/_workspace.scss +++ b/src/styles/_workspace.scss @@ -340,6 +340,35 @@ $code-color-notification: #f9f0d7; } } + .grading-comment-selector { + text-align: center; + display: flex !important; + justify-content: center; + flex-direction: column; + font-size: 0.875rem; + border-radius: 4px; + background-color: $cadet-color-1; + margin-top: 4px; + margin-bottom: 4px; + padding: 8px; + } + + .grading-comment-selector-title { + margin-bottom: 8px; + } + + .grading-comment-selector-item { + margin: 4px; + padding: 4px; + background-color: $cadet-color-2; + border: 1px solid $cadet-color-3; + cursor: pointer; + + &:hover { + background-color: $cadet-color-3; + } + } + .react-mde-parent { margin-bottom: 12px; }