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>