CLASSROOMIO-T-141: Reusable AI trigger all around the app

CLASSROOM-T-141: Dark mode improvements

CLASSROOMIO-T-141: Add ai widget to landing page settings

CLASSROOMIO-T-141: Enable AI button to text area in the landingpage settings

CLASSROOMIO-T-141: Complete AI flow

Merge branch 'main' into CLASSROOMIO-T-141

CLASSROOMIO-T-141: AI Button test


Co-authored-by: Rotimi Best <rotimiibitoyeemma@gmail.com>
Co-authored-by: Tunji Adeyemi <tunji@digdippa.com>


Merge-request: CLASSROOMIO-MR-74
Merged-by: Best Admin <best@digdippa.com>
This commit is contained in:
tunny17
2023-09-24 07:08:37 +00:00
committed by Space Cloud
co-authored by Rotimi Best
parent 5e77f9ca46
commit a2c83c7909
17 changed files with 525 additions and 271 deletions
@@ -0,0 +1,135 @@
<script lang="ts">
import { useCompletion } from 'ai/svelte';
import { Popover } from 'carbon-components-svelte';
import type { PopoverProps } from 'carbon-components-svelte/types/Popover/Popover.svelte';
import MagicWandFilled from 'carbon-icons-svelte/lib/MagicWandFilled.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import { Loading } from 'carbon-components-svelte';
export let className = '';
export let handleInsert = (v: string) => {};
export let defaultPrompt = '';
export let alignPopover: PopoverProps['align'] = 'left';
export let isHTML = false;
let openPopover = false;
let plainText = '';
let textRef: HTMLParagraphElement;
let prompt: string = defaultPrompt;
let isSubmitted = false;
const { input, handleSubmit, completion, isLoading } = useCompletion({
api: '/api/completion/customprompt'
});
function restart() {
isSubmitted = false;
plainText = '';
}
function onCompletion(completion: string) {
plainText = completion;
if (textRef) {
textRef.scrollTop = textRef.scrollHeight;
}
}
function callAI(prompt: string, rephrase: boolean) {
isSubmitted = true;
$input = rephrase ? `Can you please rephrase this: ${plainText}` : prompt;
setTimeout(() => {
handleSubmit({ preventDefault: () => {} });
}, 500);
}
$: onCompletion($completion);
</script>
<div class="{className} relative z-[100]">
<IconButton
contained={true}
color="text-primary-700 dark:bg-neutral-800"
size="large"
onClick={() => (openPopover = !openPopover)}
>
<MagicWandFilled size={16} class="carbon-icon" />
<Popover caret align={alignPopover} bind:open={openPopover}>
<button
class="p-2 w-[300px] h-[220px] text-start"
type="button"
on:click={(e) => e.stopPropagation()}
>
{#if isSubmitted}
<div class="h-[200px]">
<div
bind:this={textRef}
class="h-[82%] bg-white dark:text-black p-2 overflow-y-auto text-sm text-start w-full my-1"
>
{#if isHTML}
{@html plainText}
{:else}
<p class="font-normal">
{plainText}
</p>
{/if}
</div>
<div class="flex gap-5 ml-2">
<button
class="text-xs px-4 py-2 border-[1px] cursor-pointer rounded-md font-medium"
on:click={() => {
handleInsert(plainText);
openPopover = false;
}}
type="button"
disabled={$isLoading}
>
{#if $isLoading}
<Loading withOverlay={false} small />
{:else}
Insert
{/if}
</button>
<button
class="text-xs px-4 py-2 border-[1px] cursor-pointer rounded-md font-medium {$isLoading &&
'opacity-25 cursor-not-allowed'}"
on:click|preventDefault={() => callAI(prompt, true)}
type="button"
disabled={$isLoading}
>
Rephrase
</button>
{#if !$isLoading}
<button
class="text-xs px-4 py-2 border-[1px] cursor-pointer rounded-md font-medium"
on:click|preventDefault={restart}
type="button"
>
Reset
</button>
{/if}
</div>
</div>
{:else}
<div class="h-full">
<p class="text-sm font-medium mb-2 ml-1">Help me write</p>
<textarea
bind:value={prompt}
class="w-full border-0 rounded-lg h-[65%] text-sm m-0 dark:text-black"
placeholder="Help me write a short description"
/>
<button
class="flex text-xs px-4 py-2 m-0 border rounded-md font-medium"
on:click|preventDefault={() => callAI(prompt, false)}
type="button"
>
Start typing...
</button>
</div>
{/if}
</button>
</Popover>
</IconButton>
</div>
@@ -19,7 +19,7 @@
let errors = {}; let errors = {};
let lesson = { let lesson = {
id: null, id: null,
title: 'Untitled lesson', title: '',
profile: undefined, profile: undefined,
call_url: undefined, call_url: undefined,
lesson_at: new Date(), lesson_at: new Date(),
@@ -11,6 +11,7 @@
import ListIcon from 'carbon-icons-svelte/lib/List.svelte'; import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte'; import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.svelte';
import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte'; import MachineLearningModel from 'carbon-icons-svelte/lib/MachineLearningModel.svelte';
import MagicWandFilled from 'carbon-icons-svelte/lib/MagicWandFilled.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 Box from '$lib/components/Box/index.svelte'; import Box from '$lib/components/Box/index.svelte';
@@ -221,7 +222,7 @@
isDisabled={$isLoading} isDisabled={$isLoading}
variant={VARIANTS.OUTLINED} variant={VARIANTS.OUTLINED}
> >
<MachineLearningModel size={20} class="carbon-icon mr-3" /> <MagicWandFilled size={20} class="carbon-icon mr-3" />
AI AI
<Popover <Popover
caret caret
@@ -1,18 +1,9 @@
<script> <script>
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import get from 'lodash/get';
import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextEditor from '$lib/components/TextEditor/index.svelte';
export let course; export let value = '';
function handleChange(html) {
const _course = cloneDeep(course);
set(_course, 'metadata.description', html);
course = _course;
}
</script> </script>
<div class="h-2/5"> <div class="h-2/5">
<TextEditor value={get(course, 'metadata.description')} onChange={handleChange} /> <TextEditor bind:value />
</div> </div>
@@ -1,18 +1,9 @@
<script> <script>
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import get from 'lodash/get';
import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextEditor from '$lib/components/TextEditor/index.svelte';
export let course; export let value = '';
function handleChange(html) {
const _course = cloneDeep(course);
set(_course, 'metadata.goals', html);
course = _course;
}
</script> </script>
<div class="h-2/5"> <div class="h-2/5">
<TextEditor value={get(course, 'metadata.goals')} onChange={handleChange} /> <TextEditor bind:value />
</div> </div>
@@ -1,18 +1,9 @@
<script> <script lang="ts">
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import get from 'lodash/get';
import TextEditor from '$lib/components/TextEditor/index.svelte'; import TextEditor from '$lib/components/TextEditor/index.svelte';
export let course; export let value: string;
function handleChange(html) {
const _course = cloneDeep(course);
set(_course, 'metadata.requirements', html);
course = _course;
}
</script> </script>
<div class="h-2/5"> <div class="h-2/5">
<TextEditor value={get(course, 'metadata.requirements')} onChange={handleChange} /> <TextEditor bind:value />
</div> </div>
@@ -58,6 +58,9 @@
placeholder="" placeholder=""
bind:value={review.description} bind:value={review.description}
errorMessage={errors.description} errorMessage={errors.description}
isAIEnabled={true}
initAIPrompt="Generate a 20 word review from a student saying good things about me teaching"
aiAlignPopover="top-right"
/> />
<div class="flex justify-between w-full items-center mt-2"> <div class="flex justify-between w-full items-center mt-2">
@@ -1,10 +1,11 @@
<script> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import cloneDeep from 'lodash/cloneDeep';
import set from 'lodash/set';
import ChevronRightIcon from 'carbon-icons-svelte/lib/ChevronRight.svelte'; import ChevronRightIcon from 'carbon-icons-svelte/lib/ChevronRight.svelte';
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte'; import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
import ArrowUpRightIcon from 'carbon-icons-svelte/lib/ArrowUpRight.svelte'; import ArrowUpRightIcon from 'carbon-icons-svelte/lib/ArrowUpRight.svelte';
import Container from './Container.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
import CloseButton from '$lib/components/Buttons/Close/index.svelte'; import CloseButton from '$lib/components/Buttons/Close/index.svelte';
import HeaderForm from './HeaderForm.svelte'; import HeaderForm from './HeaderForm.svelte';
@@ -21,47 +22,70 @@
import ChevronLeftIcon from 'carbon-icons-svelte/lib/ChevronLeft.svelte'; import ChevronLeftIcon from 'carbon-icons-svelte/lib/ChevronLeft.svelte';
import { isMobile } from '$lib/utils/store/useMobile'; import { isMobile } from '$lib/utils/store/useMobile';
import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte';
import type { Course } from '$lib/utils/types';
export let course; export let course: Course;
export let courseId; export let courseId: string;
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300'; let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
let loading = false; let loading = false;
let show = false; let show = false;
const sections = [
interface Section {
key: number;
path: string;
title: string;
enableAIWriter?: boolean;
initPrompt?: string;
}
const sections: Section[] = [
{ {
key: 1, key: 1,
path: '',
title: 'Header' title: 'Header'
}, },
{ {
key: 2, key: 2,
title: 'Requirement' path: 'metadata.requirements',
title: 'Requirement',
enableAIWriter: true,
initPrompt: 'Generate a course requirement for this given course:'
}, },
{ {
key: 3, key: 3,
title: 'Description' path: 'metadata.description',
title: 'Description',
enableAIWriter: true,
initPrompt: 'Generate a course description for this given course:'
}, },
{ {
key: 4, key: 4,
title: 'Goals' path: 'metadata.goals',
title: 'Goals',
enableAIWriter: true,
initPrompt: 'What should a student expect to learn from this given course:'
}, },
{ {
key: 5, key: 5,
path: '',
title: 'Reviews' title: 'Reviews'
}, },
{ {
key: 6, key: 6,
path: '',
title: 'Instructor' title: 'Instructor'
}, },
{ {
key: 7, key: 7,
path: '',
title: 'Pricing' title: 'Pricing'
} }
]; ];
let selectedSection = null; let selectedSection: Section | null = null;
function handleClose() { function handleClose() {
console.log('closing');
if (!selectedSection) { if (!selectedSection) {
goto(`/courses/${courseId}`); goto(`/courses/${courseId}`);
} }
@@ -69,11 +93,13 @@
selectedSection = null; selectedSection = null;
} }
function handleSectionSelect(sectionKey) { function handleSectionSelect(sectionKey: number) {
return () => { return () => {
selectedSection = sections.find((section) => section.key === sectionKey); selectedSection = sections.find((section) => section.key === sectionKey) || null;
if (selectedSection) { if (selectedSection) {
const sectionId = selectedSection.title.toLowerCase(); const sectionId = selectedSection.title.toLowerCase();
const sectionEl = document.getElementById(sectionId); const sectionEl = document.getElementById(sectionId);
if (sectionEl) { if (sectionEl) {
sectionEl.scrollIntoView({ sectionEl.scrollIntoView({
@@ -120,8 +146,8 @@
<aside <aside
class={`${ class={`${
show ? '-translate-x-[100%] fixed z-[9999]' : 'translate-x-0 fixed z-[9999]' show ? '-translate-x-[100%] fixed z-[50]' : 'translate-x-0 fixed z-[50]'
} left-0 transition w-[280px] min-w-[280px] bg-gray-100 dark:bg-black h-full border border-l-0 border-t-0 border-b-0 border-r-1`} } left-0 transition w-[350px] min-w-[300px] bg-gray-100 dark:bg-black h-full border border-l-0 border-t-0 border-b-0 border-r-1`}
> >
<div class="toggler rounded-full shadow-lg absolute"> <div class="toggler rounded-full shadow-lg absolute">
<IconButton <IconButton
@@ -185,18 +211,36 @@
<IconButton onClick={handleClose}> <IconButton onClick={handleClose}>
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" title="Go back" /> <ArrowLeftIcon size={24} class="carbon-icon dark:text-white" title="Go back" />
</IconButton> </IconButton>
<h3 class="dark:text-white">{selectedSection.title}</h3> <div class=" flex items-center">
<h3 class="dark:text-white">
{selectedSection.title}
</h3>
{#if selectedSection.enableAIWriter}
<CustomPromptBtn
className="w-fit ml-2"
alignPopover="bottom-left"
defaultPrompt={`${selectedSection.initPrompt} ${course.title}. Please format in html`}
isHTML={true}
handleInsert={(v) => {
if (!selectedSection) return;
const _course = cloneDeep(course);
set(_course, selectedSection.path, v);
course = _course;
}}
/>
{/if}
</div>
</div> </div>
<div class="title-content p-2 overflow-y-auto"> <div class="title-content p-2 overflow-y-auto">
{#if selectedSection.key === 1} {#if selectedSection.key === 1}
<HeaderForm bind:course /> <HeaderForm bind:course />
{:else if selectedSection.key === 2} {:else if selectedSection.key === 2}
<RequirementForm bind:course /> <RequirementForm bind:value={course.metadata.requirements} />
{:else if selectedSection.key === 3} {:else if selectedSection.key === 3}
<DescriptionForm bind:course /> <DescriptionForm bind:value={course.metadata.description} />
{:else if selectedSection.key === 4} {:else if selectedSection.key === 4}
<GoalsForm bind:course /> <GoalsForm bind:value={course.metadata.goals} />
{:else if selectedSection.key === 5} {:else if selectedSection.key === 5}
<ReviewsForm bind:course /> <ReviewsForm bind:course />
{:else if selectedSection.key === 6} {:else if selectedSection.key === 6}
@@ -63,7 +63,11 @@
/> />
<!-- Pricing Details --> <!-- Pricing Details -->
<aside class="{className} price-container lg:sticky lg:top-10 lg:shadow-2xl lg:rounded-lg m-h-fit"> <aside
class="{className} price-container lg:sticky {editMode
? 'lg:top-0'
: 'lg:top-10'} lg:shadow-2xl lg:rounded-lg m-h-fit bg-neutral-800"
>
<div class="p-10"> <div class="p-10">
<!-- Pricing --> <!-- Pricing -->
<div class="mb-6"> <div class="mb-6">
@@ -167,12 +167,14 @@
<!-- Course Details --> <!-- Course Details -->
<div class="course-content w-full p-3 lg:w-10/12 lg:mr-14"> <div class="course-content w-full p-3 lg:w-10/12 lg:mr-14">
<!-- Navigation --> <!-- Navigation -->
<nav class="flex items-center border-b border-gray-300 py-3 sticky top-11 bg-white"> <nav
class="flex items-center border-b border-gray-300 py-3 sticky top-11 bg-white dark:bg-neutral-800"
>
{#each NAV_ITEMS as navItem} {#each NAV_ITEMS as navItem}
<a <a
href="{$page.url.pathname}{navItem.key}" href="{$page.url.pathname}{navItem.key}"
class="{navItem.key === activeNav && class="{navItem.key === activeNav &&
'active text-primary-700'} rounded-lg px-2 mr-6 text-slate-700 font-normal hover:bg-slate-100 hover:text-slate-900 dark:text-white" 'active text-primary-700'} rounded-lg px-2 mr-6 text-slate-700 font-normal hover:bg-neutral-600 hover:text-slate-900 dark:text-white"
> >
{navItem.label} {navItem.label}
</a> </a>
@@ -16,6 +16,19 @@
description: '' description: ''
}; };
let userPrompt = '';
function onClose() {
createCourseModal.update(() => ({
open: false,
title: '',
description: '',
emails: '',
tutors: '',
students: ''
}));
}
async function createCourse(e) { async function createCourse(e) {
const { hasError, fieldErrors } = validateForm($createCourseModal); const { hasError, fieldErrors } = validateForm($createCourseModal);
@@ -32,6 +45,9 @@
console.log(`newGroup`, newGroup); console.log(`newGroup`, newGroup);
const { id: group_id } = newGroup[0]; const { id: group_id } = newGroup[0];
// Set userPrompt to the description
userPrompt = description;
// 2. Create course with group_id // 2. Create course with group_id
const { data: newCourse } = await supabase const { data: newCourse } = await supabase
.from('course') .from('course')
@@ -55,7 +71,7 @@
role_id: ROLE.TUTOR role_id: ROLE.TUTOR
}); });
$createCourseModal.open = false; onClose();
} }
</script> </script>
@@ -64,7 +80,7 @@
</svelte:head> </svelte:head>
<Modal <Modal
onClose={() => ($createCourseModal.open = false)} {onClose}
bind:open={$createCourseModal.open} bind:open={$createCourseModal.open}
width="w-4/5 md:w-2/5" width="w-4/5 md:w-2/5"
modalHeading="Create a Course" modalHeading="Create a Course"
@@ -73,13 +89,13 @@
<TextField <TextField
label="Course name" label="Course name"
bind:value={$createCourseModal.title} bind:value={$createCourseModal.title}
autoFocus={true}
placeholder="Your course name" placeholder="Your course name"
className="mb-4" className="mb-4"
isRequired={true} isRequired={true}
errorMessage={errors.title} errorMessage={errors.title}
autoComplete={false} autoComplete={false}
/> />
<TextArea <TextArea
label="Short description" label="Short description"
bind:value={$createCourseModal.description} bind:value={$createCourseModal.description}
@@ -89,6 +105,8 @@
className="mb-4" className="mb-4"
isRequired={true} isRequired={true}
errorMessage={errors.description} errorMessage={errors.description}
isAIEnabled={true}
initAIPrompt="Write a 30 word description for a course titled: {$createCourseModal.title}"
/> />
<div class="mt-5 flex items-center"> <div class="mt-5 flex items-center">
+28 -11
View File
@@ -1,10 +1,13 @@
<script> <script lang="ts">
import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte';
import type { PopoverProps } from 'carbon-components-svelte/types/Popover/Popover.svelte';
export let label = ''; export let label = '';
export let disabled = false; export let disabled = false;
export let placeholder = 'Write your answer...'; export let placeholder = 'Write your answer...';
export let value = ''; export let value = '';
export let rows = '3'; export let rows = 3;
export let maxRows = 3; // export let maxRows = 3;
export let className = ''; export let className = '';
export let labelClassName = ''; export let labelClassName = '';
export let bgColor = 'bg-gray-50 focus:bg-primary-50 dark:bg-white'; export let bgColor = 'bg-gray-50 focus:bg-primary-50 dark:bg-white';
@@ -12,19 +15,33 @@
export let errorMessage = ''; export let errorMessage = '';
export let isRequired = false; export let isRequired = false;
export let onChange = () => {}; // This is to know if element is 'dirty' export let onChange = () => {}; // This is to know if element is 'dirty'
export let ref; export let ref = null;
export let isAIEnabled = false;
export let initAIPrompt = '';
export let aiAlignPopover: PopoverProps['align'] = 'left';
$: minHeight = `${1 + parseInt(rows, 10) * 1.2}em`; // $: minHeight = `${1 + parseInt(rows, 10) * 1.2}em`;
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`; // $: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`;
</script> </script>
<label class="block w-full relative {className}"> <label for="text-field" class="block w-full relative {className}">
{#if label} {#if label}
<p for="text-field" class="dark:text-white text-left {labelClassName}"> <p class="dark:text-white text-left w-full flex items-center justify-between {labelClassName}">
{label} <span>
{label}
{#if isRequired}
<span class="text-red-700">*</span>
{/if}
</span>
{#if isRequired} {#if isAIEnabled}
<span class="text-red-700">*</span> <CustomPromptBtn
defaultPrompt={initAIPrompt}
alignPopover={aiAlignPopover}
handleInsert={(v) => {
value = v;
}}
/>
{/if} {/if}
</p> </p>
{/if} {/if}
@@ -164,6 +164,7 @@
placeholder="Write your subtitle here" placeholder="Write your subtitle here"
className="w-full mb-5" className="w-full mb-5"
bind:value={$landingPageSettings.header.subtitle} bind:value={$landingPageSettings.header.subtitle}
isAIEnabled={true}
/> />
<SectionTitle>Actions</SectionTitle> <SectionTitle>Actions</SectionTitle>
@@ -221,6 +222,7 @@
placeholder="Write your Content here" placeholder="Write your Content here"
className="mb-5" className="mb-5"
bind:value={$landingPageSettings.aboutUs.content} bind:value={$landingPageSettings.aboutUs.content}
isAIEnabled={true}
/> />
<div> <div>
@@ -268,6 +270,7 @@
labelClassName="font-light" labelClassName="font-light"
placeholder="Write your subtitle here" placeholder="Write your subtitle here"
bind:value={$landingPageSettings.courses.subtitle} bind:value={$landingPageSettings.courses.subtitle}
isAIEnabled={true}
/> />
</Column> </Column>
</Row> </Row>
@@ -301,6 +304,7 @@
placeholder="Write your answer here" placeholder="Write your answer here"
bind:value={item.content} bind:value={item.content}
className="mb-5" className="mb-5"
isAIEnabled={true}
/> />
<IconButton onClick={() => deleteFaq(item.id)}> <IconButton onClick={() => deleteFaq(item.id)}>
<TrashCan size={24} class="fill-red-700" /> <TrashCan size={24} class="fill-red-700" />
@@ -320,6 +324,7 @@
placeholder="Write your answer here" placeholder="Write your answer here"
bind:value={newQuestion.content} bind:value={newQuestion.content}
className="mb-5" className="mb-5"
isAIEnabled={true}
/> />
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<PrimaryButton variant={VARIANTS.OUTLINED} label="Save" onClick={saveNewFAQ} /> <PrimaryButton variant={VARIANTS.OUTLINED} label="Save" onClick={saveNewFAQ} />
@@ -359,6 +364,7 @@
placeholder="Write your subtitle here" placeholder="Write your subtitle here"
className="mt-3 mb-5" className="mt-3 mb-5"
bind:value={$landingPageSettings.contact.subtitle} bind:value={$landingPageSettings.contact.subtitle}
isAIEnabled={true}
/> />
<TextField <TextField
label="Phone number" label="Phone number"
@@ -396,6 +402,7 @@
placeholder="Write your subtitle here" placeholder="Write your subtitle here"
className="mb-5" className="mb-5"
bind:value={$landingPageSettings.mailinglist.subtitle} bind:value={$landingPageSettings.mailinglist.subtitle}
isAIEnabled={true}
/> />
<TextField <TextField
+1 -1
View File
@@ -45,5 +45,5 @@
</script> </script>
<div> <div>
<Editor {value} {apiKey} {conf} /> <Editor bind:value {apiKey} {conf} />
</div> </div>
+4 -4
View File
@@ -2,9 +2,9 @@ export * from './config';
export * from './dashboard'; export * from './dashboard';
interface CourseMetadata { interface CourseMetadata {
requirements?: string; requirements: string;
description?: string; description: string;
goals?: string; goals: string;
videoUrl?: ''; videoUrl?: '';
showDiscount: false; showDiscount: false;
discount: 0; discount: 0;
@@ -95,7 +95,7 @@ export interface Course {
organization_id?: string /* foreign key to organization.id */; organization_id?: string /* foreign key to organization.id */;
logo?: string; logo?: string;
slug?: any; // type unknown; slug?: any; // type unknown;
metadata?: CourseMetadata; metadata: CourseMetadata;
cost: number; cost: number;
currency?: string; currency?: string;
group?: Group; group?: Group;
@@ -0,0 +1,37 @@
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 response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content: 'Act like an assistant of a lecturer or an educator'
},
{
role: 'user',
content: prompt
}
],
stream: true
});
// Convert the response into a friendly text-stream
const stream = OpenAIStream(response);
// Respond with the stream
return new StreamingTextResponse(stream);
}
+208 -195
View File
@@ -117,221 +117,234 @@
</PageNav> </PageNav>
<PageBody width="max-w-6xl" padding="p-0"> <PageBody width="max-w-6xl" padding="p-0">
<section {#if $lessons.length}
class="w-full lg:w-11/12 p-3 lg:px-4 m-auto" <section
use:dndzone={{ class="w-full lg:w-11/12 p-3 lg:px-4 m-auto"
items: $lessons, use:dndzone={{
flipDurationMs, items: $lessons,
dragDisabled: isStudent, flipDurationMs,
dropTargetStyle: { dragDisabled: isStudent,
border: '2px #1d4ed8 solid', dropTargetStyle: {
'border-style': 'dashed' border: '2px #1d4ed8 solid',
} 'border-style': 'dashed'
}} }
on:consider={handleDndConsider} }}
on:finalize={handleDndFinalize} on:consider={handleDndConsider}
> on:finalize={handleDndFinalize}
{#each $lessons as lesson (lesson.id)} >
<div {#each $lessons as lesson (lesson.id)}
class="sm:min-h-[245px] md:min-h-[100px] lg:min-h-[190px] relative m-auto rounded-md border-2 border-gray-200 py-3 px-5 mb-4 flex items-center dark:bg-neutral-800" <div
> class="sm:min-h-[245px] md:min-h-[100px] lg:min-h-[190px] relative m-auto rounded-md border-2 border-gray-200 py-3 px-5 mb-4 flex items-center dark:bg-neutral-800"
<div class="mr-5"> >
<TextChip <div class="mr-5">
value={getLessonOrder(lesson.id)} <TextChip
size="sm" value={getLessonOrder(lesson.id)}
shape="rounded-full" size="sm"
className="bg-primary-200 text-primary-600 text-xs" shape="rounded-full"
/> className="bg-primary-200 text-primary-600 text-xs"
</div> />
</div>
<div class="w-4/5"> <div class="w-4/5">
{#if lessonEditing === lesson.id} {#if lessonEditing === lesson.id}
<TextField bind:value={lesson.title} autofocus={true} className="max-w-lg" /> <TextField bind:value={lesson.title} autofocus={true} className="max-w-lg" />
{:else} {:else}
<h3 class="dark:text-white text-xl m-0 flex items-center"> <h3 class="dark:text-white text-xl m-0 flex items-center">
<a <a
href={isStudent && !lesson.is_unlocked href={isStudent && !lesson.is_unlocked
? $page.url.pathname ? $page.url.pathname
: '/courses/' + $course.id + '/lessons/' + lesson.id} : '/courses/' + $course.id + '/lessons/' + lesson.id}
class="dark:text-white no-underline hover:underline font-semibold text-black {isStudent && class="dark:text-white no-underline hover:underline font-semibold text-black {isStudent &&
!lesson.is_unlocked !lesson.is_unlocked
? 'cursor-not-allowed' ? 'cursor-not-allowed'
: ''}" : ''}"
> >
{lesson.title} {lesson.title}
</a> </a>
</h3> </h3>
{/if} {/if}
<!-- <div class="flex items-center mb-2"> <!-- <div class="flex items-center mb-2">
<div class="flex items-center mr-2"> <div class="flex items-center mr-2">
{resources.map((r) => `${r.value} ${r.label}`).join(', ')} {resources.map((r) => `${r.value} ${r.label}`).join(', ')}
</div> </div>
</div> --> </div> -->
<div <div
class="flex items-start justify-between flex-col lg:flex-row lg:items-center mt-2 w-4/5" class="flex items-start justify-between flex-col lg:flex-row lg:items-center mt-2 w-4/5"
> >
<div class="lg:mb-0"> <div class="lg:mb-0">
{#if lessonEditing === lesson.id}
<Select
bind:value={lesson.profile}
options={$group.tutors}
labelKey="fullname"
className="sm:my-1 w-[100%]"
/>
{:else if !lesson.profile}
<p class="dark:text-white ml-2 text-sm mb-3">No tutor added</p>
{:else}
<a href="." class="flex items-center hover:underline mb-2">
<Avatar
className="w-6 h-6"
src={lesson.profile.avatar_url}
name="Avatar placeholder"
/>
<p class="dark:text-white ml-2 text-sm">
{lesson.profile.fullname}
</p>
</a>
{/if}
</div>
<div class="flex items-center lg:mb-0">
{#if lessonEditing === lesson.id}
<input
type="date"
name="lesson-date-picker"
class="p-2 my-2 rounded-md sm:w-[179px]"
value={formatDate(lesson.lesson_at)}
on:input={(e) => (lesson.lesson_at = e.target.value)}
/>
{:else}
<div class="flex mb-2">
<Calendar size={20} class="carbon-icon text-gray-400 dark:text-white" />
<p class="dark:text-white text-sm ml-2">
{new Date(lesson.lesson_at).toDateString()}
</p>
</div>
{/if}
</div>
<div class="flex items-center mb-3 lg:mb-0">
{#if lessonEditing === lesson.id}
<TextField
bind:value={lesson.call_url}
autofocus={true}
className="w-[179px]"
placeholder="https://meet.google.com/mga-dsjs-fmb"
/>
{:else}
<div class="flex">
<Video size={20} class="carbon-icon text-gray-400 dark:text-white ml-0.5" />
<a
class="text-sm ml-2 text-primary-600"
href={lesson.call_url || '#'}
target="_blank"
>
{lesson.call_url ? 'Join lesson' : 'No link'}
</a>
</div>
{/if}
</div>
</div>
<div class="flex flex-row absolute sm:bottom-20 bottom-10 right-10">
<div class="success hidden md:block">
<IconButton
disabled={isStudent}
onClick={() => {
lesson.is_unlocked = !lesson.is_unlocked;
handleSaveLesson(lesson, $course.id);
}}
toolTipProps={isStudent
? {}
: {
title: `Click to ${lesson.is_unlocked ? 'lock' : 'unlock'}`,
direction: 'right'
}}
>
{#if lesson.is_unlocked}
<UnlockedIcon size={24} class="carbon-icon dark:text-white" />
{:else}
<LockedIcon size={24} class="carbon-icon dark:text-white" />
{/if}
</IconButton>
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden md:block">
{#if lessonEditing === lesson.id} {#if lessonEditing === lesson.id}
<IconButton <Select
onClick={() => { bind:value={lesson.profile}
lessonEditing = null; options={$group.tutors}
handleSaveLesson(lesson, $course.id); labelKey="fullname"
}} className="sm:my-1 w-[100%]"
> />
<SaveIcon size={24} class="carbon-icon dark:text-white" /> {:else if !lesson.profile}
</IconButton> <p class="dark:text-white ml-2 text-sm mb-3">No tutor added</p>
{:else} {:else}
<IconButton onClick={() => (lessonEditing = lesson.id)}> <a href="." class="flex items-center hover:underline mb-2">
<WatsonHealthTextAnnotationToggle <Avatar
size={32} className="w-6 h-6"
class="carbon-icon dark:text-white" src={lesson.profile.avatar_url}
name="Avatar placeholder"
/> />
</IconButton> <p class="dark:text-white ml-2 text-sm">
{lesson.profile.fullname}
</p>
</a>
{/if} {/if}
</div> </div>
</RoleBasedSecurity>
<RoleBasedSecurity allowedRoles={[1, 2]}> <div class="flex items-center lg:mb-0">
<div class="hidden md:block"> {#if lessonEditing === lesson.id}
<IconButton onClick={handleDelete(lesson.id)}> <input
<TrashCanIcon size={24} class="carbon-icon dark:text-white" /> type="date"
name="lesson-date-picker"
class="p-2 my-2 rounded-md sm:w-[179px]"
value={formatDate(lesson.lesson_at)}
on:input={(e) => (lesson.lesson_at = e.target.value)}
/>
{:else}
<div class="flex mb-2">
<Calendar size={20} class="carbon-icon text-gray-400 dark:text-white" />
<p class="dark:text-white text-sm ml-2">
{new Date(lesson.lesson_at).toDateString()}
</p>
</div>
{/if}
</div>
<div class="flex items-center mb-3 lg:mb-0">
{#if lessonEditing === lesson.id}
<TextField
bind:value={lesson.call_url}
autofocus={true}
className="w-[179px]"
placeholder="https://meet.google.com/mga-dsjs-fmb"
/>
{:else}
<div class="flex">
<Video size={20} class="carbon-icon text-gray-400 dark:text-white ml-0.5" />
<a
class="text-sm ml-2 text-primary-600"
href={lesson.call_url || '#'}
target="_blank"
>
{lesson.call_url ? 'Join lesson' : 'No link'}
</a>
</div>
{/if}
</div>
</div>
<div class="flex flex-row absolute sm:bottom-20 bottom-10 right-10">
<div class="success hidden md:block">
<IconButton
disabled={isStudent}
onClick={() => {
lesson.is_unlocked = !lesson.is_unlocked;
handleSaveLesson(lesson, $course.id);
}}
toolTipProps={isStudent
? {}
: {
title: `Click to ${lesson.is_unlocked ? 'lock' : 'unlock'}`,
direction: 'right'
}}
>
{#if lesson.is_unlocked}
<UnlockedIcon size={24} class="carbon-icon dark:text-white" />
{:else}
<LockedIcon size={24} class="carbon-icon dark:text-white" />
{/if}
</IconButton> </IconButton>
</div> </div>
</RoleBasedSecurity>
<RoleBasedSecurity allowedRoles={[1, 2]}> <RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="overflowmenu"> <div class="hidden md:block">
<OverflowMenu size="xl" class="block absolute right-10 bottom-4 pl-4"> {#if lessonEditing === lesson.id}
<OverflowMenuItem <IconButton
disabled={isStudent} onClick={() => {
text={lesson.is_unlocked ? 'Lock' : 'Unlock'}
on:click={() => {
lesson.is_unlocked = !lesson.is_unlocked;
handleSaveLesson(lesson, $course.id);
}}
/>
<OverflowMenuItem
text={lessonEditing === lesson.id ? 'Save' : 'Edit'}
on:click={() => {
if (lessonEditing === lesson.id) {
lessonEditing = null; lessonEditing = null;
handleSaveLesson(lesson, $course.id); handleSaveLesson(lesson, $course.id);
} else { }}
lessonEditing = lesson.id; >
} <SaveIcon size={24} class="carbon-icon dark:text-white" />
}} </IconButton>
/> {:else}
<OverflowMenuItem danger text="Delete" on:click={handleDelete(lesson.id)} /> <IconButton onClick={() => (lessonEditing = lesson.id)}>
</OverflowMenu> <WatsonHealthTextAnnotationToggle
</div> size={32}
</RoleBasedSecurity> class="carbon-icon dark:text-white"
/>
</IconButton>
{/if}
</div>
</RoleBasedSecurity>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden md:block">
<IconButton onClick={handleDelete(lesson.id)}>
<TrashCanIcon size={24} class="carbon-icon dark:text-white" />
</IconButton>
</div>
</RoleBasedSecurity>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="overflowmenu">
<OverflowMenu size="xl" class="block absolute right-10 bottom-4 pl-4">
<OverflowMenuItem
disabled={isStudent}
text={lesson.is_unlocked ? 'Lock' : 'Unlock'}
on:click={() => {
lesson.is_unlocked = !lesson.is_unlocked;
handleSaveLesson(lesson, $course.id);
}}
/>
<OverflowMenuItem
text={lessonEditing === lesson.id ? 'Save' : 'Edit'}
on:click={() => {
if (lessonEditing === lesson.id) {
lessonEditing = null;
handleSaveLesson(lesson, $course.id);
} else {
lessonEditing = lesson.id;
}
}}
/>
<OverflowMenuItem danger text="Delete" on:click={handleDelete(lesson.id)} />
</OverflowMenu>
</div>
</RoleBasedSecurity>
</div>
</div> </div>
</div> </div>
{:else}
<Box>
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
<p class="text-sm text-center text-slate-500">
Share your knowledge with the world by creating engaging lessons. Start by clicking
on the Add button.
</p>
</div>
</Box>
{/each}
</section>
{:else}
<Box className="w-full lg:w-11/12 lg:px-4 m-auto">
<div class="flex justify-between flex-col items-center">
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
<p class="text-sm text-center text-slate-500">
Share your knowledge with the world by creating engaging lessons. Start by clicking on
the Add button.
</p>
</div> </div>
{:else} </Box>
<Box> {/if}
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
<p class="text-sm text-center text-slate-500">
Share your knowledge with the world by creating engaging lessons. Start by clicking on
the Add button.
</p>
</div>
</Box>
{/each}
</section>
</PageBody> </PageBody>
</CourseContainer> </CourseContainer>