diff --git a/.gitignore b/.gitignore index d02bff94..4588a9b4 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ package-lock.json # Sentry Auth Token .env.sentry-build-plugin -/supascripts/ +/_supascripts/ diff --git a/src/lib/components/Apps/components/Comments/index.svelte b/src/lib/components/Apps/components/Comments/index.svelte index 56272ee6..440e1dff 100644 --- a/src/lib/components/Apps/components/Comments/index.svelte +++ b/src/lib/components/Apps/components/Comments/index.svelte @@ -194,7 +194,7 @@ -
+
{#each comments as comment}
diff --git a/src/lib/components/Apps/components/Poll/index.svelte b/src/lib/components/Apps/components/Poll/index.svelte index 853b0723..c9031615 100644 --- a/src/lib/components/Apps/components/Poll/index.svelte +++ b/src/lib/components/Apps/components/Poll/index.svelte @@ -155,7 +155,7 @@
-
+
{#if shouldCreatePoll} {:else} diff --git a/src/lib/components/Apps/index.svelte b/src/lib/components/Apps/index.svelte index 3bf3b9b0..e3288b44 100644 --- a/src/lib/components/Apps/index.svelte +++ b/src/lib/components/Apps/index.svelte @@ -1,9 +1,7 @@ @@ -178,21 +98,32 @@
-
+
- +
+
+ + + +
-
- - - -
{#if !!$apps.selectedApp}
- {#if $apps.selectedApp === APPS_CONSTANTS.APPS.QANDA} - - {:else if $apps.selectedApp === APPS_CONSTANTS.APPS.COMMENTS} - - {:else if $apps.selectedApp === APPS_CONSTANTS.APPS.POLL} - - {:else if $apps.selectedApp === APPS_CONSTANTS.APPS.NOTES} - - {/if} + {#key $apps.selectedApp} +
+ {#if $apps.selectedApp === APPS_CONSTANTS.APPS.QANDA} + + {:else if $apps.selectedApp === APPS_CONSTANTS.APPS.COMMENTS} + + {:else if $apps.selectedApp === APPS_CONSTANTS.APPS.POLL} + + {:else if $apps.selectedApp === APPS_CONSTANTS.APPS.NOTES} + + {/if} +
+ {/key}
{/if}
diff --git a/src/lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte b/src/lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte index 9a921205..8df0b366 100644 --- a/src/lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte +++ b/src/lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte @@ -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; + export let handleTemplateCreate: (template: ExerciseTemplate) => Promise; 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 || ''); + {#if !$isLoading && isAIStarted} + + {/if} {#if step === 0}

How do you want to create your exercise?

@@ -119,7 +169,13 @@ className="my-4" /> -
+
+
@@ -170,7 +226,13 @@ {/each}
-
+
+
+ {:else if type === Type.AI} +
+
+
+ {#if note.length} +

Create exercises from Notes with AI

+

+ Choose how many questions and options you want and AI will help you create an + exercise out of your note. Let's go. +

+ + + {:else} +

Please add a note to use this feature

+ {/if} +
+ + Generate + +
+
+
+ {#if $isLoading} + + {:else if isAIStarted} +

AI Generation Complete

+ {:else} +

Click "Generate" for some magic

+ {/if} +
+
+
+ + +
+
{/if} - -
{/if} diff --git a/src/lib/components/Course/components/Lesson/Exercises/index.svelte b/src/lib/components/Course/components/Lesson/Exercises/index.svelte index d3146971..f734d5a7 100644 --- a/src/lib/components/Course/components/Lesson/Exercises/index.svelte +++ b/src/lib/components/Course/components/Lesson/Exercises/index.svelte @@ -30,21 +30,21 @@ description: '' }; - async function handleTemplateCreate(template: ExerciseTemplate) { + async function handleTemplateCreate(template: ExerciseTemplate): Promise { 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 diff --git a/src/lib/components/Course/components/Lesson/Materials/index.svelte b/src/lib/components/Course/components/Lesson/Materials/index.svelte index 90c12cc6..9c412e28 100644 --- a/src/lib/components/Course/components/Lesson/Materials/index.svelte +++ b/src/lib/components/Course/components/Lesson/Materials/index.svelte @@ -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']) { diff --git a/src/lib/utils/functions/course.ts b/src/lib/utils/functions/course.ts index 1b3bbddd..fef3fdda 100644 --- a/src/lib/utils/functions/course.ts +++ b/src/lib/utils/functions/course.ts @@ -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() || ''; +}; diff --git a/src/routes/api/completion/exerciseprompt/+server.js b/src/routes/api/completion/exerciseprompt/+server.js new file mode 100644 index 00000000..7f09272c --- /dev/null +++ b/src/routes/api/completion/exerciseprompt/+server.js @@ -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); +} diff --git a/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte b/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte index 733d1f92..2c71ea56 100644 --- a/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte +++ b/src/routes/courses/[id]/lessons/[...lessonParams]/+page.svelte @@ -232,7 +232,7 @@ }, { label: 'Exercises', - badgeValue: $lesson.totalExercises, + badgeValue: $lesson.exercises.length, isActive: !data.isMaterialsTabActive, href: `${path}/exercises` }