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:
committed by
Space Cloud
co-authored by
Rotimi Best
parent
5e77f9ca46
commit
a2c83c7909
@@ -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 lesson = {
|
||||
id: null,
|
||||
title: 'Untitled lesson',
|
||||
title: '',
|
||||
profile: undefined,
|
||||
call_url: undefined,
|
||||
lesson_at: new Date(),
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import ListIcon from 'carbon-icons-svelte/lib/List.svelte';
|
||||
import IbmWatsonKnowledgeStudioIcon from 'carbon-icons-svelte/lib/IbmWatsonKnowledgeStudio.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 TabContent from '$lib/components/TabContent/index.svelte';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
@@ -221,7 +222,7 @@
|
||||
isDisabled={$isLoading}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
>
|
||||
<MachineLearningModel size={20} class="carbon-icon mr-3" />
|
||||
<MagicWandFilled size={20} class="carbon-icon mr-3" />
|
||||
AI
|
||||
<Popover
|
||||
caret
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
<script>
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import get from 'lodash/get';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
export let course;
|
||||
|
||||
function handleChange(html) {
|
||||
const _course = cloneDeep(course);
|
||||
set(_course, 'metadata.description', html);
|
||||
course = _course;
|
||||
}
|
||||
export let value = '';
|
||||
</script>
|
||||
|
||||
<div class="h-2/5">
|
||||
<TextEditor value={get(course, 'metadata.description')} onChange={handleChange} />
|
||||
<TextEditor bind:value />
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
<script>
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import get from 'lodash/get';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
export let course;
|
||||
|
||||
function handleChange(html) {
|
||||
const _course = cloneDeep(course);
|
||||
set(_course, 'metadata.goals', html);
|
||||
course = _course;
|
||||
}
|
||||
export let value = '';
|
||||
</script>
|
||||
|
||||
<div class="h-2/5">
|
||||
<TextEditor value={get(course, 'metadata.goals')} onChange={handleChange} />
|
||||
<TextEditor bind:value />
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
<script>
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import get from 'lodash/get';
|
||||
<script lang="ts">
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
export let course;
|
||||
|
||||
function handleChange(html) {
|
||||
const _course = cloneDeep(course);
|
||||
set(_course, 'metadata.requirements', html);
|
||||
course = _course;
|
||||
}
|
||||
export let value: string;
|
||||
</script>
|
||||
|
||||
<div class="h-2/5">
|
||||
<TextEditor value={get(course, 'metadata.requirements')} onChange={handleChange} />
|
||||
<TextEditor bind:value />
|
||||
</div>
|
||||
|
||||
@@ -58,6 +58,9 @@
|
||||
placeholder=""
|
||||
bind:value={review.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">
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
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 ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import ArrowUpRightIcon from 'carbon-icons-svelte/lib/ArrowUpRight.svelte';
|
||||
|
||||
import Container from './Container.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import CloseButton from '$lib/components/Buttons/Close/index.svelte';
|
||||
import HeaderForm from './HeaderForm.svelte';
|
||||
@@ -21,47 +22,70 @@
|
||||
import ChevronLeftIcon from 'carbon-icons-svelte/lib/ChevronLeft.svelte';
|
||||
|
||||
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 courseId;
|
||||
export let course: Course;
|
||||
export let courseId: string;
|
||||
|
||||
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
|
||||
let loading = false;
|
||||
let show = false;
|
||||
const sections = [
|
||||
|
||||
interface Section {
|
||||
key: number;
|
||||
path: string;
|
||||
title: string;
|
||||
enableAIWriter?: boolean;
|
||||
initPrompt?: string;
|
||||
}
|
||||
|
||||
const sections: Section[] = [
|
||||
{
|
||||
key: 1,
|
||||
path: '',
|
||||
title: 'Header'
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
title: 'Requirement'
|
||||
path: 'metadata.requirements',
|
||||
title: 'Requirement',
|
||||
enableAIWriter: true,
|
||||
initPrompt: 'Generate a course requirement for this given course:'
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
title: 'Description'
|
||||
path: 'metadata.description',
|
||||
title: 'Description',
|
||||
enableAIWriter: true,
|
||||
initPrompt: 'Generate a course description for this given course:'
|
||||
},
|
||||
{
|
||||
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,
|
||||
path: '',
|
||||
title: 'Reviews'
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
path: '',
|
||||
title: 'Instructor'
|
||||
},
|
||||
{
|
||||
key: 7,
|
||||
path: '',
|
||||
title: 'Pricing'
|
||||
}
|
||||
];
|
||||
let selectedSection = null;
|
||||
let selectedSection: Section | null = null;
|
||||
|
||||
function handleClose() {
|
||||
console.log('closing');
|
||||
if (!selectedSection) {
|
||||
goto(`/courses/${courseId}`);
|
||||
}
|
||||
@@ -69,11 +93,13 @@
|
||||
selectedSection = null;
|
||||
}
|
||||
|
||||
function handleSectionSelect(sectionKey) {
|
||||
function handleSectionSelect(sectionKey: number) {
|
||||
return () => {
|
||||
selectedSection = sections.find((section) => section.key === sectionKey);
|
||||
selectedSection = sections.find((section) => section.key === sectionKey) || null;
|
||||
|
||||
if (selectedSection) {
|
||||
const sectionId = selectedSection.title.toLowerCase();
|
||||
|
||||
const sectionEl = document.getElementById(sectionId);
|
||||
if (sectionEl) {
|
||||
sectionEl.scrollIntoView({
|
||||
@@ -120,8 +146,8 @@
|
||||
|
||||
<aside
|
||||
class={`${
|
||||
show ? '-translate-x-[100%] fixed z-[9999]' : 'translate-x-0 fixed z-[9999]'
|
||||
} 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`}
|
||||
show ? '-translate-x-[100%] fixed z-[50]' : 'translate-x-0 fixed z-[50]'
|
||||
} 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">
|
||||
<IconButton
|
||||
@@ -185,18 +211,36 @@
|
||||
<IconButton onClick={handleClose}>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" title="Go back" />
|
||||
</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 class="title-content p-2 overflow-y-auto">
|
||||
{#if selectedSection.key === 1}
|
||||
<HeaderForm bind:course />
|
||||
{:else if selectedSection.key === 2}
|
||||
<RequirementForm bind:course />
|
||||
<RequirementForm bind:value={course.metadata.requirements} />
|
||||
{:else if selectedSection.key === 3}
|
||||
<DescriptionForm bind:course />
|
||||
<DescriptionForm bind:value={course.metadata.description} />
|
||||
{:else if selectedSection.key === 4}
|
||||
<GoalsForm bind:course />
|
||||
<GoalsForm bind:value={course.metadata.goals} />
|
||||
{:else if selectedSection.key === 5}
|
||||
<ReviewsForm bind:course />
|
||||
{:else if selectedSection.key === 6}
|
||||
|
||||
@@ -63,7 +63,11 @@
|
||||
/>
|
||||
|
||||
<!-- 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">
|
||||
<!-- Pricing -->
|
||||
<div class="mb-6">
|
||||
|
||||
@@ -167,12 +167,14 @@
|
||||
<!-- Course Details -->
|
||||
<div class="course-content w-full p-3 lg:w-10/12 lg:mr-14">
|
||||
<!-- 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}
|
||||
<a
|
||||
href="{$page.url.pathname}{navItem.key}"
|
||||
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}
|
||||
</a>
|
||||
|
||||
@@ -16,6 +16,19 @@
|
||||
description: ''
|
||||
};
|
||||
|
||||
let userPrompt = '';
|
||||
|
||||
function onClose() {
|
||||
createCourseModal.update(() => ({
|
||||
open: false,
|
||||
title: '',
|
||||
description: '',
|
||||
emails: '',
|
||||
tutors: '',
|
||||
students: ''
|
||||
}));
|
||||
}
|
||||
|
||||
async function createCourse(e) {
|
||||
const { hasError, fieldErrors } = validateForm($createCourseModal);
|
||||
|
||||
@@ -32,6 +45,9 @@
|
||||
console.log(`newGroup`, newGroup);
|
||||
const { id: group_id } = newGroup[0];
|
||||
|
||||
// Set userPrompt to the description
|
||||
userPrompt = description;
|
||||
|
||||
// 2. Create course with group_id
|
||||
const { data: newCourse } = await supabase
|
||||
.from('course')
|
||||
@@ -55,7 +71,7 @@
|
||||
role_id: ROLE.TUTOR
|
||||
});
|
||||
|
||||
$createCourseModal.open = false;
|
||||
onClose();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -64,7 +80,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<Modal
|
||||
onClose={() => ($createCourseModal.open = false)}
|
||||
{onClose}
|
||||
bind:open={$createCourseModal.open}
|
||||
width="w-4/5 md:w-2/5"
|
||||
modalHeading="Create a Course"
|
||||
@@ -73,13 +89,13 @@
|
||||
<TextField
|
||||
label="Course name"
|
||||
bind:value={$createCourseModal.title}
|
||||
autoFocus={true}
|
||||
placeholder="Your course name"
|
||||
className="mb-4"
|
||||
isRequired={true}
|
||||
errorMessage={errors.title}
|
||||
autoComplete={false}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label="Short description"
|
||||
bind:value={$createCourseModal.description}
|
||||
@@ -89,6 +105,8 @@
|
||||
className="mb-4"
|
||||
isRequired={true}
|
||||
errorMessage={errors.description}
|
||||
isAIEnabled={true}
|
||||
initAIPrompt="Write a 30 word description for a course titled: {$createCourseModal.title}"
|
||||
/>
|
||||
|
||||
<div class="mt-5 flex items-center">
|
||||
|
||||
@@ -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 disabled = false;
|
||||
export let placeholder = 'Write your answer...';
|
||||
export let value = '';
|
||||
export let rows = '3';
|
||||
export let maxRows = 3;
|
||||
export let rows = 3;
|
||||
// export let maxRows = 3;
|
||||
export let className = '';
|
||||
export let labelClassName = '';
|
||||
export let bgColor = 'bg-gray-50 focus:bg-primary-50 dark:bg-white';
|
||||
@@ -12,19 +15,33 @@
|
||||
export let errorMessage = '';
|
||||
export let isRequired = false;
|
||||
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`;
|
||||
$: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`;
|
||||
// $: minHeight = `${1 + parseInt(rows, 10) * 1.2}em`;
|
||||
// $: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`;
|
||||
</script>
|
||||
|
||||
<label class="block w-full relative {className}">
|
||||
<label for="text-field" class="block w-full relative {className}">
|
||||
{#if label}
|
||||
<p for="text-field" class="dark:text-white text-left {labelClassName}">
|
||||
{label}
|
||||
<p class="dark:text-white text-left w-full flex items-center justify-between {labelClassName}">
|
||||
<span>
|
||||
{label}
|
||||
{#if isRequired}
|
||||
<span class="text-red-700">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
{#if isRequired}
|
||||
<span class="text-red-700">*</span>
|
||||
{#if isAIEnabled}
|
||||
<CustomPromptBtn
|
||||
defaultPrompt={initAIPrompt}
|
||||
alignPopover={aiAlignPopover}
|
||||
handleInsert={(v) => {
|
||||
value = v;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
placeholder="Write your subtitle here"
|
||||
className="w-full mb-5"
|
||||
bind:value={$landingPageSettings.header.subtitle}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<SectionTitle>Actions</SectionTitle>
|
||||
@@ -221,6 +222,7 @@
|
||||
placeholder="Write your Content here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.aboutUs.content}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<div>
|
||||
@@ -268,6 +270,7 @@
|
||||
labelClassName="font-light"
|
||||
placeholder="Write your subtitle here"
|
||||
bind:value={$landingPageSettings.courses.subtitle}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
@@ -301,6 +304,7 @@
|
||||
placeholder="Write your answer here"
|
||||
bind:value={item.content}
|
||||
className="mb-5"
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<IconButton onClick={() => deleteFaq(item.id)}>
|
||||
<TrashCan size={24} class="fill-red-700" />
|
||||
@@ -320,6 +324,7 @@
|
||||
placeholder="Write your answer here"
|
||||
bind:value={newQuestion.content}
|
||||
className="mb-5"
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} label="Save" onClick={saveNewFAQ} />
|
||||
@@ -359,6 +364,7 @@
|
||||
placeholder="Write your subtitle here"
|
||||
className="mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.contact.subtitle}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<TextField
|
||||
label="Phone number"
|
||||
@@ -396,6 +402,7 @@
|
||||
placeholder="Write your subtitle here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.mailinglist.subtitle}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
|
||||
@@ -45,5 +45,5 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<Editor {value} {apiKey} {conf} />
|
||||
<Editor bind:value {apiKey} {conf} />
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,9 @@ export * from './config';
|
||||
export * from './dashboard';
|
||||
|
||||
interface CourseMetadata {
|
||||
requirements?: string;
|
||||
description?: string;
|
||||
goals?: string;
|
||||
requirements: string;
|
||||
description: string;
|
||||
goals: string;
|
||||
videoUrl?: '';
|
||||
showDiscount: false;
|
||||
discount: 0;
|
||||
@@ -95,7 +95,7 @@ export interface Course {
|
||||
organization_id?: string /* foreign key to organization.id */;
|
||||
logo?: string;
|
||||
slug?: any; // type unknown;
|
||||
metadata?: CourseMetadata;
|
||||
metadata: CourseMetadata;
|
||||
cost: number;
|
||||
currency?: string;
|
||||
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);
|
||||
}
|
||||
@@ -117,221 +117,234 @@
|
||||
</PageNav>
|
||||
|
||||
<PageBody width="max-w-6xl" padding="p-0">
|
||||
<section
|
||||
class="w-full lg:w-11/12 p-3 lg:px-4 m-auto"
|
||||
use:dndzone={{
|
||||
items: $lessons,
|
||||
flipDurationMs,
|
||||
dragDisabled: isStudent,
|
||||
dropTargetStyle: {
|
||||
border: '2px #1d4ed8 solid',
|
||||
'border-style': 'dashed'
|
||||
}
|
||||
}}
|
||||
on:consider={handleDndConsider}
|
||||
on:finalize={handleDndFinalize}
|
||||
>
|
||||
{#each $lessons as lesson (lesson.id)}
|
||||
<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
|
||||
value={getLessonOrder(lesson.id)}
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
/>
|
||||
</div>
|
||||
{#if $lessons.length}
|
||||
<section
|
||||
class="w-full lg:w-11/12 p-3 lg:px-4 m-auto"
|
||||
use:dndzone={{
|
||||
items: $lessons,
|
||||
flipDurationMs,
|
||||
dragDisabled: isStudent,
|
||||
dropTargetStyle: {
|
||||
border: '2px #1d4ed8 solid',
|
||||
'border-style': 'dashed'
|
||||
}
|
||||
}}
|
||||
on:consider={handleDndConsider}
|
||||
on:finalize={handleDndFinalize}
|
||||
>
|
||||
{#each $lessons as lesson (lesson.id)}
|
||||
<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
|
||||
value={getLessonOrder(lesson.id)}
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="w-4/5">
|
||||
{#if lessonEditing === lesson.id}
|
||||
<TextField bind:value={lesson.title} autofocus={true} className="max-w-lg" />
|
||||
{:else}
|
||||
<h3 class="dark:text-white text-xl m-0 flex items-center">
|
||||
<a
|
||||
href={isStudent && !lesson.is_unlocked
|
||||
? $page.url.pathname
|
||||
: '/courses/' + $course.id + '/lessons/' + lesson.id}
|
||||
class="dark:text-white no-underline hover:underline font-semibold text-black {isStudent &&
|
||||
!lesson.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
>
|
||||
{lesson.title}
|
||||
</a>
|
||||
</h3>
|
||||
{/if}
|
||||
<!-- <div class="flex items-center mb-2">
|
||||
<div class="w-4/5">
|
||||
{#if lessonEditing === lesson.id}
|
||||
<TextField bind:value={lesson.title} autofocus={true} className="max-w-lg" />
|
||||
{:else}
|
||||
<h3 class="dark:text-white text-xl m-0 flex items-center">
|
||||
<a
|
||||
href={isStudent && !lesson.is_unlocked
|
||||
? $page.url.pathname
|
||||
: '/courses/' + $course.id + '/lessons/' + lesson.id}
|
||||
class="dark:text-white no-underline hover:underline font-semibold text-black {isStudent &&
|
||||
!lesson.is_unlocked
|
||||
? 'cursor-not-allowed'
|
||||
: ''}"
|
||||
>
|
||||
{lesson.title}
|
||||
</a>
|
||||
</h3>
|
||||
{/if}
|
||||
<!-- <div class="flex items-center mb-2">
|
||||
<div class="flex items-center mr-2">
|
||||
{resources.map((r) => `${r.value} ${r.label}`).join(', ')}
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div
|
||||
class="flex items-start justify-between flex-col lg:flex-row lg:items-center mt-2 w-4/5"
|
||||
>
|
||||
<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">
|
||||
<div
|
||||
class="flex items-start justify-between flex-col lg:flex-row lg:items-center mt-2 w-4/5"
|
||||
>
|
||||
<div class="lg:mb-0">
|
||||
{#if lessonEditing === lesson.id}
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
lessonEditing = null;
|
||||
handleSaveLesson(lesson, $course.id);
|
||||
}}
|
||||
>
|
||||
<SaveIcon size={24} class="carbon-icon dark:text-white" />
|
||||
</IconButton>
|
||||
<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}
|
||||
<IconButton onClick={() => (lessonEditing = lesson.id)}>
|
||||
<WatsonHealthTextAnnotationToggle
|
||||
size={32}
|
||||
class="carbon-icon dark:text-white"
|
||||
<a href="." class="flex items-center hover:underline mb-2">
|
||||
<Avatar
|
||||
className="w-6 h-6"
|
||||
src={lesson.profile.avatar_url}
|
||||
name="Avatar placeholder"
|
||||
/>
|
||||
</IconButton>
|
||||
<p class="dark:text-white ml-2 text-sm">
|
||||
{lesson.profile.fullname}
|
||||
</p>
|
||||
</a>
|
||||
{/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" />
|
||||
<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>
|
||||
|
||||
<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) {
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<div class="hidden md:block">
|
||||
{#if lessonEditing === lesson.id}
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
lessonEditing = null;
|
||||
handleSaveLesson(lesson, $course.id);
|
||||
} else {
|
||||
lessonEditing = lesson.id;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem danger text="Delete" on:click={handleDelete(lesson.id)} />
|
||||
</OverflowMenu>
|
||||
</div>
|
||||
</RoleBasedSecurity>
|
||||
}}
|
||||
>
|
||||
<SaveIcon size={24} class="carbon-icon dark:text-white" />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<IconButton onClick={() => (lessonEditing = lesson.id)}>
|
||||
<WatsonHealthTextAnnotationToggle
|
||||
size={32}
|
||||
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>
|
||||
{: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>
|
||||
{: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>
|
||||
</Box>
|
||||
{/if}
|
||||
</PageBody>
|
||||
</CourseContainer>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user