feat: grade exercise with AI (#296)

* working on the UI of the grading

* working on the grading function of the radio and checkboxes

* ai grading fully implemented with dummy functions for generating scores

* working on the openai response

* working on the openai response

* finished with the ai_grading

* finished with the ai_grading

* creating new migration file

* working on reviews

* updated the translations

* improvements

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Em_Dev
2024-05-13 19:35:06 +01:00
committed by GitHub
co-authored by Rotimi Best
parent fc8b8e1898
commit e243564113
37 changed files with 688 additions and 150 deletions
@@ -161,7 +161,7 @@
class="w-[50px]"
bind:selected={question.question_type.id}
on:change={(e) => {
const id = parseInt(e.target.value);
const id = parseInt(e.target?.value);
question.question_type = QUESTION_TYPES.find((q) => q.id === id);
question.is_dirty = true;
}}
@@ -176,7 +176,7 @@
<div class="flex justify-between items-center my-3 w-3/5">
<TextArea
bind:value={question.code}
rows="2"
rows={2}
placeholder={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.write')}
/>
<IconButton value="write-code" onClick={() => handleCode(question.id, false)}>
@@ -1,24 +1,43 @@
<script>
import { onMount } from 'svelte';
<script lang="ts">
import Modal from '$lib/components/Modal/index.svelte';
import Select from '$lib/components/Form/Select.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Preview from './Preview.svelte';
import { SELECTABLE_STATUS } from './constants';
import { STATUS } from './constants';
import { snackbar } from '$lib/components/Snackbar/store';
import TextArea from '$lib/components/Form/TextArea.svelte';
import { Dropdown, Tag } from 'carbon-components-svelte';
import { useCompletion } from 'ai/svelte';
import { QUESTION_TYPE } from '$lib/components/Question/constants';
import { t } from '$lib/utils/functions/translations';
export let open = false;
export let onClose = () => {};
export let handleSave = () => {};
export let isGradeWithAI = false;
// export let submissionId;
export let data = {};
export let updateStatus = () => {};
let status = SELECTABLE_STATUS[0];
const SELECTABLE_STATUS = [
{
id: STATUS.SUBMITTED,
text: $t('course.navItem.submissions.submission_status.submitted')
},
{
id: STATUS.IN_PROGRESS,
text: $t('course.navItem.submissions.submission_status.in_progress')
},
{
id: STATUS.GRADED,
text: $t('course.navItem.submissions.submission_status.graded')
}
];
let status = SELECTABLE_STATUS[0];
let selectedId = status.id;
let reasons = {};
let isLoading = false;
let total = 0;
let maxPoints = 0;
@@ -26,44 +45,107 @@
return (questions || []).reduce((acc, question) => acc + question.points, 0);
}
function calculateTotal(grades) {
function calculateTotal(grades: Record<string, string>): number {
if (!grades) return 0;
return Object.values(grades).reduce((acc, grade) => acc + parseInt(grade), 0);
return Object.values(grades).reduce((acc, grade) => acc + parseInt(grade || '0'), 0);
}
function handleStatusChange() {
function handleStatusChange(event) {
const newSelectedId = event.detail.selectedId;
setStatus({
status_id: newSelectedId
});
updateStatus({
submissionId: data.id,
prevStatusId: data.status_id,
nextStatusId: status.value,
nextStatusId: status.id,
total
});
snackbar.success(`${$t('snackbar.exercise.submission_updated')} '${status.label}'`);
snackbar.success(`${$t('snackbar.exercise.submission_updated')} '${status.text}'`);
}
function setStatus(data) {
status = SELECTABLE_STATUS.find((status) => status.value === data.status_id);
const statusBySelectedId = SELECTABLE_STATUS.find((status) => status.id === data.status_id);
if (!statusBySelectedId) {
return;
}
status = statusBySelectedId;
if (data.status_id !== selectedId) {
selectedId = data.status_id;
}
}
function getStatusColor(status) {
// if (!status)
return '';
const { input, handleSubmit, completion } = useCompletion({
api: '/api/completion/gradingprompt',
onFinish: async () => {
try {
const responseData = $completion.replace('```json', '').replace('```', '');
// switch (status.value) {
// case STATUS.SUBMITTED:
// return '';
// case STATUS.IN_PROGRESS:
// return 'text-white bg-primary-700';
// case STATUS.GRADED:
// return 'text-white bg-green-700';
// }
}
let aiResponses = [];
try {
// Parse the modified response data as JSON
aiResponses = JSON.parse(responseData);
} catch (error) {
console.error('Error parsing AI response', error);
}
onMount(() => {
console.log('mounting');
data?.questions.forEach((question) => {
const { id, points, question_type_id } = question;
if (question_type_id !== QUESTION_TYPE.TEXTAREA) {
const answer = data.questionAnswers.find((q) => q.question_id === id);
reasons = {
...reasons,
[id]: `${$t('course.navItem.submissions.grading_modal.questions_tried')} ${
answer.answers.length
} `
};
data.questionAnswerByPoint[id] = points / answer.answers.length;
} else if (aiResponses.length) {
const graded = aiResponses.find((res) => res.id === id);
reasons = {
...reasons,
[id]: `${graded?.explanation}`
};
data.questionAnswerByPoint[id] = graded.score;
}
});
} catch (error) {
console.error('Error', error);
} finally {
isLoading = false;
}
}
});
function gradeWithAI() {
isGradeWithAI = true;
isLoading = true;
const paragraphAiInput = data.questions
.filter((q) => q.question_type_id === QUESTION_TYPE.TEXTAREA)
.map((q) => {
const answer = data.questionAnswers.find((qA) => qA.question_id === q.id); // { open_answer: '' }
return {
id: q.id,
question: q.title,
answer: answer?.open_answer,
point: q.points
};
});
$input = JSON.stringify(paragraphAiInput);
handleSubmit({ preventDefault: () => {} });
}
$: total = calculateTotal(data.questionAnswerByPoint);
$: maxPoints = getMaxPoints(data.questions);
$: setStatus(data);
@@ -73,10 +155,12 @@
modalHeading={data.title}
bind:open
{onClose}
width="w-4/5"
containerClass="flex items-start"
width="w-4/5 h-[90%]"
containerClass="flex items-start !max-h-full h-[85%] py-0 px-4"
headerClass="py-2"
labelClass="text-base font-semibold"
>
<div class="w-full">
<div class="w-full h-full">
<Preview
questions={Array.isArray(data.questions)
? data.questions.sort((a, b) => a.order - b.order)
@@ -86,26 +170,41 @@
isFinished: true
}}
bind:grades={data.questionAnswerByPoint}
bind:reasons
bind:isGradeWithAI
bind:isLoading
disableGrading={false}
/>
</div>
<div class="ml-4 w-2/5 sticky top-0">
<div class="border border-gray-300 rounded-md mt-2">
<div class="border border-gray-300 rounded-md">
<div
class="hover:bg-gray-100 dark:bg-neutral-800 border-b border-t-0 border-l-0 border-r-0 border-gray-300 p-3"
>
<p class="dark:text-white font-bold text-lg">
Details
<p class="dark:text-white font-bold text-base">
{$t('course.navItem.submissions.grading_modal.details')}
{#if data.isEarly}
<span class="ml-2 text-sm badge rounded-sm px-2 bg-green-500 text-white"> early </span>
<span class="ml-2 text-sm badge rounded-sm px-2 bg-green-500 text-white">
{$t('course.navItem.submissions.grading_modal.early')}</span
>
{:else}
<span class="ml-2 badge text-sm rounded-sm px-2 bg-red-500 text-white"> late </span>
<span class="ml-2 badge text-sm rounded-sm px-2 bg-red-500 text-white">
{$t('course.navItem.submissions.grading_modal.late')}
</span>
{/if}
</p>
</div>
<div class="flex items-center text-sm p-3">
<p class="dark:text-white font-bold w-2/5">Total grade</p>
<p class="dark:text-white">{total} / {maxPoints}</p>
<div class="flex items-center space-x-4 text-sm px-3 py-2">
<p class="dark:text-white text-sm text-gray-500 font-semibold">
{$t('course.navItem.submissions.grading_modal.total_grade')}:
</p>
<Tag
class="dark:text-white font-semibold text-black bg-gray-100 dark:bg-neutral-700 rounded-md w-fit"
>
{total}/{maxPoints}
</Tag>
</div>
<!-- <div class="flex items-center text-sm p-3">
<p class="dark:text-white font-bold w-1/2">Status</p>
@@ -114,35 +213,75 @@
<p class="dark:text-white">Grading</p>
</div>
</div> -->
<div class="flex items-center text-sm p-3">
<p class="dark:text-white font-bold w-2/5">Student</p>
<div class="flex items-center space-x-4 text-sm px-3 py-2">
<p class="dark:text-white text-sm text-gray-500 font-semibold">
{$t('course.navItem.submissions.grading_modal.student')}:
</p>
{#if data.student}
<img
alt="Student avatar"
class="block rounded-full h-6 w-6"
src={data.student.avatar_url}
/>
<p class="dark:text-white ml-2 text-sm">{data.student.fullname}</p>
<div
class="flex flex-row justify-center items-center bg-gray-100 dark:bg-neutral-700 rounded-md p-[6px]"
>
<img
alt="Student avatar"
class="flex rounded-full h-5 w-5"
src={data.student.avatar_url}
/>
<p class="dark:text-white font-semibold ml-2 text-sm line-clamp-1">
{data.student.fullname}
</p>
</div>
{/if}
</div>
<div class="flex items-center text-sm p-3">
<p class="dark:text-white font-bold w-2/5">Status</p>
<Select
bind:value={status}
options={SELECTABLE_STATUS}
selectClassName={getStatusColor(status)}
onChange={handleStatusChange}
className="w-full"
<!-- <div class="flex items-center space-x-4 text-sm px-3 py-2">
<p class="dark:text-white text-sm text-gray-500 font-semibold">Assesment Type:</p>
<Tag
class="dark:text-white font-semibold bg-gray-100 dark:bg-neutral-700 rounded-md text-black w-fit"
>Paragraph</Tag
>
</div> -->
<div class="flex flex-col items-start text-sm px-3 py-2">
<p class="dark:text-white text-gray-500 font-semibold">
{$t('course.navItem.submissions.grading_modal.status')}:
</p>
<Dropdown
bind:selectedId
items={SELECTABLE_STATUS}
class="w-full"
on:select={handleStatusChange}
/>
</div>
<div class="flex items-center justify-center w-full p-3">
<div class="flex flex-col items-start text-sm px-3 py-2">
<p class="dark:text-white text-gray-500 font-semibold">
{$t('course.navItem.submissions.grading_modal.add_comment')}:
</p>
<TextArea
bgColor="bg-gray-100 dark:bg-neutral-700"
className="font-semibold"
placeholder={$t('course.navItem.submissions.grading_modal.add_comment_placeholder')}
bind:value={data.feedback}
/>
</div>
<div class="flex flex-col w-full space-y-3 px-3 py-2">
<PrimaryButton
onClick={gradeWithAI}
variant={VARIANTS.OUTLINED}
className="space-x-3 py-3 px-8 w-full "
>
<img src="/ai.svg" alt="ai" />
<p class="font-semibold text-sm">
{$t('course.navItem.submissions.grading_modal.grade_with_ai')}
</p>
</PrimaryButton>
<PrimaryButton
onClick={() => {
handleSave(data);
onClose();
}}
label="Submit Grades"
variant={VARIANTS.CONTAINED_SUCCESS}
label={$t('course.navItem.submissions.grading_modal.submit_grades')}
variant={VARIANTS.CONTAINED}
className="py-3 px-8 w-full"
/>
</div>
@@ -11,7 +11,10 @@
export let onSubmit = () => {};
export let onPrevious = () => {};
export let handleGrade = () => {};
export let disableGrading = false;
export let disableGrading = true;
export let isGradeWithAI = false;
export let isLoading = false;
export let reasons = {};
</script>
{#each questions as currentQuestion, currentQuestionIndex}
@@ -27,6 +30,9 @@
true
)}
bind:grade={grades[currentQuestion.id]}
bind:isGradeWithAI
bind:reason={reasons[currentQuestion.id]}
bind:isLoading
gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.id)}
{disableGrading}
@@ -44,6 +50,9 @@
true
)}
bind:grade={grades[currentQuestion.id]}
bind:isGradeWithAI
bind:reason={reasons[currentQuestion.id]}
bind:isLoading
gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.id)}
{disableGrading}
@@ -61,6 +70,9 @@
true
)}
bind:grade={grades[currentQuestion.id]}
bind:isGradeWithAI
bind:reason={reasons[currentQuestion.id]}
bind:isLoading
gradeMax={currentQuestion.points}
handleGrade={handleGrade(currentQuestion.id)}
{disableGrading}
@@ -26,9 +26,12 @@
} from '$lib/utils/services/notification/notification';
import { lesson } from '../store/lessons';
import { browser } from '$app/environment';
import { CloseFilled } from 'carbon-icons-svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
export let preview: false;
export let preview: boolean = false;
export let exerciseId = '';
export let isFetchingExercise = false;
let currentQuestion = {};
let renderProps = {};
@@ -36,6 +39,7 @@
let hasSubmission = false;
let isLoadingAutoSavedData = false;
let alreadyCheckedAutoSavedData = false;
let gradedComment = true;
function handleStart() {
$questionnaireMetaData.currentQuestionIndex += 1;
@@ -110,13 +114,14 @@
$questionnaireMetaData.status = 1;
$questionnaireMetaData.totalPossibleGrade = getTotalPossibleGrade($questionnaire.questions);
$questionnaireMetaData.grades = {};
submitExercise(
$questionnaireMetaData.answers,
questions,
exerciseId,
$course.id,
getGroupMemberId($group.people, $profile.id)
);
($questionnaireMetaData.comment = ''),
submitExercise(
$questionnaireMetaData.answers,
questions,
exerciseId,
$course.id,
getGroupMemberId($group.people, $profile.id)
);
notifyEducator();
}
}
@@ -140,7 +145,7 @@
}, 0);
}
async function checkForSubmission(people, profileId: string, courseId: string) {
async function checkForSubmission(people, profileId?: string, courseId?: string) {
if (!Array.isArray(people) || !profileId || !courseId || !!submission) {
return;
}
@@ -157,6 +162,7 @@
if (Array.isArray(data) && data.length) {
submission = data[0];
$questionnaireMetaData.answers = formatAnswers({
questions: $questionnaire.questions,
answers: submission.answers
@@ -168,6 +174,7 @@
$questionnaireMetaData.isFinished = true;
$questionnaireMetaData.status = submission.status_id;
$questionnaireMetaData.finalTotalGrade = 0;
$questionnaireMetaData.comment = submission.feedback;
$questionnaireMetaData.grades = submission.answers.reduce((acc, answer) => {
acc[answer.question_id] = answer.point;
$questionnaireMetaData.finalTotalGrade += answer.point;
@@ -194,6 +201,10 @@
alreadyCheckedAutoSavedData = true;
}
const toggleComments = () => {
gradedComment = !gradedComment;
};
$: browser && !alreadyCheckedAutoSavedData && getAutoSavedData();
// Reactive code
@@ -218,7 +229,8 @@
$questionnaireMetaData.currentQuestionIndex
);
}
$: checkForSubmission($group.people, $profile.id, $course.id);
$: !isFetchingExercise && checkForSubmission($group.people, $profile.id, $course.id);
</script>
{#if !preview && $questionnaire.questions.length && !$questionnaireMetaData.isFinished}
@@ -281,18 +293,18 @@
{:else if $questionnaireMetaData.isFinished}
{#if !isLoadingAutoSavedData}
<div class="flex items-center justify-between">
<div class="flex flex-col justify-between w-full">
<h2 class="text-xl mb-2 mt-0">{$questionnaire.title}</h2>
<div class="flex flex-col lg:flex-row items-start lg:items-center lg:space-x-4 w-full">
<h2 class="text-xl font-normal">{$questionnaire.title}</h2>
{#if STATUS.GRADED === $questionnaireMetaData.status}
<span
class="status-text bg-green-700 text-white rounded-full py-3 px-6 text-center"
class="status-text bg-green-700 text-white py-1 px-2 text-center"
title="Status: Pending Review"
>
Graded
</span>
{:else}
<span
class="status-text bg-yellow-600 text-white rounded-full py-3 px-6 text-center"
class="status-text bg-yellow-600 text-white py-1 px-2 text-center"
title="Status: Pending Review"
>
Pending
@@ -301,13 +313,21 @@
</div>
{#if STATUS.GRADED === $questionnaireMetaData.status}
<span
class="p-5 border-2 border-gray-700 rounded-full h-24 w-24 flex items-center justify-center text-2xl"
class="p-6 border-2 border-gray-300 bg-[#F5F8FE] rounded-full h-10 w-10 flex items-center justify-center text-[#2751DA] text-sm font-semibold"
title="Status: Pending Review"
>
{$questionnaireMetaData.finalTotalGrade}/{$questionnaireMetaData.totalPossibleGrade}
</span>
{/if}
</div>
{#if $questionnaireMetaData.status === STATUS.GRADED && $questionnaireMetaData.comment}
<div
class="flex items-center justify-between bg-primary-700 p-4 text-white font-semibold rounded-sm mt-3"
>
<span> {$questionnaireMetaData.comment}</span>
</div>
{/if}
<Preview
questions={$questionnaire.questions.sort((a, b) => a.order - b.order)}
questionnaireMetaData={$questionnaireMetaData}
@@ -320,11 +340,11 @@
<!-- <div transition:fade id="question"> -->
<div in:fly={{ x: 500, duration: 1000 }} id="question">
{#if QUESTION_TYPE.RADIO === currentQuestion.question_type.id}
<RadioQuestion {...renderProps} key={currentQuestion.id} />
<RadioQuestion {...renderProps} key={currentQuestion.id} hideGrading={true} />
{:else if QUESTION_TYPE.CHECKBOX === currentQuestion.question_type.id}
<CheckboxQuestion {...renderProps} key={currentQuestion.id} />
<CheckboxQuestion {...renderProps} key={currentQuestion.id} hideGrading={true} />
{:else if QUESTION_TYPE.TEXTAREA === currentQuestion.question_type.id}
<TextareaQuestion {...renderProps} key={currentQuestion.id} />
<TextareaQuestion {...renderProps} key={currentQuestion.id} hideGrading={true} />
{/if}
</div>
{/key}
@@ -1,20 +1,7 @@
import { t } from '$lib/utils/functions/translations';
export const STATUS = {
SUBMITTED: 1,
IN_PROGRESS: 2,
GRADED: 3
};
export const SELECTABLE_STATUS = [
{
label: 'Submitted',
value: STATUS.SUBMITTED
},
{
label: 'In progress',
value: STATUS.IN_PROGRESS
},
{
label: 'Graded',
value: STATUS.GRADED
}
];
@@ -1,4 +1,4 @@
<script>
<script lang="ts">
import { onDestroy } from 'svelte';
import { goto } from '$app/navigation';
import AddAltIcon from 'carbon-icons-svelte/lib/AddAlt.svelte';
@@ -36,8 +36,9 @@
export let path = '';
export let goBack = () => {};
export let isStudent = true;
export let isFetching = false;
let preview = false;
let preview: boolean = false;
let shouldDeleteExercise = false;
let isSaving = false;
let selectedIndex = 0;
@@ -78,12 +79,11 @@
});
$: $questionnaire?.questions?.length < 1 && handleAddQuestion();
console.log('question', $questionnaire.questions);
</script>
<PageBody bind:isPageNavHidden={isStudent} padding="px-4 overflow-x-hidden">
<div class="bg-gray-100 dark:bg-neutral-800 top-0 z-10 sticky p-2 mb-3">
<Breadcrumb noTrailingSlash class="mb-5">
<Breadcrumb noTrailingSlash>
<BreadcrumbItem href={path}
>{$t('course.navItem.lessons.exercises.all_exercises.heading')}</BreadcrumbItem
>
@@ -157,7 +157,7 @@
{#if !isStudent && !preview}
<EditMode bind:shouldDeleteExercise {exerciseId} {goBack} />
{:else}
<ViewMode {preview} {exerciseId} />
<ViewMode {preview} {exerciseId} isFetchingExercise={isFetching} />
{/if}
{:else if selectedIndex === 1}
<Analytics bind:exerciseId />
@@ -151,7 +151,7 @@
{/if}
{#if exerciseId}
<Exercise {exerciseId} {goBack} bind:path bind:isStudent />
<Exercise {exerciseId} {goBack} bind:path bind:isStudent {isFetching} />
{:else}
<NewExerciseModal
bind:open
@@ -19,7 +19,8 @@ const initAnswerState = {
currentQuestionIndex: 0,
isFinished: false,
progressValue: 100,
status: STATUS.PENDING
status: STATUS.PENDING,
comment: ''
};
export const questionnaireMetaData = writable(initAnswerState);
@@ -170,7 +170,9 @@
isDisabled={!courseData.metadata.allowNewStudent}
/>
{#if courseData?.metadata?.showDiscount && courseData.metadata.allowNewStudent}
<p class="dark:text-white font-light text-sm text-gray-500">{$t('course.navItem.landing_page.pricing_section.bird')}</p>
<p class="dark:text-white font-light text-sm text-gray-500">
{$t('course.navItem.landing_page.pricing_section.bird')}
</p>
{/if}
</div>
</div>
@@ -53,10 +53,6 @@
// TODO: Delete course functionality
alert('WIP: Delete course');
}
onDestroy(() => {
console.log('unmounting card');
});
</script>
{#if !isLMS && !isOnLandingPage}
@@ -21,7 +21,7 @@
{/if}
<select
bind:value
class="form-select block w-auto mt-1 dark:bg-neutral-800 dark:text-white {selectClassName}"
class="form-select block w-auto mt-1 dark:bg-neutral-700 dark:text-white {selectClassName}"
required={isRequired}
on:blur={() => {}}
on:change={onChange}
@@ -17,8 +17,8 @@
export let autoFocus = false;
export let isRequired = false;
export let isDisabled = false;
export let min = undefined;
export let max = undefined;
export let min: string | number | null | undefined = undefined;
export let max: string | number | null | undefined = undefined;
export let errorMessage = '';
export let helperMessage = '';
export let autoComplete = true;
@@ -117,4 +117,14 @@
top: 30px;
right: 6px;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
/* input[type='number'] {
-moz-appearance: textfield;
} */
</style>
@@ -5,6 +5,8 @@
export let open = false;
export let onClose = () => {};
export let modalHeading = '';
export let headerClass = '';
export let labelClass = '';
export let width = '';
export let maxWidth = '';
export let containerClass = '';
@@ -37,9 +39,9 @@
role="presentation"
>
<div
class="flex items-center justify-between border border-l-0 border-r-0 border-t-0 border-gray-100 dark:border-neutral-600 p-4 px-5"
class="flex items-center justify-between border border-l-0 border-r-0 border-t-0 border-gray-100 dark:border-neutral-600 p-4 px-5 {headerClass}"
>
<p class="text-md m-0 font-medium dark:text-white">
<p class="text-md m-0 font-medium dark:text-white {labelClass}">
{modalHeading}
</p>
{#if isCloseable}
@@ -22,7 +22,8 @@
</script>
<div
class="header dark:bg-black dark:border-neutral-600 bg-white {!disableSticky &&
class="{hideOnMobile &&
'hideOnMobile'} header dark:bg-black dark:border-neutral-600 bg-white {!disableSticky &&
'sticky'} {dynamicRootClass}"
style={overidableStyle}
>
@@ -125,9 +126,13 @@
padding: 10px;
}
@media (max-width: 760px) {
@media (max-width: 1024px) {
.title {
width: fit-content;
}
.hideOnMobile {
display: none;
}
}
</style>
@@ -6,6 +6,7 @@
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte';
import { t } from '$lib/utils/functions/translations';
import ReasonBox from '../ReasonBox.svelte';
export let title = '';
export let index = 1;
@@ -22,6 +23,12 @@
export let grade: number | undefined;
export let gradeMax = 0;
export let disableGrading = false;
export let isGradeWithAI = false;
export let reason;
export let isLoading = false;
export let hideGrading = false;
let gradeWithAI = false;
function getVal(form, name) {
let values = [];
@@ -59,6 +66,17 @@
return '';
}
function acceptGrade() {
gradeWithAI = false;
}
function rejectGrade() {
gradeWithAI = false;
grade = 0;
}
$: {
gradeWithAI = isGradeWithAI;
}
</script>
<form on:submit|preventDefault={handleFormSubmit}>
@@ -73,7 +91,9 @@
</span>
</svelte:fragment>
</HtmlRender>
<Grade {gradeMax} bind:grade {disableGrading} />
{#if !hideGrading}
<Grade {gradeMax} bind:grade {disableGrading} />
{/if}
</div>
{#if code}
@@ -99,6 +119,10 @@
</button>
{/each}
</div>
{#if gradeWithAI}
<ReasonBox {reason} {isLoading} {acceptGrade} {rejectGrade} />
{/if}
{#if !isPreview}
<div class="mt-3 flex items-center justify-between w-full">
<PrimaryButton
@@ -1,23 +1,30 @@
<script lang="ts">
import TextField from '$lib/components/Form/TextField.svelte';
import { t } from '$lib/utils/functions/translations';
import { snackbar } from '../Snackbar/store';
export let gradeMax = 0;
export let disableGrading = false;
export let grade: number | undefined;
export let grade: number | undefined = 0;
$: if (grade && grade > gradeMax) {
snackbar.error('grade cant be more than max value');
grade = gradeMax;
}
</script>
{#if typeof grade !== 'undefined'}
<div class="flex items-center">
<TextField
placeholder={$t('course.navItem.lessons.exercises.new_exercise_modal.points')}
bind:value={grade}
type="number"
className="w-12"
isDisabled={disableGrading}
/>
<p class="dark:text-white ml-2 text-lg flex items-center">
<span class="mr-1">/</span> <span>{gradeMax}</span>
</p>
</div>
{/if}
<div class="flex items-center font-semibold">
<TextField
placeholder={$t('course.navItem.lessons.exercises.new_exercise_modal.points')}
bind:value={grade}
max={gradeMax}
type="number"
className="w-12"
inputClassName="h-10"
isDisabled={disableGrading}
/>
<p class="dark:text-white ml-2 text-base flex items-center font-semibold">
<span class="mr-1">/</span> <span>{gradeMax}</span>
</p>
</div>
@@ -6,6 +6,7 @@
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte';
import { t } from '$lib/utils/functions/translations';
import ReasonBox from '../ReasonBox.svelte';
export let title = '';
export let index = 1;
@@ -24,6 +25,12 @@
export let disableGrading = false;
export let labelClassName = '';
export let disableOptContainerMargin = false;
export let isGradeWithAI = false;
export let reason;
export let isLoading = false;
export let hideGrading = false;
let gradeWithAI = false;
function getRadioVal(form, name) {
let val;
@@ -61,6 +68,18 @@
return '';
}
function acceptGrade() {
gradeWithAI = false;
}
function rejectGrade() {
gradeWithAI = false;
grade = 0;
}
$: {
gradeWithAI = isGradeWithAI;
}
</script>
<form on:submit|preventDefault={handleFormSubmit}>
@@ -73,14 +92,16 @@
</span>
</svelte:fragment>
</HtmlRender>
<Grade {gradeMax} bind:grade {disableGrading} />
{#if !hideGrading}
<Grade {gradeMax} bind:grade {disableGrading} />
{/if}
</div>
{#if code}
<CodeSnippet {code} />
{/if}
<div class={!disableOptContainerMargin && 'ml-4'}>
<div class={disableOptContainerMargin ? '' : 'ml-4'}>
{#each options as option}
<button
class="cursor-pointer text-left my-2 border-2 border-gray-300 rounded-md hover:bg-gray-200 dark:hover:bg-neutral-800 w-full {getValidationClassName(
@@ -99,6 +120,9 @@
</button>
{/each}
</div>
{#if gradeWithAI}
<ReasonBox {reason} {isLoading} {acceptGrade} {rejectGrade} />
{/if}
{#if !isPreview}
<div class="mt-3 flex items-center justify-between w-full">
@@ -0,0 +1,53 @@
<script>
import { VARIANTS } from '../PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { t } from '$lib/utils/functions/translations';
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
export let reason = '';
export let isLoading = false;
export let rejectGrade = () => {};
export let acceptGrade = () => {};
</script>
<div class="border rounded-md">
{#if isLoading}
<div class="flex w-full px-2 py-4">
<div class="w-full flex flex-row items-start space-x-4">
<img src="/ai.svg" alt="alt" class="animate-pulse" />
<div class="w-full">
<SkeletonText width="100%" paragraph lines={3} />
</div>
</div>
<div class="flex space-x-2">
<SkeletonPlaceholder style="height: 2rem; width: 5rem;" />
<SkeletonPlaceholder style="height: 2rem; width: 5rem;" />
</div>
</div>
{:else}
<div class="flex items-start justify-between px-2 py-4">
<div class="flex items-center space-x-4">
<img src="/ai.svg" alt="alt" />
<p class="font-normal text-sm">
{reason}
</p>
</div>
<div class="flex space-x-2">
<PrimaryButton
variant={VARIANTS.CONTAINED_SUCCESS}
label={$t('course.navItem.submissions.grading_modal.accept_grade')}
className="rounded-none py-1 px-2"
disablePadding={true}
onClick={acceptGrade}
/>
<PrimaryButton
variant={VARIANTS.CONTAINED_DANGER}
label={$t('course.navItem.submissions.grading_modal.reject_grade')}
className="rounded-none py-1 px-2"
disablePadding={true}
onClick={rejectGrade}
/>
</div>
</div>
{/if}
</div>
@@ -6,6 +6,7 @@
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import Grade from '$lib/components/Question/Grade.svelte';
import { t } from '$lib/utils/functions/translations';
import ReasonBox from '../ReasonBox.svelte';
export let title = '';
export let index = 1;
@@ -21,6 +22,12 @@
export let grade: number | undefined;
export let gradeMax = 0;
export let disableGrading = false;
export let isGradeWithAI = false;
export let reason;
export let isLoading = false;
export let hideGrading = false;
let gradeWithAI = false;
function handleFormSubmit(event) {
if (isPreview) return;
@@ -33,6 +40,18 @@
event.preventDefault();
onPrevious();
}
function acceptGrade() {
gradeWithAI = false;
}
function rejectGrade() {
gradeWithAI = false;
grade = 0;
}
$: {
gradeWithAI = isGradeWithAI;
}
</script>
<form on:submit|preventDefault={handleFormSubmit}>
@@ -45,7 +64,9 @@
</span>
</svelte:fragment>
</HtmlRender>
<Grade {gradeMax} bind:grade {disableGrading} />
{#if !hideGrading}
<Grade {gradeMax} bind:grade {disableGrading} />
{/if}
</div>
{#if code}
@@ -54,9 +75,12 @@
<div class="ml-4">
{#if disabled}
<div class="bg-gray-200 dark:bg-gray-500 p-5 rounded-md mb-3">
<div class="bg-gray-200 dark:bg-gray-500 py-3 px-5 rounded-md mb-3">
{defaultValue}
</div>
{#if gradeWithAI}
<ReasonBox {reason} {isLoading} {acceptGrade} {rejectGrade} />
{/if}
{:else}
<TextArea
bind:value={defaultValue}
@@ -42,7 +42,9 @@
class="flex justify-between items-center border-gray border-t-2 border-r-0 border-b-0 border-l-0 p-2"
>
<div class="flex items-center w-40">
<p class="dark:text-white text-sm mr-2">{$t('course.navItem.lessons.exercises.new_exercise_modal.points')}:</p>
<p class="dark:text-white text-sm mr-2">
{$t('course.navItem.lessons.exercises.new_exercise_modal.points')}:
</p>
<TextField
placeholder={$t('course.navItem.lessons.exercises.new_exercise_modal.points')}
bind:value={points}
@@ -14,7 +14,7 @@ import { STATUS } from '$lib/utils/constants/course';
import type { PostgrestSingleResponse } from '@supabase/supabase-js';
export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug']) {
let match: { slug?: string; id?: string; status?: string } = {};
const match: { slug?: string; id?: string; status?: string } = {};
if (slug) {
match.slug = slug;
@@ -24,6 +24,7 @@ export function fetchSubmissions(course_id: string) {
)
),
status_id,
feedback,
course:course_id(*),
groupmember:submitted_by(
profile(*)
@@ -66,6 +67,7 @@ export async function fetchSubmission({
id,
answers:question_answer(*),
status_id,
feedback,
submitted_by:groupmember!inner(
profile!inner(
id,
@@ -80,14 +82,21 @@ export async function fetchSubmission({
}
export async function updateSubmission(
{ id, status_id, total }: { id?: number; status_id?: number; total?: number },
{
id,
status_id,
total,
feedback
}: { id?: number; status_id?: number; total?: number; feedback?: string },
otherArgs?: any
) {
const toUpdate: {
status_id?: number;
total?: number;
feedback?: string;
} = {
status_id
status_id,
feedback
};
if (typeof total === 'number') {
@@ -447,6 +447,21 @@
"submitted": "Eingereicht",
"in_progress": "Im Gange",
"graded": "Benotet"
},
"grading_modal": {
"details": "Einzelheiten",
"early": "Früh",
"late": "Spät",
"total_grade": "Gesamtnote",
"student": "Student",
"status": "Status",
"add_comment": "Einen Kommentar hinzufügen",
"grade_with_ai": "Benoten Sie mit KI",
"submit_grades": "Noten einreichen",
"accept_grade": "Akzeptieren",
"reject_grade": "Ablehnen",
"add_comment_placeholder": "Hinterlasse einen Kommentar",
"questions_tried": "Diese Punktzahl wurde aufgrund der Gesamtzahl der Versuche vergeben:"
}
},
"attendance": {
@@ -1137,4 +1152,4 @@
"seconds": "Sekunden",
"snackbar_error": "Fehler beim Senden des Codes"
}
}
}
@@ -716,6 +716,21 @@
"submitted": "Submitted",
"in_progress": "In Progress",
"graded": "Graded"
},
"grading_modal": {
"details": "Details",
"early": "Early",
"late": "Late",
"total_grade": "Total grade",
"student": "Student",
"status": "Status",
"add_comment": "Add comment",
"add_comment_placeholder": "Leave a comment",
"grade_with_ai": "Grade with AI",
"submit_grades": "Submit Grades",
"accept_grade": "Accept",
"reject_grade": "Reject",
"questions_tried": "Allocated this score because of total tries:"
}
},
@@ -447,6 +447,21 @@
"submitted": "Enviado",
"in_progress": "En curso",
"graded": "Calificado"
},
"grading_modal": {
"details": "Detalles",
"early": "Temprano",
"late": "Tarde",
"total_grade": "Nota total",
"student": "Alumno",
"status": "Estado",
"add_comment": "Agregar comentario",
"grade_with_ai": "Calificar con IA",
"submit_grades": "Enviar calificaciones",
"accept_grade": "Aceptar",
"reject_grade": "Rechazar",
"add_comment_placeholder": "Deja un comentario",
"questions_tried": "Se asignó esta puntuación debido al total de intentos:"
}
},
"attendance": {
@@ -1137,4 +1152,4 @@
"seconds": "segundos",
"snackbar_error": "error al enviar código"
}
}
}
@@ -447,6 +447,19 @@
"submitted": "Soumis",
"in_progress": "En cours",
"graded": "Noté"
},
"grading_modal": {
"details": "Détails",
"early": "Tôt",
"late": "En retard",
"total_grade": "Note totale",
"student": "Étudiant",
"status": "Statut",
"add_comment": "Ajouter un commentaire",
"grade_with_ai": "Noter avec l'IA",
"submit_grades": "Soumettre les notes",
"accept_grade": "Accepter",
"reject_grade": "Rejeter"
}
},
"attendance": {
@@ -1137,4 +1150,4 @@
"seconds": "secondes",
"snackbar_error": "erreur lors de l'envoi du code"
}
}
}
@@ -447,6 +447,21 @@
"submitted": "प्रस्तुत",
"in_progress": "प्रगति पर है",
"graded": "ग्रेड दिया गया"
},
"grading_modal": {
"details": "विवरण",
"early": "जल्दी",
"late": "देर",
"total_grade": "कुल ग्रेड",
"student": "विद्यार्थी",
"status": "स्थिति",
"add_comment": "टिप्पणी जोड़ना",
"grade_with_ai": "एआई के साथ ग्रेड",
"submit_grades": "ग्रेड जमा करें",
"accept_grade": "स्वीकार करना",
"reject_grade": "अस्वीकार करना",
"add_comment_placeholder": "एक टिप्पणी छोड़ें",
"questions_tried": "कुल प्रयासों के कारण यह स्कोर आवंटित किया गया:"
}
},
"attendance": {
@@ -1137,4 +1152,4 @@
"seconds": "सेकंड",
"snackbar_error": "कोड भेजते समय त्रुटि"
}
}
}
@@ -447,6 +447,21 @@
"submitted": "Submetido",
"in_progress": "Em andamento",
"graded": "Classificado"
},
"grading_modal": {
"details": "Detalhes",
"early": "Cedo",
"late": "Tarde",
"total_grade": "Nota total",
"student": "Estudante",
"status": "Status",
"add_comment": "Adicionar comentário",
"grade_with_ai": "Nota com IA",
"submit_grades": "Enviar notas",
"accept_grade": "Aceitar",
"reject_grade": "Rejeitar",
"add_comment_placeholder": "Deixe um comentário",
"questions_tried": "Alocada esta pontuação por causa do total de tentativas:"
}
},
"attendance": {
@@ -1137,4 +1152,4 @@
"seconds": "segundos",
"snackbar_error": "erro ao enviar código"
}
}
}
@@ -447,6 +447,21 @@
"submitted": "Поданный",
"in_progress": "В ходе выполнения",
"graded": "Оцененный"
},
"grading_modal": {
"details": "Подробности",
"early": "Рано",
"late": "Поздно",
"total_grade": "Общая оценка",
"student": "Студент",
"status": "Положение дел",
"add_comment": "Добавить комментарий",
"grade_with_ai": "Оценка с помощью ИИ",
"submit_grades": "Отправить оценки",
"accept_grade": "Принимать",
"reject_grade": "Отклонять",
"add_comment_placeholder": "Оставить комментарий",
"questions_tried": "Присвоен этот балл из-за общего количества попыток:"
}
},
"attendance": {
@@ -1137,4 +1152,4 @@
"seconds": "секунды",
"snackbar_error": "ошибка при отправке кода"
}
}
}
@@ -447,6 +447,19 @@
"submitted": "Đã gửi",
"in_progress": "Trong tiến trình",
"graded": "Đã chấm điểm"
},
"grading_modal": {
"details": "Chi tiết",
"early": "Sớm",
"late": "Muộn",
"total_grade": "Tổng điểm",
"student": "Học sinh",
"status": "Trạng thái",
"add_comment": "Thêm bình luận",
"grade_with_ai": "Chấm điểm bằng AI",
"submit_grades": "Gửi điểm",
"accept_grade": "Chấp nhận",
"reject_grade": "Từ chối"
}
},
"attendance": {
@@ -1137,4 +1150,4 @@
"seconds": "giây",
"snackbar_error": "lỗi khi gửi mã"
}
}
}
@@ -0,0 +1,51 @@
import { OPENAI_API_KEY } from '$env/static/private';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { Configuration, OpenAIApi } from 'openai-edge';
// Create an OpenAI API client (that's edge friendly!)
const openAIConfig = new Configuration({
apiKey: OPENAI_API_KEY
});
const openai = new OpenAIApi(openAIConfig);
// Set the runtime to edge for best performance
export const config = {
runtime: 'edge'
};
export async function POST({ request }) {
const { prompt } = await request.json();
const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content:
'You are a teaching assistant, you only spit out JSON and nothing else, dont give any explanation.'
},
{
role: 'user',
content: `
Given this array of objects below, there are 4 keys , id, question, answer and points. You are required to come up with a score between 0 and the point for each answer provided to the question and a simple explanation for giving that score, you are to return an array of object in json with the following keys; id which is the id provided for that object in the initial array, score which is the value you gave for that question and then your explanation in this format below:
{
id,
score,
explanation,
}
IMPORTANT: DO NOT GIVE ANY OTHER TEXT EXCEPT THE JSON
The array you need to grade is below:
${prompt}
`
}
],
stream: true
});
const stream = OpenAIStream(response);
return new StreamingTextResponse(stream);
}
@@ -368,9 +368,8 @@
{/if}
</PageBody>
{/if}
<!-- Mobile Navigation -->
<div class="fixed bottom-5 w-[90%] flex items-center justify-center lg:hidden">
<div class="sticky bottom-5 flex items-center justify-center lg:hidden">
<div
class="flex items-center gap-2 w-fit rounded-full shadow-xl bg-gray-100 dark:bg-neutral-700 px-5 py-1"
>
@@ -61,6 +61,7 @@
let submissionIdData: { [key: number]: any } = {};
let submissionId: string | number | null;
let openExercise = false;
let isGradeWithAI = false;
const submissionStatus: { [key: number]: string } = {
1: $t('course.navItem.submissions.submission_status.submitted'),
@@ -176,6 +177,7 @@
}
function handleModalClose() {
isGradeWithAI = false;
goto($page.url.pathname);
}
@@ -230,8 +232,12 @@
}
}
async function handleSave(submission: { questionAnswerByPoint: any; questionAnswers: any }) {
const { questionAnswerByPoint, questionAnswers } = submission;
async function handleSave(submission: {
questionAnswerByPoint: any;
questionAnswers: any;
feedback: any;
}) {
const { questionAnswerByPoint, questionAnswers, feedback } = submission;
let totalPoints = 0;
@@ -249,7 +255,8 @@
updateSubmission({
id: submissionId,
total: totalPoints
total: totalPoints,
feedback: feedback
}).then((res) => console.log('Updated submission', res));
snackbar.success('snackbar.submissions.success.grading');
@@ -268,7 +275,8 @@
const sectionById: { [key: number]: sectionType[] } = {};
if (submissions) {
for (const submission of submissions) {
const { id, created_at, exercise, course, answers, groupmember, status_id } = submission;
const { id, created_at, exercise, course, answers, groupmember, status_id, feedback } =
submission;
const isEarly = isSubmissionEarly(created_at, exercise.due_by);
@@ -276,6 +284,7 @@
id,
statusId: status_id,
isEarly,
feedback,
submittedAt: formatDate(created_at),
exercise: {
id: exercise.id,
@@ -296,6 +305,7 @@
submissionIdData[id] = {
id,
status_id,
feedback,
isEarly,
title: exercise.title,
student: submissionItem.student,
@@ -334,6 +344,7 @@
{handleSave}
{updateStatus}
{submissionId}
bind:isGradeWithAI
/>
<RoleBasedSecurity allowedRoles={[1, 2]}>