CLASSROOMIO-T-92: AI Powered lesson notes

This commit is contained in:
Rotimi Best
2023-08-03 15:52:20 +01:00
parent 8b35baa5c3
commit d279f3ee09
6 changed files with 476 additions and 148 deletions
+339 -144
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -23,6 +23,7 @@
"@types/marked": "^5.0.1",
"@types/paystack": "^2.0.1",
"@types/pluralize": "0.0.30",
"ai": "^2.1.31",
"autoprefixer": "^10.4.14",
"body-parser": "^1.20.2",
"canvas-confetti": "^1.6.0",
@@ -33,6 +34,7 @@
"lodash": "^4.17.21",
"marked": "^6.0.0",
"netlify": "^13.1.10",
"openai-edge": "^1.2.2",
"pluralize": "^8.0.0",
"quill": "^1.3.7",
"sirv": "^2.0.3",
@@ -69,4 +71,4 @@
"vite": "^4.4.8"
},
"type": "module"
}
}
@@ -1,14 +1,21 @@
<script>
import { useCompletion } from 'ai/svelte';
import MODES from '$lib/utils/constants/mode.js';
import { Popover } from 'carbon-components-svelte';
import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte';
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte';
import Tabs from '$lib/components/Tabs/index.svelte';
import TabContent from '$lib/components/TabContent/index.svelte';
import MarkdownRender from '$lib/components/MarkdownRender/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import EditContent from '$lib/components/EditContent/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import { lesson, handleUpdateLessonMaterials, isLessonDirty } from '../store/lessons';
import { lesson, lessons, handleUpdateLessonMaterials, isLessonDirty } from '../store/lessons';
import { course } from '$lib/components/Course/store';
import * as CONSTANTS from './constants';
export let mode = MODES.view;
@@ -18,6 +25,11 @@
let tabs = CONSTANTS.tabs;
let currentTab = tabs[0].value;
let errors = {};
let textareaRef = {};
let aiButtonRef = {};
let openPopover = false;
let aiButtonClass =
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 rounded-md w-full mb-2';
const onChange = (tab) => () => (currentTab = tab);
@@ -46,9 +58,34 @@
});
}
const { input, handleSubmit, completion, isLoading } = useCompletion({
api: '/api/completion'
});
function updateNoteByCompletion(completion) {
$lesson.materials.note = completion;
if (textareaRef) {
textareaRef.scrollTop = textareaRef.scrollHeight;
}
}
function callAI(type = '') {
const _lesson = $lessons.find((les) => les.id === $lesson.id);
$input = {
type,
lessonTitle: _lesson?.title || '',
courseTitle: $course.title
};
setTimeout(() => {
handleSubmit({ preventDefault: () => {} });
}, 500);
}
$: handleSave(prevMode);
$: addBadgeValueToTab($lesson.materials);
$: updateNoteByCompletion($completion);
</script>
<Tabs {tabs} {currentTab} {onChange}>
@@ -93,7 +130,46 @@
bind:value={$lesson.materials.note}
placeholder="Start typing your lesson"
onInputChange={() => ($isLessonDirty = true)}
/>
bind:textareaRef
bind:buttonRef={aiButtonRef}
>
<div slot="buttons">
<PrimaryButton
className="flex items-center relative"
onClick={() => {
openPopover = !openPopover;
}}
isLoading={$isLoading}
isDisabled={$isLoading}
>
<MachineLearningModel size={20} class="carbon-icon mr-3" />
AI Assistant
<Popover
caret
align="left"
bind:open={openPopover}
on:click:outside={({ detail }) => {
openPopover = aiButtonRef?.contains(detail.target);
}}
>
<div class="p-2">
<button class={aiButtonClass} on:click={() => callAI('outline')}>
<ListIcon class="carbon-icon mr-2" />
Generate Lesson Outline
</button>
<button class={aiButtonClass} on:click={() => callAI('note')}>
<AlignBoxTopLeftIcon class="carbon-icon mr-2" />
Generate Lesson Note
</button>
<button class={aiButtonClass} on:click={() => callAI('activities')}>
<IbmWatsonKnowledgeStudioIcon class="carbon-icon mr-2" />
Generate Lesson Activities
</button>
</div>
</Popover>
</PrimaryButton>
</div>
</EditContent>
{:else if $lesson.materials.note}
<MarkdownRender content={$lesson.materials.note} />
{:else}
@@ -7,6 +7,8 @@
export let placeholder = ``;
export let onInputChange = () => {};
export let textAreaHeight = '';
export let textareaRef;
export let buttonRef;
const activeClassName = 'border-b-2 border-blue-700';
const handleTabClick = (tab) => () => {
@@ -28,11 +30,19 @@
>
Preview
</button>
{#if $$slots.buttons}
<div class="grow" />
<div bind:this={buttonRef}>
<slot name="buttons" />
</div>
{/if}
</div>
<div class="m-2 mb-0 p-0">
{#if isWriteMode}
<textarea
bind:value
bind:this={textareaRef}
{placeholder}
style="height: {textAreaHeight}"
class="border border-gray rounded-md p-2"
+42
View File
@@ -0,0 +1,42 @@
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'
};
const instruction = {
outline: 'lesson outline',
note: 'lesson note',
activities: 'interactive lesson activities'
};
export async function POST({ request }) {
const { prompt } = await request.json();
const { courseTitle, lessonTitle, type } = prompt;
console.log('prompt', prompt);
const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'user',
content: `Generate a ${instruction[type]} given the course title is "${courseTitle}" and the lesson is "${lessonTitle}". Format in markdown`
}
],
stream: true
});
// Convert the response into a friendly text-stream
const stream = OpenAIStream(response);
// Respond with the stream
return new StreamingTextResponse(stream);
}
@@ -4,6 +4,7 @@
import { fetchCourse, fetchLesson } from '$lib/utils/services/courses';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import PageNav from '$lib/components/PageNav/index.svelte';
import PageBody from '$lib/components/PageBody/index.svelte';
@@ -92,6 +93,7 @@
<PrimaryButton
className="mr-2"
label={mode === MODES.edit ? 'Save' : 'Edit'}
variant={VARIANTS.CONTAINED_INFO}
onClick={() => {
prevMode = mode;
mode = mode === MODES.edit ? MODES.view : MODES.edit;
@@ -114,6 +116,7 @@
onClick={markLessonComplete}
isLoading={isMarkingComplete}
isDisabled={isMarkingComplete}
variant={VARIANTS.OUTLINED}
className="mt-10"
>
<CheckmarkOutlineIcon size={24} class="carbon-icon mr-2" />