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/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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user