CLASSROOMIO-T-92: AI Powered lesson notes
This commit is contained in:
Generated
+339
-144
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
"@types/marked": "^5.0.1",
|
"@types/marked": "^5.0.1",
|
||||||
"@types/paystack": "^2.0.1",
|
"@types/paystack": "^2.0.1",
|
||||||
"@types/pluralize": "0.0.30",
|
"@types/pluralize": "0.0.30",
|
||||||
|
"ai": "^2.1.31",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"canvas-confetti": "^1.6.0",
|
"canvas-confetti": "^1.6.0",
|
||||||
@@ -33,6 +34,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "^6.0.0",
|
"marked": "^6.0.0",
|
||||||
"netlify": "^13.1.10",
|
"netlify": "^13.1.10",
|
||||||
|
"openai-edge": "^1.2.2",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"quill": "^1.3.7",
|
"quill": "^1.3.7",
|
||||||
"sirv": "^2.0.3",
|
"sirv": "^2.0.3",
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { useCompletion } from 'ai/svelte';
|
||||||
import MODES from '$lib/utils/constants/mode.js';
|
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 { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||||
|
import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte';
|
||||||
import Tabs from '$lib/components/Tabs/index.svelte';
|
import Tabs from '$lib/components/Tabs/index.svelte';
|
||||||
import TabContent from '$lib/components/TabContent/index.svelte';
|
import TabContent from '$lib/components/TabContent/index.svelte';
|
||||||
import MarkdownRender from '$lib/components/MarkdownRender/index.svelte';
|
import MarkdownRender from '$lib/components/MarkdownRender/index.svelte';
|
||||||
import Box from '$lib/components/Box/index.svelte';
|
import Box from '$lib/components/Box/index.svelte';
|
||||||
import EditContent from '$lib/components/EditContent/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 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';
|
import * as CONSTANTS from './constants';
|
||||||
|
|
||||||
export let mode = MODES.view;
|
export let mode = MODES.view;
|
||||||
@@ -18,6 +25,11 @@
|
|||||||
let tabs = CONSTANTS.tabs;
|
let tabs = CONSTANTS.tabs;
|
||||||
let currentTab = tabs[0].value;
|
let currentTab = tabs[0].value;
|
||||||
let errors = {};
|
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);
|
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);
|
$: handleSave(prevMode);
|
||||||
|
|
||||||
$: addBadgeValueToTab($lesson.materials);
|
$: addBadgeValueToTab($lesson.materials);
|
||||||
|
|
||||||
|
$: updateNoteByCompletion($completion);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Tabs {tabs} {currentTab} {onChange}>
|
<Tabs {tabs} {currentTab} {onChange}>
|
||||||
@@ -93,7 +130,46 @@
|
|||||||
bind:value={$lesson.materials.note}
|
bind:value={$lesson.materials.note}
|
||||||
placeholder="Start typing your lesson"
|
placeholder="Start typing your lesson"
|
||||||
onInputChange={() => ($isLessonDirty = true)}
|
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}
|
{:else if $lesson.materials.note}
|
||||||
<MarkdownRender content={$lesson.materials.note} />
|
<MarkdownRender content={$lesson.materials.note} />
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
export let placeholder = ``;
|
export let placeholder = ``;
|
||||||
export let onInputChange = () => {};
|
export let onInputChange = () => {};
|
||||||
export let textAreaHeight = '';
|
export let textAreaHeight = '';
|
||||||
|
export let textareaRef;
|
||||||
|
export let buttonRef;
|
||||||
const activeClassName = 'border-b-2 border-blue-700';
|
const activeClassName = 'border-b-2 border-blue-700';
|
||||||
|
|
||||||
const handleTabClick = (tab) => () => {
|
const handleTabClick = (tab) => () => {
|
||||||
@@ -28,11 +30,19 @@
|
|||||||
>
|
>
|
||||||
Preview
|
Preview
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{#if $$slots.buttons}
|
||||||
|
<div class="grow" />
|
||||||
|
<div bind:this={buttonRef}>
|
||||||
|
<slot name="buttons" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="m-2 mb-0 p-0">
|
<div class="m-2 mb-0 p-0">
|
||||||
{#if isWriteMode}
|
{#if isWriteMode}
|
||||||
<textarea
|
<textarea
|
||||||
bind:value
|
bind:value
|
||||||
|
bind:this={textareaRef}
|
||||||
{placeholder}
|
{placeholder}
|
||||||
style="height: {textAreaHeight}"
|
style="height: {textAreaHeight}"
|
||||||
class="border border-gray rounded-md p-2"
|
class="border border-gray rounded-md p-2"
|
||||||
|
|||||||
@@ -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 { fetchCourse, fetchLesson } from '$lib/utils/services/courses';
|
||||||
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.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 RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||||
import PageBody from '$lib/components/PageBody/index.svelte';
|
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||||
@@ -92,6 +93,7 @@
|
|||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
label={mode === MODES.edit ? 'Save' : 'Edit'}
|
label={mode === MODES.edit ? 'Save' : 'Edit'}
|
||||||
|
variant={VARIANTS.CONTAINED_INFO}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
prevMode = mode;
|
prevMode = mode;
|
||||||
mode = mode === MODES.edit ? MODES.view : MODES.edit;
|
mode = mode === MODES.edit ? MODES.view : MODES.edit;
|
||||||
@@ -114,6 +116,7 @@
|
|||||||
onClick={markLessonComplete}
|
onClick={markLessonComplete}
|
||||||
isLoading={isMarkingComplete}
|
isLoading={isMarkingComplete}
|
||||||
isDisabled={isMarkingComplete}
|
isDisabled={isMarkingComplete}
|
||||||
|
variant={VARIANTS.OUTLINED}
|
||||||
className="mt-10"
|
className="mt-10"
|
||||||
>
|
>
|
||||||
<CheckmarkOutlineIcon size={24} class="carbon-icon mr-2" />
|
<CheckmarkOutlineIcon size={24} class="carbon-icon mr-2" />
|
||||||
|
|||||||
Reference in New Issue
Block a user