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 <[email protected]>
Co-authored-by: Tunji Adeyemi <[email protected]>


Merge-request: CLASSROOMIO-MR-74
Merged-by: Best Admin <[email protected]>
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 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">
+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 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
+1 -1
View File
@@ -45,5 +45,5 @@
</script>
<div>
<Editor {value} {apiKey} {conf} />
<Editor bind:value {apiKey} {conf} />
</div>
+4 -4
View File
@@ -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);
}
+208 -195
View File
@@ -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>