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 <[email protected]>
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]}>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10.1602" cy="9.43977" r="9.17119" transform="rotate(1.6909 10.1602 9.43977)" fill="#0233BD"/>
|
||||
<path d="M11.3852 5.35419C11.4728 5.03967 11.9232 5.05297 11.9921 5.3721L12.6384 8.36772C12.6629 8.48123 12.7483 8.57182 12.8602 8.60297L15.8125 9.42493C16.127 9.51249 16.1137 9.96289 15.7945 10.0317L12.7989 10.6781C12.6854 10.7026 12.5948 10.788 12.5637 10.8999L11.7417 13.8522C11.6542 14.1667 11.2038 14.1534 11.1349 13.8342L10.4885 10.8386C10.464 10.7251 10.3786 10.6345 10.2668 10.6034L7.31449 9.78141C6.99998 9.69385 7.01328 9.24345 7.33241 9.17459L10.328 8.52822C10.4415 8.50372 10.5321 8.41833 10.5633 8.30646L11.3852 5.35419Z" fill="white"/>
|
||||
<path d="M5.91584 5.19179C6.00341 4.87727 6.4538 4.89057 6.52266 5.2097L6.70599 6.05932C6.73048 6.17284 6.81588 6.26343 6.92775 6.29458L7.76508 6.5277C8.0796 6.61527 8.0663 7.06566 7.74717 7.13452L6.89754 7.31785C6.78403 7.34234 6.69344 7.42774 6.66229 7.53961L6.42916 8.37694C6.3416 8.69146 5.8912 8.67816 5.82234 8.35903L5.63902 7.50941C5.61452 7.39589 5.52913 7.3053 5.41726 7.27415L4.57992 7.04103C4.26541 6.95346 4.27871 6.50307 4.59784 6.43421L5.44746 6.25088C5.56098 6.22639 5.65157 6.14099 5.68272 6.02912L5.91584 5.19179Z" fill="white"/>
|
||||
<path d="M5.74983 10.8148C5.83739 10.5003 6.28779 10.5136 6.35665 10.8327L6.53997 11.6824C6.56447 11.7959 6.64986 11.8865 6.76174 11.9176L7.59907 12.1508C7.91358 12.2383 7.90028 12.6887 7.58115 12.7576L6.73153 12.9409C6.61801 12.9654 6.52742 13.0508 6.49627 13.1627L6.26315 14C6.17558 14.3145 5.72519 14.3012 5.65633 13.9821L5.473 13.1325C5.44851 13.0189 5.36311 12.9283 5.25124 12.8972L4.41391 12.6641C4.0994 12.5765 4.11269 12.1261 4.43182 12.0573L5.28145 11.8739C5.39496 11.8494 5.48555 11.764 5.5167 11.6522L5.74983 10.8148Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,37 @@
|
||||
<svg width="82" height="106" viewBox="0 0 82 106" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_dddd_5412_25762)">
|
||||
<rect x="13" y="4" width="56" height="56" rx="28" fill="#0F62FE"/>
|
||||
<g clip-path="url(#clip0_5412_25762)">
|
||||
<path d="M41 15.1562C30.3674 15.1562 21.75 22.1569 21.75 30.7969C21.75 34.5266 23.3592 37.9404 26.0361 40.6249C25.0962 44.4147 21.953 47.791 21.9154 47.8286C21.75 48.0016 21.7049 48.2572 21.8026 48.4828C21.9004 48.7084 22.1109 48.8438 22.3516 48.8438C27.337 48.8438 31.0742 46.4525 32.924 44.9787C35.3829 45.9036 38.1125 46.4375 41 46.4375C51.6326 46.4375 60.25 39.4368 60.25 30.7969C60.25 22.1569 51.6326 15.1562 41 15.1562ZM31.375 33.2031C30.044 33.2031 28.9687 32.1278 28.9687 30.7969C28.9687 29.4659 30.044 28.3906 31.375 28.3906C32.706 28.3906 33.7812 29.4659 33.7812 30.7969C33.7812 32.1278 32.706 33.2031 31.375 33.2031ZM41 33.2031C39.669 33.2031 38.5938 32.1278 38.5938 30.7969C38.5938 29.4659 39.669 28.3906 41 28.3906C42.331 28.3906 43.4062 29.4659 43.4062 30.7969C43.4062 32.1278 42.331 33.2031 41 33.2031ZM50.625 33.2031C49.294 33.2031 48.2188 32.1278 48.2188 30.7969C48.2188 29.4659 49.294 28.3906 50.625 28.3906C51.956 28.3906 53.0312 29.4659 53.0312 30.7969C53.0312 32.1278 51.956 33.2031 50.625 33.2031Z" fill="#F5F8FE"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dddd_5412_25762" x="0.555554" y="0.888889" width="80.8889" height="104.222" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="1.55556"/>
|
||||
<feGaussianBlur stdDeviation="2.33333"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0156863 0 0 0 0 0.501961 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5412_25762"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="7.77778"/>
|
||||
<feGaussianBlur stdDeviation="3.88889"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0156863 0 0 0 0 0.501961 0 0 0 0.17 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow_5412_25762" result="effect2_dropShadow_5412_25762"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="18.6667"/>
|
||||
<feGaussianBlur stdDeviation="5.44445"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0156863 0 0 0 0 0.501961 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="effect2_dropShadow_5412_25762" result="effect3_dropShadow_5412_25762"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="32.6667"/>
|
||||
<feGaussianBlur stdDeviation="6.22222"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0.0156863 0 0 0 0 0.501961 0 0 0 0.03 0"/>
|
||||
<feBlend mode="normal" in2="effect3_dropShadow_5412_25762" result="effect4_dropShadow_5412_25762"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow_5412_25762" result="shape"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_5412_25762">
|
||||
<rect width="38.5" height="38.5" fill="white" transform="translate(21.75 12.75)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -13,7 +13,7 @@
|
||||
"Unlimited Q/A",
|
||||
"50 Students",
|
||||
"AI Course Builder (no video upload)",
|
||||
"Live chat & polls"
|
||||
"ClassroomIO branding"
|
||||
],
|
||||
"CTA": {
|
||||
"LABEL": "Signup Now",
|
||||
@@ -37,7 +37,7 @@
|
||||
"Unlimted Teachers",
|
||||
"10K Students",
|
||||
"Advanced AI Course Builder (Video upload & Certificate)",
|
||||
"No ClassroomIO Branding",
|
||||
"Custom Branding",
|
||||
"Slack Invite",
|
||||
"Includes all Upcoming Features"
|
||||
],
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
alter table "public"."submission" add column "feedback" text;
|
||||
Reference in New Issue
Block a user