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:
@@ -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)}>
|
||||
|
||||
+195
-56
@@ -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}
|
||||
|
||||
+38
-18
@@ -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);
|
||||
|
||||
+3
-1
@@ -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]}>
|
||||
|
||||
Reference in New Issue
Block a user