CLASSROOMIO-T-95: Upload to S3 and Muse AI

This commit is contained in:
Rotimi Best
2023-08-10 12:46:14 +01:00
parent ec807a1db9
commit 0ff76e4aa8
21 changed files with 460 additions and 206 deletions
+3 -2
View File
@@ -13,6 +13,7 @@
"prepare": "svelte-kit sync" "prepare": "svelte-kit sync"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.386.0",
"@sentry/browser": "^7.61.0", "@sentry/browser": "^7.61.0",
"@sentry/integrations": "^7.61.0", "@sentry/integrations": "^7.61.0",
"@sentry/tracing": "^7.61.0", "@sentry/tracing": "^7.61.0",
@@ -25,10 +26,12 @@
"@types/pluralize": "0.0.30", "@types/pluralize": "0.0.30",
"ai": "^2.1.31", "ai": "^2.1.31",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"axios": "^1.4.0",
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"canvas-confetti": "^1.6.0", "canvas-confetti": "^1.6.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"copy-to-clipboard": "^3.3.3", "copy-to-clipboard": "^3.3.3",
"form-data": "^4.0.0",
"hotkeys-js": "^3.11.2", "hotkeys-js": "^3.11.2",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -47,8 +50,6 @@
"@babel/plugin-transform-runtime": "^7.22.9", "@babel/plugin-transform-runtime": "^7.22.9",
"@babel/preset-env": "^7.22.9", "@babel/preset-env": "^7.22.9",
"@babel/runtime": "^7.22.6", "@babel/runtime": "^7.22.6",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@sveltejs/kit": "^1.22.4", "@sveltejs/kit": "^1.22.4",
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.14.196", "@types/lodash": "^4.14.196",
@@ -0,0 +1,25 @@
<script>
import { Accordion, AccordionItem } from 'carbon-components-svelte';
</script>
<Accordion>
<AccordionItem title="Natural Language Classifier">
<p>
Natural Language Classifier uses advanced natural language processing and machine learning
techniques to create custom classification models. Users train their data and the service
predicts the appropriate category for the inputted text.
</p>
</AccordionItem>
<AccordionItem title="Natural Language Understanding">
<p>
Analyze text to extract meta-data from content such as concepts, entities, emotion, relations,
sentiment and more.
</p>
</AccordionItem>
<AccordionItem title="Language Translator">
<p>
Translate text, documents, and websites from one language to another. Create industry or
region-specific translations via the service's customization capability.
</p>
</AccordionItem>
</Accordion>
@@ -3,6 +3,9 @@
import LocalVideo from './LocalVideo.svelte'; import LocalVideo from './LocalVideo.svelte';
import * as CONSTANTS from './constants'; import * as CONSTANTS from './constants';
export let lessonId = '';
export let saveLesson = () => {};
let tabs = CONSTANTS.videoTabs; let tabs = CONSTANTS.videoTabs;
let currentTab = tabs[0].value; let currentTab = tabs[0].value;
@@ -34,7 +37,7 @@
{#if currentTab === 1} {#if currentTab === 1}
<YoutubeVideo /> <YoutubeVideo />
{:else} {:else}
<LocalVideo /> <LocalVideo {lessonId} {saveLesson} />
{/if} {/if}
</main> </main>
</section> </section>
@@ -1,45 +1,95 @@
<script> <script>
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { deleteLessonVideo } from '$lib/components/Course/components/Lesson/store/lessons'; import {
lesson,
deleteLessonVideo,
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons';
import { enhance } from '$app/forms';
import { Moon } from 'svelte-loading-spinners';
export let lessonId = '';
export let saveLesson = () => {};
let isWrongFormat = false; let isWrongFormat = false;
let isUploaded = false; let isUploaded = false;
let isLoaded = false; let isLoaded = false;
let isBig = false; let isBig = false;
let fileInput;
let submit;
let uploadedFileUrl = '';
let isLoading = false;
const uploadVideo = () => { function isVideoAdded(link) {
// if the file is too big or not the correct format return $lesson.materials?.videos?.find((v) => v.link === link);
isWrongFormat = true; }
// when the upload has been sucessfully completed async function isDoneUploading(response) {
// here the you set the video_url to something and that would be displayed on the page uploadedFileUrl = response?.success && response?.url;
isUploaded = false;
// closes the modal if (uploadedFileUrl && !isVideoAdded(uploadedFileUrl)) {
// $uploadCourseVideo.isModalOpen = false; $lesson.materials.videos = [
}; ...$lesson.materials.videos,
{
type: 'muse',
link: uploadedFileUrl,
metadata: {}
}
];
saveLesson();
}
}
const onUpload = () => { $: isDoneUploading($uploadCourseVideoStore.formRes);
// PROBABLY GET THE SNAPSHOT OR SOMETHING
isLoaded = true;
};
</script> </script>
{#if isLoaded == false} {#if isLoaded === false}
<button <button
on:click={onUpload} type="button"
class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl" on:click={() => (fileInput && !isLoading ? fileInput.click() : null)}
class="w-full h-full"
disabled={isLoading}
> >
<img src="/upload-video.svg" alt="upload" /> <form
<span class="pt-3"> class="h-full w-full flex flex-col items-center justify-center border border-blue-300 border-dashed rounded-xl"
<h3 class="text-center text-xl font-normal dark:text-white">Upload video</h3> method="POST"
<p class=" text-center text-sm font-normal"> action="?/create"
Select file( Mp4, MOV, AVI) to upload or drag and drop video here.<br /> Size must be less than enctype="multipart/form-data"
20 MB. use:enhance={() => {
</p> isLoading = true;
</span>
return async ({ update }) => {
await update();
isLoading = false;
isLoaded = true;
};
}}
>
{#if isLoading}
<Moon size="40" color="#1d4ed8" unit="px" duration="1s" />
<p class="mt-5">Uploading Video</p>
{:else}
<img src="/upload-video.svg" alt="upload" />
<span class="pt-3">
<h3 class="text-center text-xl font-normal dark:text-white">Upload video</h3>
<p class=" text-center text-sm font-normal">
Select file( Mp4, MOV, AVI) to upload to your lesson.
</p>
</span>
{/if}
<input
style="display:none;"
type="file"
accept="video/*"
name="file"
on:change={() => submit.click()}
bind:this={fileInput}
/>
<input type="text" name="lessonId" value={lessonId} style="display: none;" />
<input style="display:none;" type="submit" bind:this={submit} />
</form>
</button> </button>
{:else if isBig && isUploaded == false} {:else if isBig && isUploaded === false}
<div class="h-full w-full flex flex-col items-center justify-center rounded-xl"> <div class="h-full w-full flex flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" /> <img src="/video-upload-error.svg" alt="upload error" />
<span class="pt-3 pb-2"> <span class="pt-3 pb-2">
@@ -52,7 +102,7 @@
</span> </span>
<PrimaryButton label="Try again" /> <PrimaryButton label="Try again" />
</div> </div>
{:else if isWrongFormat && isUploaded == false} {:else if isWrongFormat && isUploaded === false}
<div class="h-full w-full flex flex-col items-center justify-center rounded-xl"> <div class="h-full w-full flex flex-col items-center justify-center rounded-xl">
<img src="/video-upload-error.svg" alt="upload error" /> <img src="/video-upload-error.svg" alt="upload error" />
<span class="pt-3 pb-2"> <span class="pt-3 pb-2">
@@ -72,17 +122,19 @@
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<div class="rounded-sm overflow-hidden"> <div class="rounded-sm overflow-hidden">
<img src="/course-video.png" alt="coursevideo" class="object-cover" /> <video class="w-[200px]">
<source src={uploadedFileUrl} type="video/mp4" />
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
<!-- Captions are optional -->
<track kind="captions" />
</video>
</div> </div>
<p>Video.png</p> <p>{fileInput?.files?.[0]?.name || uploadedFileUrl}</p>
</span> </span>
<button on:click={deleteLessonVideo}> <button on:click={deleteLessonVideo}>
<img src="/delete-video.svg" alt="deletevideo" class="dark:invert" /> <img src="/delete-video.svg" alt="deletevideo" class="dark:invert" />
</button> </button>
</div> </div>
</div> </div>
<div class="ml-auto">
<PrimaryButton label="upload" onClick={uploadVideo} />
</div>
</div> </div>
{/if} {/if}
@@ -1,47 +1,49 @@
<script> <script>
import { CopyButton, Tag } from 'carbon-components-svelte'; import { CopyButton, Tag } from 'carbon-components-svelte';
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte'; import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte'; import TextField from '$lib/components/Form/TextField.svelte';
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons'; import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte'; import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Chip from '$lib/components/Chip/Text.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
let youtubeLinks = ''; let youtubeLinks = '';
let error = ''; let error = '';
function getVideoUrls(urls = '') {
return (urls || '').split(',').filter((url) => !!url.trim());
}
function addVideo() { function addVideo() {
const links = youtubeLinks.split(',').map((link) => link.trim()); const links = getVideoUrls(youtubeLinks);
const validLinks = links.filter(isValidYouTubeLink); const validLinks = links.filter(isValidYouTubeLink);
if (validLinks.length === 0) { if (validLinks.length === 0) {
error = 'Invalid YouTube link(s)'; error = 'Invalid YouTube link(s)';
} else { } else {
const existingLinks = $lesson.materials.video_url const existingLinks = $lesson?.materials?.videos || [];
.split(',')
.map((link) => link.trim()) $lesson.materials.videos = [
.filter(Boolean);
const updatedLinks = [
...existingLinks, ...existingLinks,
...validLinks.filter((link) => !existingLinks.includes(link)) ...validLinks.map((link = '') => ({
type: 'youtube',
link,
metadata: {}
}))
]; ];
$lesson.materials.video_url = updatedLinks.join(',').trim();
youtubeLinks = ''; youtubeLinks = '';
error = ''; error = '';
} }
} }
function isValidYouTubeLink(link) { function isValidYouTubeLink(link = '') {
const youtubeRegex = const youtubeRegex =
/^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/; /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
return youtubeRegex.test(link.trim()); return youtubeRegex.test(link.trim());
} }
function removeVideo(index) { function removeVideo(index = 0) {
const links = $lesson.materials.video_url.split(',').map((link) => link.trim()); $lesson.materials.videos = $lesson.materials.videos.filter((v, i) => i !== index);
links.splice(index, 1);
$lesson.materials.video_url = links.join(',').trim();
} }
</script> </script>
@@ -58,22 +60,23 @@
<PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} /> <PrimaryButton label="Add Video" className="rounded-md" onClick={addVideo} />
</div> </div>
<p class="mt-4 pl-2 text-sm"> <p class="mt-4 pl-2 text-sm">
Added : <strong Videos added: <strong>
>{($lesson?.materials?.video_url && $lesson.materials.video_url.trim().split(',').length) || 0} Videos</strong {$lesson?.materials?.videos.filter((v) => v.type === 'youtube' && isValidYouTubeLink(v.link))
> .length || 0}
</strong>
</p> </p>
<div class=""> <div class="">
{#each $lesson?.materials?.video_url && $lesson.materials.video_url {#each $lesson?.materials?.videos as video, index}
.trim() {#if video.type === 'youtube'}
.split(',') as videoLink, index} <div class="flex items-center gap-1">
<div class="flex items-center gap-1"> <Tag type="blue">
<Tag type="blue"> {video.link}
{videoLink} </Tag>
</Tag> <CopyButton text={video.link} feedback="Copied to clipboard" />
<CopyButton text={videoLink} feedback="Copied to clipboard" /> <IconButton value="delete-video" onClick={() => removeVideo(index)}>
<IconButton value="delete-video" onClick={() => removeVideo(index)}> <TrashCanIcon size={16} class="carbon-icon dark:text-white" />
<TrashCanIcon size={16} class="carbon-icon dark:text-white" /> </IconButton>
</IconButton> </div>
</div> {/if}
{/each} {/each}
</div> </div>
@@ -1,11 +1,13 @@
<script lang="ts"> <script lang="ts">
import { useCompletion } from 'ai/svelte'; import { useCompletion } from 'ai/svelte';
import merge from 'lodash/merge';
import MODES from '$lib/utils/constants/mode.js'; import MODES from '$lib/utils/constants/mode.js';
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo'; import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import Modal from '$lib/components/Modal/index.svelte'; import Modal from '$lib/components/Modal/index.svelte';
import { Popover } from 'carbon-components-svelte'; import { Popover } from 'carbon-components-svelte';
import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte'; import AlignBoxTopLeftIcon from 'carbon-icons-svelte/lib/AlignBoxTopLeft.svelte';
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
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';
@@ -27,7 +29,8 @@
import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte'; import VideoUploader from '$lib/components/Course/components/Lesson/Materials/Video/Index.svelte';
import { course } from '$lib/components/Course/store'; import { course } from '$lib/components/Course/store';
import * as CONSTANTS from './constants'; import * as CONSTANTS from './constants';
import { browser } from '$app/environment'; import { fetchLesson } from '$lib/utils/services/courses';
import type { LessonPage } from '$lib/utils/types';
export let mode = MODES.view; export let mode = MODES.view;
export let prevMode = ''; export let prevMode = '';
@@ -36,11 +39,9 @@
let tabs = CONSTANTS.tabs; let tabs = CONSTANTS.tabs;
let currentTab = tabs[0].value; let currentTab = tabs[0].value;
let errors = {}; let errors = {};
let player = {};
let textareaRef = {}; let textareaRef = {};
let aiButtonRef = {}; let aiButtonRef = {};
let openPopover = false; let openPopover = false;
let videoUrls: Array<string> = [];
let aiButtonClass = let aiButtonClass =
'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 rounded-md w-full mb-2'; 'flex items-center px-5 py-2 border border-gray-300 hover:bg-gray-200 rounded-md w-full mb-2';
@@ -49,26 +50,26 @@
() => () =>
(currentTab = tab); (currentTab = tab);
function initPlyr(_player, videoUrl = '') { // problem: after we save an uploaded video,
if (!browser) return; async function saveLesson() {
const { data } = await fetchLesson(lessonId);
const materials = merge(data, $lesson.materials);
const players = Array.from(document.querySelectorAll('.plyr-video-trigger')).map((p) => { lesson.update((l) => ({
// @ts-ignore ...l,
return new Plyr(p); materials
}); }));
handleUpdateLessonMaterials($lesson, lessonId);
// @ts-ignore
window.players = players;
} }
function handleSave(prevMode) { function handleSave(prevMode: string) {
if (prevMode === MODES.edit) { if (prevMode === MODES.edit) {
handleUpdateLessonMaterials($lesson, lessonId); saveLesson();
} }
} }
function addBadgeValueToTab(materials) { function addBadgeValueToTab(materials) {
const { slide_url, video_url, note } = materials; const { slide_url, videos, note } = materials;
tabs = tabs.map((tab) => { tabs = tabs.map((tab) => {
let badgeValue; let badgeValue;
@@ -78,7 +79,7 @@
} else if (tab.value === 2) { } else if (tab.value === 2) {
badgeValue = !!note ? 1 : 0; badgeValue = !!note ? 1 : 0;
} else { } else {
badgeValue = typeof video_url === 'string' && !!video_url ? video_url.split(',').length : 0; badgeValue = Array.isArray(videos) ? videos.length : 0;
} }
tab.badgeValue = badgeValue; tab.badgeValue = badgeValue;
@@ -114,19 +115,16 @@
} }
const onClose = () => { const onClose = () => {
handleUpdateLessonMaterials($lesson, lessonId); saveLesson();
$uploadCourseVideoStore.isModalOpen = false; $uploadCourseVideoStore.isModalOpen = false;
$uploadCourseVideoStore.formRes = null;
}; };
$: handleSave(prevMode); $: handleSave(prevMode);
$: initPlyr(player, $lesson.materials.video_url);
$: addBadgeValueToTab($lesson.materials); $: addBadgeValueToTab($lesson.materials);
$: updateNoteByCompletion($completion); $: updateNoteByCompletion($completion);
$: videoUrls = ($lesson?.materials?.video_url || '').split(',').filter((url) => !!url.trim());
</script> </script>
<Modal <Modal
@@ -135,7 +133,7 @@
width="w-4/5 h-[566px]" width="w-4/5 h-[566px]"
modalHeading="Add a Video" modalHeading="Add a Video"
> >
<VideoUploader {onClose} /> <VideoUploader {lessonId} {saveLesson} />
</Modal> </Modal>
<Tabs {tabs} {currentTab} {onChange}> <Tabs {tabs} {currentTab} {onChange}>
@@ -236,34 +234,45 @@
</TabContent> </TabContent>
<TabContent value={tabs[2].value} index={currentTab}> <TabContent value={tabs[2].value} index={currentTab}>
{#if mode === MODES.edit && videoUrls.length} {#if mode === MODES.edit && $lesson.materials.videos.length}
<PrimaryButton label="Add/Edit Video" onClick={openAddVideoModal} className="mb-2" /> <PrimaryButton label="Add/Edit Video(s)" onClick={openAddVideoModal} className="mb-2" />
{/if} {/if}
<!-- Line 230 would run if its in edit mode or the video is truthy but then in line 235 we are mapping for an empty string --> {#if $lesson.materials.videos.length}
{#if videoUrls.length}
<div class="flex flex-col items-start w-full h-full"> <div class="flex flex-col items-start w-full h-full">
{#each videoUrls as url, index} {#each $lesson.materials.videos as video, index}
{#if mode === MODES.edit}
<div class="ml-auto">
<IconButton
value="delete-video"
contained={true}
onClick={() => deleteLessonVideo(index)}
>
<TrashCanIcon size={20} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{/if}
<div class="w-full h-full flex flex-col gap-2 overflow-hidden"> <div class="w-full h-full flex flex-col gap-2 overflow-hidden">
{#if url.includes('youtube')} {#key video.link}
<div class="mb-5"> <div class="mb-5">
<div {#if video.type === 'youtube'}
bind:this={player} <iframe
class="plyr-video-trigger" width="100%"
data-plyr-provider="youtube" height="569"
data-plyr-embed-id={getEmbedId(url)} class="iframe"
/> src={formatYoutubeVideo(video.link, errors)}
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen
/>
{:else}
<video class="plyr-video-trigger" playsinline controls>
<source src={video.link} type="video/mp4" />
<track kind="captions" />
</video>
{/if}
</div> </div>
{:else} {/key}
<video bind:this={player} class="plyr-video-trigger" playsinline controls>
<source
src="https://pub-bd38ff7251aa402a93762181a947f7ea.r2.dev/generate-lesson-draft.mp4"
type="video/mp4"
/>
<!-- <source src="/path/to/video.webm" type="video/webm" /> -->
<!-- Captions are optional -->
<track kind="captions" />
</video>
{/if}
</div> </div>
{/each} {/each}
</div> </div>
@@ -1,21 +1,23 @@
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import type { Writable, Updater } from 'svelte/store'; import type { Writable, Updater } from 'svelte/store';
import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses'; import { createLesson, updateLesson, deleteLesson } from '$lib/utils/services/courses';
import type { Lesson, Course } from '$lib/utils/types'; import type { Lesson, Course, LessonPage } from '$lib/utils/types';
export const uploadCourseVideoStore = writable({ export const uploadCourseVideoStore = writable({
isModalOpen: false isModalOpen: false,
formRes: null
}); });
export const lessons: Writable<Lesson[]> = writable([]); export const lessons: Writable<Lesson[]> = writable([]);
export const lesson = writable({
export const lesson = writable<LessonPage>({
id: null, id: null,
totalExercises: 0, totalExercises: 0,
is_complete: false, is_complete: false,
materials: { materials: {
note: '', note: '',
slide_url: '', slide_url: '',
video_url: '' videos: []
}, },
exercises: [] exercises: []
}); });
@@ -103,14 +105,12 @@ export async function handleUpdateLessonMaterials(lesson: any, lessonId: Lesson[
export const deleteLessonVideo = (index: any) => { export const deleteLessonVideo = (index: any) => {
lesson.update((currentLesson) => { lesson.update((currentLesson) => {
const links = currentLesson.materials.video_url.split(',').map((link) => link.trim()); const videos = currentLesson.materials.videos || [];
links.splice(index, 1);
const updatedVideoUrl = links.join(',').trim();
return { return {
...currentLesson, ...currentLesson,
materials: { materials: {
...currentLesson.materials, ...currentLesson.materials,
video_url: updatedVideoUrl videos: videos?.filter((video, i) => i !== index)
} }
}; };
}); });
@@ -1,5 +1,5 @@
<script> <script>
import CaretSortDownIcon from 'carbon-icons-svelte/lib/CaretSortDown.svelte'; import { slide } from 'svelte/transition';
import ChevronDownIcon from 'carbon-icons-svelte/lib/ChevronDown.svelte'; import ChevronDownIcon from 'carbon-icons-svelte/lib/ChevronDown.svelte';
import ChevronUpIcon from 'carbon-icons-svelte/lib/ChevronUp.svelte'; import ChevronUpIcon from 'carbon-icons-svelte/lib/ChevronUp.svelte';
import IconButton from '$lib/components/IconButton/index.svelte'; import IconButton from '$lib/components/IconButton/index.svelte';
@@ -48,7 +48,7 @@
{/if} {/if}
</button> </button>
{#if isExpanded} {#if isExpanded}
<div class="flex flex-col"> <div in:slide out:slide class="flex flex-col">
<slot /> <slot />
</div> </div>
{/if} {/if}
@@ -249,10 +249,13 @@
{/if} {/if}
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
{#if lesson.video_url} {#if lesson.videos}
<span class="text-sm font-light flex w-2/4" <span class="text-sm font-light flex w-2/4"
><Video size={16} class="mr-1" />1 video</span ><Video size={16} class="mr-1" />{lesson.videos.length} video{lesson.videos
> .length > 1
? 's'
: ''}
</span>
{/if} {/if}
{#if get(lesson, 'totalExercises[0].count')} {#if get(lesson, 'totalExercises[0].count')}
<span class="text-sm font-light flex w-2/4" <span class="text-sm font-light flex w-2/4"
-58
View File
@@ -1,58 +0,0 @@
<script>
import Icon from 'svelte-awesome/components/Icon.svelte';
import { faTelegram } from '@fortawesome/free-brands-svg-icons';
import { faEnvelope } from '@fortawesome/free-regular-svg-icons';
export let avatarUrl =
'https://tailwindcss.com/_next/static/media/sarah-dayan.a8ff3f1095a58085a82e3bb6aab12eb2.jpg';
export let name = 'Sarah Dayan';
export let title = 'Staff Engineer, Algolia';
export let mailLink;
export let telegramLink;
</script>
<figure class="w-52 bg-gray-100 dark:bg-gray-700 px-4 py-7 mr-4 mb-4">
<img
class="w-32 h-32 rounded-full mx-auto"
src={avatarUrl}
alt=""
width="384"
height="512"
/>
<div class="pt-6 text-center space-y-4">
<div class="description font-medium flex flex-col justify-around">
<div class="text-cyan-600">{name}</div>
<div class="text-gray-500 dark:text-white">{title}</div>
</div>
</div>
{#if telegramLink && mailLink}
<div class="flex justify-center">
<a href={telegramLink} target="_blank">
<Icon
data={faTelegram}
scale="1"
class="social-icon"
style="color:#4dabf7;margin:0 5px;"
/>
</a>
<a href={mailLink} target="_blank">
<Icon
data={faEnvelope}
scale="1"
class="social-icon"
style="color:#d44c3d;margin:0 5px;"
/>
</a>
</div>
{/if}
</figure>
<style>
img {
width: 104px;
height: 104px;
}
.description {
min-height: 70px;
}
</style>
+1
View File
@@ -0,0 +1 @@
export default () => Date.now().toString(36) + Math.random().toString(36).substr(2);
+1 -1
View File
@@ -108,7 +108,7 @@ async function cloneLessons(lessons: Lesson[], courseId: Course['id']) {
public: lesson.public, public: lesson.public,
slide_url: lesson.slide_url, slide_url: lesson.slide_url,
title: lesson.title, title: lesson.title,
video_url: lesson.video_url videos: lesson.videos
})); }));
const { data } = await supabase.from('lesson').insert(clonedLessons).select(); const { data } = await supabase.from('lesson').insert(clonedLessons).select();
+2 -2
View File
@@ -34,7 +34,7 @@ export async function fetchCourse(courseId?: Course['id'], slug?: Course['slug']
certificate_theme, certificate_theme,
lessons:lesson( lessons:lesson(
id, title,public, lesson_at, call_url, is_unlocked, is_complete, order, created_at, id, title,public, lesson_at, call_url, is_unlocked, is_complete, order, created_at,
note, video_url, slide_url, call_url, totalExercises:exercise(count), note, videos, slide_url, call_url, totalExercises:exercise(count),
profile:teacher_id(id, avatar_url, fullname) profile:teacher_id(id, avatar_url, fullname)
), ),
attendance:group_attendance(*) attendance:group_attendance(*)
@@ -150,7 +150,7 @@ export function deleteGroupMember(groupMemberId: Groupmember['id']) {
export function fetchLesson(lessonId: Lesson['id']) { export function fetchLesson(lessonId: Lesson['id']) {
return supabase return supabase
.from('lesson') .from('lesson')
.select(`id, note, video_url, slide_url, call_url, totalExercises:exercise(count)`) .select(`id, note, videos, slide_url, call_url, totalExercises:exercise(count)`)
.eq('id', lessonId) .eq('id', lessonId)
.single(); .single();
} }
+21 -1
View File
@@ -136,9 +136,29 @@ export interface Group_attendance {
groupmember?: Groupmember; groupmember?: Groupmember;
} }
enum VideoType {
youtube,
muse
}
export interface LessonPage {
id?: string | null;
totalExercises: Number;
is_complete: Boolean;
materials: {
note: string;
slide_url: string;
videos: Array<{
type: string;
link: string;
metadata?: {};
}>;
};
exercises: [];
}
export interface Lesson { export interface Lesson {
note?: any; // type unknown; note?: any; // type unknown;
video_url?: any; // type unknown; videos?: []; // type unknown;
slide_url?: any; // type unknown; slide_url?: any; // type unknown;
course_id: string /* foreign key to course.id */; course_id: string /* foreign key to course.id */;
id?: string /* primary key */; id?: string /* primary key */;
+2 -2
View File
@@ -39,7 +39,7 @@
import '../app.postcss'; import '../app.postcss';
let supabase = getSupabase(); let supabase = getSupabase();
let path = $page.url.pathname.replace('/', ''); let path = $page.url?.pathname?.replace('/', '');
let theme = 'white'; let theme = 'white';
let skipAuth = false; let skipAuth = false;
@@ -261,7 +261,7 @@
}; };
}); });
$: path = $page.url.pathname.replace('/', ''); $: path = $page.url?.pathname?.replace('/', '');
$: theme = $appStore.isDark ? 'g100' : 'white'; $: theme = $appStore.isDark ? 'g100' : 'white';
$: setOrgBasedOnUrl($page.url.host); $: setOrgBasedOnUrl($page.url.host);
</script> </script>
+42
View File
@@ -0,0 +1,42 @@
import { json } from '@sveltejs/kit';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import {
CLOUDFLARE_ACCESS_KEY,
CLOUDFLARE_SECRET_ACCESS_KEY,
CLOUDFLARE_ACCOUNT_ID,
CLOUDFLARE_PUBLIC_ACCOUNT_ID,
MUSE_API_KEY
} from '$env/static/private';
const S3 = new S3Client({
region: 'auto',
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CLOUDFLARE_ACCESS_KEY,
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY
}
});
const downloadTranscript = async () => {};
const uploadTranscriptToS3 = async () => {};
export async function GET({ url }) {
const museFid = url.searchParams.get('museFid');
console.log('museFid');
// check the transcript table and see if we find a record
// if we find one
// check if downloaded = true
// if true
// return the transcript
// if false
// download the transcript from muse
// upload it to S3
// return the transcript
// if we dont
return json({ success: true, museFid });
}
+2 -1
View File
@@ -3,6 +3,7 @@
import CourseContainer from '$lib/components/CourseContainer/index.svelte'; import CourseContainer from '$lib/components/CourseContainer/index.svelte';
import { setCourse } from '$lib/components/Course/store'; import { setCourse } from '$lib/components/Course/store';
import { fetchCourse } from '$lib/utils/services/courses'; import { fetchCourse } from '$lib/utils/services/courses';
import { browser } from '$app/environment';
export let data; export let data;
const { courseId } = data; const { courseId } = data;
@@ -10,7 +11,7 @@
let prevCourseId = ''; let prevCourseId = '';
async function onCourseIdChange(courseId = '') { async function onCourseIdChange(courseId = '') {
if (!courseId || prevCourseId === courseId) return; if (!courseId || prevCourseId === courseId || !browser) return;
const { data: _data } = await fetchCourse(courseId); const { data: _data } = await fetchCourse(courseId);
@@ -1,13 +0,0 @@
export const load = ({ params = { id: '' } }) => {
const { id: courseId, lessonParams = [] } = params;
const splitparams = lessonParams.split('/');
const [lessonId, exerciseRouteName, exerciseId] = splitparams;
return {
courseId,
lessonId,
exerciseRouteName,
exerciseId,
isMaterialsTabActive: !exerciseRouteName
};
};
@@ -0,0 +1,157 @@
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import FormData from 'form-data';
import axios from 'axios';
import { fail } from '@sveltejs/kit';
import genUniqueId from '$lib/utils/functions/genUniqueId.js';
import { getSupabase } from '$lib/utils/functions/supabase';
import {
CLOUDFLARE_ACCESS_KEY,
CLOUDFLARE_SECRET_ACCESS_KEY,
CLOUDFLARE_ACCOUNT_ID,
CLOUDFLARE_PUBLIC_ACCOUNT_ID,
MUSE_API_KEY
} from '$env/static/private';
// Set the runtime to edge for best performance
export const config = {
runtime: 'edge'
};
const S3 = new S3Client({
region: 'auto',
endpoint: `https://${CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId: CLOUDFLARE_ACCESS_KEY,
secretAccessKey: CLOUDFLARE_SECRET_ACCESS_KEY
}
});
const supabase = getSupabase();
const uploadToMuse = (s3Data, fileName, lessonId, fileUrl) => {
const form = new FormData();
form.append('file', s3Data, fileName);
axios({
method: 'POST',
url: 'https://api.muse.ai/api/files/upload',
data: form,
maxContentLength: Infinity,
maxBodyLength: Infinity,
headers: {
...form.getHeaders(),
Key: MUSE_API_KEY
}
})
.then(async function (response) {
console.log('Upload to muse complete', response.data);
const { data: lesson } = await supabase
.from('lesson')
.select(`*`)
.eq('id', lessonId)
.single();
if (lesson) {
// Add Muse response to metadata
const videos = lesson.videos.map((v) => {
if (v.link === fileUrl) {
console.log('matches');
v.metadata = JSON.parse(JSON.stringify(response.data));
}
return v;
});
const { error, data } = await supabase
.from('lesson')
.update({ videos })
.eq('id', lessonId)
.select();
if (error) {
console.log('Error updating supabase configs');
}
console.log('update error', error);
console.log('update data', data, data[0].videos);
}
})
.catch(function (error) {
console.log('Upload error', error);
});
};
export const load = ({ params = { id: '' } }) => {
const { id: courseId, lessonParams = [] } = params;
const splitparams = lessonParams.split('/');
const [lessonId, exerciseRouteName, exerciseId] = splitparams;
return {
courseId,
lessonId,
exerciseRouteName,
exerciseId,
isMaterialsTabActive: !exerciseRouteName
};
};
export const actions = {
create: async ({ request }) => {
const formData = Object.fromEntries(await request.formData());
console.log('formData', formData);
if (!formData.file?.name || formData.file?.name === 'undefined') {
return fail(400, {
success: false,
message: 'You must provide a file to upload'
});
}
const { file, lessonId } = formData;
console.log('file', file);
console.log('lessonId', lessonId);
const stream = await file.arrayBuffer();
const fileName = `${genUniqueId()}-${file.name}`;
const params = {
Bucket: 'videos',
Key: fileName,
Body: stream,
ContentLength: stream.length
};
console.log('params', params);
let upload;
const fileUrl = `https://pub-${CLOUDFLARE_PUBLIC_ACCOUNT_ID}.r2.dev/${fileName}`;
try {
upload = await S3.send(new PutObjectCommand(params));
// process data.
} catch (error) {
// error handling.
const { requestId, cfId, extendedRequestId } = error.$$metadata;
console.error('Error uploading file');
console.log({ requestId, cfId, extendedRequestId });
} finally {
// finally.
console.log('File upload complete', upload);
console.log('\n\nInitilizing Muse Request');
const data = await S3.send(
new GetObjectCommand({
Bucket: 'videos',
Key: fileName
})
);
// console.log('GetObjectCommand', data);
uploadToMuse(data.Body, file.name, lessonId, fileUrl);
}
return {
success: true,
url: fileUrl,
message: 'Uploaded successfully'
};
}
};
@@ -12,11 +12,17 @@
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte'; import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import MODES from '$lib/utils/constants/mode.js'; import MODES from '$lib/utils/constants/mode.js';
import { setCourse, course } from '$lib/components/Course/store'; import { setCourse, course } from '$lib/components/Course/store';
import { lesson, handleSaveLesson } from '$lib/components/Course/components/Lesson/store/lessons'; import {
lesson,
handleSaveLesson,
uploadCourseVideoStore
} from '$lib/components/Course/components/Lesson/store/lessons';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
export let data; export let data;
export let form;
let path = ''; let path = '';
let mode = MODES.view; let mode = MODES.view;
let prevMode = ''; let prevMode = '';
@@ -68,12 +74,14 @@
mode = mode === MODES.edit ? MODES.view : MODES.edit; mode = mode === MODES.edit ? MODES.view : MODES.edit;
} }
$: path = $page.url.pathname.replace(/\/exercises[\/ 0-9 a-z -]*/, ''); $: path = $page.url?.pathname?.replace(/\/exercises[\/ 0-9 a-z -]*/, '');
$: if (data.courseId && browser) { $: if (data.courseId && browser) {
mode = MODES.view; mode = MODES.view;
fetchReqData(data.lessonId); fetchReqData(data.lessonId);
} }
$: $uploadCourseVideoStore.formRes = form;
</script> </script>
<CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}> <CourseContainer {isFetching} {path} isExercisePage={!data.isMaterialsTabActive && data.exerciseId}>
@@ -293,7 +293,7 @@
</a> </a>
<a <a
class="flex items-center no-underline hover:underline text-black my-2" class="flex items-center no-underline hover:underline text-black my-2"
href="{$page.url.pathname.replace('submissions', 'lessons')}/{item.lesson href="{$page.url?.pathname?.replace('submissions', 'lessons')}/{item.lesson
.id}/exercises/{item.exercise.id}" .id}/exercises/{item.exercise.id}"
> >
<p class="dark:text-white text-grey text-sm"> <p class="dark:text-white text-grey text-sm">