CLASSROOMIO-T-95: Upload to S3 and Muse AI
This commit is contained in:
+3
-2
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default () => Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 */;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user