CLASSROOMIO-T-219: AI generated exercise

delete sql

Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio into CLASSROOMIO-T-219

remove console

CLASSROOMIO-T-219: AI generation from note + apps improvements

CLASSROOMIO-T-219: Quizify Func in Exercise


Co-authored-by: Rotimi Best <[email protected]>
Co-authored-by: Tunji Adeyemi <[email protected]>


Merge-request: CLASSROOMIO-MR-104
Merged-by: Best Admin <[email protected]>
This commit is contained in:
tunny17
2023-10-15 04:38:37 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent 575d4fca25
commit e48410b241
10 changed files with 278 additions and 150 deletions
+1 -1
View File
@@ -13,4 +13,4 @@ package-lock.json
# Sentry Auth Token
.env.sentry-build-plugin
/supascripts/
/_supascripts/
@@ -194,7 +194,7 @@
</div>
</PageNav>
<div bind:this={bodyRef} class="overflow-y-auto h-[87%] pb-10 px-2 md:min-w-[300px]">
<div bind:this={bodyRef} class="overflow-y-auto h-[87%] pb-10 px-2 md:min-w-[350px] w-full">
{#each comments as comment}
<div class="pb-2 mt-2">
<div class="flex items-start">
@@ -155,7 +155,7 @@
</div>
</PageNav>
<div class="m-2 overlow-y-auto min-w-[350px]">
<div class="m-2 overlow-y-auto md:min-w-[350px] w-full">
{#if shouldCreatePoll}
<CreatePollForm onSubmit={handlePollCreate} onCancel={handleCreatePoll} />
{:else}
+46 -123
View File
@@ -1,9 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { writable } from 'svelte/store';
import { onMount } from 'svelte';
import hotkeys from 'hotkeys-js';
import Forum from 'carbon-icons-svelte/lib/Forum.svelte';
import AlignBoxTopLeft from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import { fade } from 'svelte/transition';
import SendAlt from 'carbon-icons-svelte/lib/SendAlt.svelte';
import ChartPie from 'carbon-icons-svelte/lib/ChartPie.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
@@ -19,14 +17,8 @@
import { browser } from '$app/environment';
import { lesson } from '$lib/components/Course/components/Lesson/store/lessons';
let resize = false;
let isDragging = false;
let isDown = writable(false);
let startX: number;
let initialWidth: number;
let appBarRef: HTMLDivElement;
let appContentRef: HTMLDivElement;
let topPadding = $apps.isStudent ? '48px' : '109px';
const getResizableSidebar = () => {
@@ -37,21 +29,12 @@
function handleClose() {
$apps.selectedApp = undefined;
if (getResizableSidebar()) {
appBarRef.style.width = '60px';
} else if (!getResizableSidebar() && $apps.isStudent) {
if ($apps.isStudent) {
$apps.open = false;
}
}
function handleAppClick(appName?: string) {
if (!$apps.selectedApp && getResizableSidebar()) {
if (window.innerWidth <= 1024) {
appBarRef.style.width = '300px';
} else {
appBarRef.style.width = '500px';
}
}
if (appName === $apps.selectedApp) {
handleClose();
} else {
@@ -60,101 +43,38 @@
$apps.dropdown = false;
}
function handleCursor(event: MouseEvent) {
if (!getResizableSidebar()) return;
if (!resize && $apps.selectedApp) {
const isNearLeftBorder = event.clientX - appContentRef.getBoundingClientRect().left < 8;
const isNearRightBorder = appContentRef.getBoundingClientRect().right - event.clientX < 8;
if (isNearLeftBorder || isNearRightBorder) {
appContentRef.style.cursor = 'ew-resize';
} else {
appContentRef.style.cursor = 'auto';
}
}
}
function startDragging(event: MouseEvent) {
$isDown = true;
setTimeout(() => {
if (event.button === 0 && $apps.selectedApp && $isDown) {
event.preventDefault();
const isNearLeftBorder = event.clientX - appContentRef.getBoundingClientRect().left < 5;
const isNearRightBorder = appContentRef.getBoundingClientRect().right - event.clientX < 5;
if (
(isNearRightBorder || isNearLeftBorder) &&
event.clientX >= 0 &&
event.clientX <= window.innerWidth
) {
isDragging = true;
resize = true;
startX = event.clientX;
initialWidth = parseInt(getComputedStyle(appBarRef).width, 10) + 5;
}
}
}, 100);
}
function stopDragging() {
isDragging = false;
resize = false;
$isDown = false;
}
function dragSidebar(event: MouseEvent) {
if (!getResizableSidebar()) return;
if (!isDragging || !$apps.selectedApp) return;
const deltaX = startX - event.clientX + 60;
let newWidth = initialWidth + deltaX;
if (newWidth <= 250) {
handleClose();
appBarRef.style.width = '60px';
appContentRef.style.width = '0';
} else {
appBarRef.style.width = newWidth + 'px';
}
}
function updateTopPadding(isStudent: boolean) {
topPadding = isStudent ? '48px' : '109px';
}
function getAppClass(appName: string, selected: string | undefined) {
return `transition duration-500 delay-150 ease-in-out border-t-0 border-b-0 border-r-0 ${
appName === selected ? 'border-l-4 border-indigo-600 focus:border-indigo-600' : 'border-l-0'
}`;
}
onMount(() => {
updateTopPadding($apps.isStudent);
if (getResizableSidebar()) {
appBarRef.addEventListener('mousedown', startDragging);
document.addEventListener('mousemove', dragSidebar);
document.addEventListener('mouseup', stopDragging);
document.addEventListener('mousemove', handleCursor);
}
hotkeys('A+1,A+2,A+3,A+4', function (event, handler) {
hotkeys('A+1,A+2', function (event, handler) {
event.preventDefault();
switch (handler.key) {
case 'A+1':
handleAppClick(APPS_CONSTANTS.APPS.COMMENTS);
break;
case 'A+2':
handleAppClick(APPS_CONSTANTS.APPS.QANDA);
break;
case 'A+3':
handleAppClick(APPS_CONSTANTS.APPS.NOTES);
break;
case 'A+4':
handleAppClick(APPS_CONSTANTS.APPS.POLL);
break;
// case 'A+3':
// handleAppClick(APPS_CONSTANTS.APPS.NOTES);
// break;
// case 'A+4':
// handleAppClick(APPS_CONSTANTS.APPS.QANDA);
// break;
}
});
});
onDestroy(() => {
if (getResizableSidebar()) {
appBarRef.removeEventListener('mousedown', startDragging);
document.removeEventListener('mousemove', dragSidebar);
document.removeEventListener('mouseup', stopDragging);
document.removeEventListener('mousemove', handleCursor);
}
});
$: updateTopPadding($apps.isStudent);
$: getResizableSidebar();
</script>
@@ -178,21 +98,32 @@
</IconButton>
</div>
<div class="mb-2 relative">
<div class="mb-2 relative {getAppClass(APPS_CONSTANTS.APPS.COMMENTS, $apps.selectedApp)}">
<IconButton
toolTipProps={{ title: 'Comments', hotkeys: ['A', '1'] }}
value={APPS_CONSTANTS.APPS.COMMENTS}
onClick={handleAppClick}
contained={APPS_CONSTANTS.APPS.COMMENTS === $apps.selectedApp}
buttonClassName="relative"
>
<Chip value={$lesson.totalComments} className="absolute -top-2 right-0 bg-primary-500" />
<Chip
value={$lesson.totalComments}
className="absolute -top-1 right-0 bg-primary-600 text-white"
/>
<SendAlt size={24} class="carbon-icon dark:text-white" />
</IconButton>
</div>
<div class="mb-2 {getAppClass(APPS_CONSTANTS.APPS.POLL, $apps.selectedApp)}">
<IconButton
toolTipProps={{ title: 'Poll', hotkeys: ['A', '2'] }}
value={APPS_CONSTANTS.APPS.POLL}
onClick={handleAppClick}
>
<ChartPie size={24} class="carbon-icon dark:text-white" />
</IconButton>
</div>
<!-- <div class="mb-2">
<IconButton
toolTipProps={{ title: 'QandA', hotkeys: ['A', '2'] }}
toolTipProps={{ title: 'QandA', hotkeys: ['A', '3'] }}
value={APPS_CONSTANTS.APPS.QANDA}
onClick={handleAppClick}
contained={APPS_CONSTANTS.APPS.QANDA === $apps.selectedApp}
@@ -210,34 +141,26 @@
<AlignBoxTopLeft size={24} class="carbon-icon dark:text-white" />
</IconButton>
</div> -->
<div class="mb-2">
<IconButton
toolTipProps={{ title: 'Poll', hotkeys: ['A', '4'] }}
value={APPS_CONSTANTS.APPS.POLL}
onClick={handleAppClick}
contained={APPS_CONSTANTS.APPS.POLL === $apps.selectedApp}
>
<ChartPie size={24} class="carbon-icon dark:text-white" />
</IconButton>
</div>
</div>
{#if !!$apps.selectedApp}
<div
class={`app cursor-auto lg:cursor-ew-resize ${
resize && ' border-l-8 border-l-blue-500 transition-none'
} `}
class={`app cursor-auto lg:cursor-ew-resize max-w-[350px] transition ease-in-out delay-150 duration-100`}
bind:this={appContentRef}
>
{#if $apps.selectedApp === APPS_CONSTANTS.APPS.QANDA}
<QandA {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.COMMENTS}
<Comments {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.POLL}
<Poll {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.NOTES}
<Notes {handleClose} />
{/if}
{#key $apps.selectedApp}
<div class="h-full w-full" transition:fade={{ duration: 200 }}>
{#if $apps.selectedApp === APPS_CONSTANTS.APPS.QANDA}
<QandA {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.COMMENTS}
<Comments {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.POLL}
<Poll {handleClose} />
{:else if $apps.selectedApp === APPS_CONSTANTS.APPS.NOTES}
<Notes {handleClose} />
{/if}
</div>
{/key}
</div>
{/if}
</div>
@@ -8,11 +8,19 @@
import { Tag } from 'carbon-components-svelte';
import { TEMPLATES, TAGS } from '$lib/mocks';
import type { ExerciseTemplate } from '$lib/utils/types';
import { lesson } from '../store/lessons';
import { useCompletion } from 'ai/svelte';
import Confetti from '$lib/components/Confetti/index.svelte';
import { toggleConfetti } from '$lib/components/Confetti/store';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { getTextFromHTML } from '$lib/utils/functions/course';
import { writable } from 'svelte/store';
import { Circle3 } from 'svelte-loading-spinners';
export let open = false;
export let handleAddExercise = () => {};
export let handleCancelAddExercise = () => {};
export let handleTemplateCreate = (template: ExerciseTemplate) => Promise<void>;
export let handleTemplateCreate: (template: ExerciseTemplate) => Promise<void>;
export let title = '';
enum Type {
@@ -23,6 +31,11 @@
let step = 0;
let type: Type = Type.SCRATCH;
let questionNumber = 5;
let optionNumber = 5;
let isLoading = writable(false);
let isAIStarted = false;
let note = '';
const options = [
{
@@ -41,7 +54,7 @@
title: 'Use AI',
subtitle: 'You can generate an exercise with AI from your notes',
type: Type.AI,
isDisabled: true
isDisabled: false
}
];
@@ -50,9 +63,43 @@
let selectedTemplateId = '';
let isTemplateFinishedLoading = false;
const { input, handleSubmit, completion } = useCompletion({
api: '/api/completion/exerciseprompt',
onFinish: async (prompt: string, completion: string) => {
$isLoading = false;
if (!$lesson.id) return;
toggleConfetti();
const template: ExerciseTemplate = JSON.parse($completion);
await handleTemplateCreate(template);
toggleConfetti();
}
});
function handleNext() {
step = 1;
step = step + 1;
}
function handleBack() {
step = step - 1;
}
function callAI() {
$input = JSON.stringify({
questionNumber,
optionNumber,
lessonNote: note
});
setTimeout(() => {
isAIStarted = true;
$isLoading = true;
handleSubmit({ preventDefault: () => {} });
}, 500);
}
$: note = getTextFromHTML($lesson?.materials?.note || '');
</script>
<Modal
@@ -62,6 +109,9 @@
maxWidth="max-w-4xl"
width="w-4/5"
>
{#if !$isLoading && isAIStarted}
<Confetti />
{/if}
{#if step === 0}
<div>
<h2 class="text-2xl font-medium my-5">How do you want to create your exercise?</h2>
@@ -119,7 +169,13 @@
className="my-4"
/>
<div class="mt-5 flex items-center flex-row-reverse">
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-3"
label="Back"
variant={VARIANTS.OUTLINED}
onClick={handleBack}
/>
<PrimaryButton className="px-6 py-3" label="Finish" onClick={handleAddExercise} />
</div>
</div>
@@ -170,7 +226,13 @@
{/each}
</div>
<div class="mt-5 flex items-center flex-row-reverse">
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-3"
label="Back"
variant={VARIANTS.OUTLINED}
onClick={handleBack}
/>
<PrimaryButton
isDisabled={!selectedTemplateId}
className="px-6 py-3"
@@ -189,8 +251,67 @@
</div>
</div>
</div>
{:else if type === Type.AI}
<div>
<div class="flex flex-row justify-between max-h-[500px]">
<div class="w-[60%] mr-1 border px-3 py-2 rounded-md">
{#if note.length}
<h3>Create exercises from Notes with AI</h3>
<p class="text-sm mb-4">
Choose how many questions and options you want and AI will help you create an
exercise out of your note. Let's go.
</p>
<TextField
label="How many questions do you want to create?"
type="number"
bind:value={questionNumber}
placeholder="5"
className="mb-2"
isRequired
/>
<TextField
label="How many options per questions do you want?"
type="number"
bind:value={optionNumber}
placeholder="5"
isRequired
/>
{:else}
<h3>Please add a note to use this feature</h3>
{/if}
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton
onClick={callAI}
isLoading={$isLoading}
isDisabled={$isLoading || !note}
variant={VARIANTS.OUTLINED}
>
Generate
</PrimaryButton>
</div>
</div>
<div
class="w-[40%] px-5 py-3 border rounded-md overflow-y-auto flex justify-center items-center"
>
{#if $isLoading}
<Circle3 size="60" unit="px" duration="1s" />
{:else if isAIStarted}
<p class="max-h-[200px] leading-7 text-sm">AI Generation Complete</p>
{:else}
<p class="max-h-[200px] leading-7 text-sm">Click "Generate" for some magic</p>
{/if}
</div>
</div>
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-3"
label="Back"
variant={VARIANTS.TEXT}
onClick={handleBack}
/>
<PrimaryButton className="px-6 py-3" label="Finish" onClick={handleAddExercise} />
</div>
</div>
{/if}
<div />
{/if}
</Modal>
@@ -30,21 +30,21 @@
description: ''
};
async function handleTemplateCreate(template: ExerciseTemplate) {
async function handleTemplateCreate(template: ExerciseTemplate): Promise<void> {
const newExercise = await createExerciseFromTemplate(lessonId, template);
console.log('newExercise', newExercise);
if (!newExercise) return;
if (newExercise) {
lesson.update((_lesson) => ({
..._lesson,
exercises: [..._lesson.exercises, newExercise]
}));
lesson.update((_lesson) => ({
..._lesson,
exercises: [..._lesson.exercises, newExercise]
}));
handleCancelAddExercise();
goto(path + '/' + newExercise.id);
handleCancelAddExercise();
goto(path + '/' + newExercise.id);
}
}
async function handleAddExercise() {
// createExerciseFromTemplate(lessonId);
const { data, error } = await createExercise({
title: newExercise.title,
lesson_id: lessonId
@@ -35,6 +35,7 @@
import ComponentVideo from './components/ComponentVideo.svelte';
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
import type { LessonPage } from '$lib/utils/types';
import { getTextFromHTML } from '$lib/utils/functions/course';
export let mode = MODES.view;
export let prevMode = '';
@@ -82,12 +83,7 @@
if (!document) return false;
const dummyDiv = document.createElement('div');
dummyDiv.innerHTML = note;
const rawText = dummyDiv.textContent?.trim();
return rawText === '';
return getTextFromHTML(note) === '';
}
function isMaterialsEmpty(materials: LessonPage['materials']) {
+7
View File
@@ -16,3 +16,10 @@ export const getStudentInviteLink = (_course: Course, orgSiteName: string) => {
return `https://${orgSiteName}.classroomio.com/invite/s/${hash}`;
};
export const getTextFromHTML = (html: string): string => {
const dummyDiv = document.createElement('div');
dummyDiv.innerHTML = html;
return dummyDiv.textContent?.trim() || '';
};
@@ -0,0 +1,81 @@
import { Configuration, OpenAIApi } from 'openai-edge';
import { OpenAIStream, StreamingTextResponse } from 'ai';
import { OPENAI_API_KEY } from '$env/static/private';
// 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 { questionNumber, optionNumber, lessonNote } = JSON.parse(prompt);
const refinedPrompt = `
Please note this typescript interface:
interface Question_type {
id: number;
label: any;
}
const QuestionTypes: Question_type[] = [
{
id: 1,
label: 'RADIO'
},
{
id: 2,
label: 'CHECKBOX'
},
{
id: 3,
label: 'TEXTAREA'
}
];
interface ExerciseTemplate {
title: string;
description: string;
questionnaire: {
questions: {
title: string;
name: string;
points: number; // a number out of 10 to score the student if student gets the correct answer
order: number; // a unique number from 0 to n+1, that orders the question in ascending order
question_type: Question_type;
options: {
label: string;
is_correct: boolean;
};
}[]
};
}
I am a teacher and I need your help evaluating my students after I have taught a lesson. Please generate a test with ${questionNumber} questions and ${optionNumber} options. The sourse of this test should come from my lesson note in thripple quotes: """${lessonNote}""". Format the quiz to match the ExerciseTemplate interface above and VERY IMPORTANTANTLY: ONLY RETURN JSON FORMAT AND DO NOT EXPLAIN ANTHING.
NB: Ask a mixture of 10 DIFFERENT Question Types(RADIO, CHECKBOX AND TEXTAREA), mark the correct answer.`;
const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content: 'You only spit out JSON and nothing else, dont give any explanation.'
},
{
role: 'user',
content: refinedPrompt
}
],
stream: true
});
// Convert the response into a friendly text-stream
const stream = OpenAIStream(response);
// Respond with the stream
return new StreamingTextResponse(stream);
}
@@ -232,7 +232,7 @@
},
{
label: 'Exercises',
badgeValue: $lesson.totalExercises,
badgeValue: $lesson.exercises.length,
isActive: !data.isMaterialsTabActive,
href: `${path}/exercises`
}