This content originally appeared on DEV Community and was authored by Hama
sggs
<!-- Header -->
:type="learnModule?.status === 'draft' ? 'draft' : 'edit'"
saveMode="auto"
:menu-items="menuItems"
:publishText="$t('Publish')"
@preview-module="modalPreviewModule.modalSkeleton.dialog = true"
@toggle-ai="openAiSection = !openAiSection"
@publish-module="openPublishModuleDialog"
@close-and-go-back="handleClose"
/>
<modal-preview-learn-module
v-if="learnModule"
ref="modalPreviewModule"
:title="title"
:cover="learnModule?.cover_url?.['500']"
:themes="learnModule?.themes"
:duration="learnModule?.duration"
:editor-data="editorContentData"
:reactions="learnModule?.user_reactions"
:has-learn-pieces-quiz="learnModule?.has_learn_pieces_quiz"
:inputs="inputs"
:learn-module="learnModule"
>
<template #button>
<div class="hidden opacity-0" />
</template>
</modal-preview-learn-module>
<div class="px-5 pt-4 w-full flex flex-col gap-6 mx-auto md:!px-0 md:!max-w-[752px] lg:!max-w-[752px] xl:!max-w-[752px]">
<div class="flex flex-col gap-1">
<svn-pro-text body-medium regular color="onSurfaceVariant">
{{ $t('Training banner (Accepts only .jpg, .png and .jpeg file formats)') }}
</svn-pro-text>
<bkt-image-cover-position
:mode="mode"
:url="learnModule?.cover_url ? learnModule?.cover_url?.['1000'] : null"
v-model:coordinates="coordinates"
@update:coordinates="updateModule({id: route?.params?.id, cover_offset_left: $event.left, cover_offset_top: $event.top})"
@file-upload="uploadImage"
@error="errorUploading"
@change-mode="mode = $event"
/>
</div>
<div class="w-full flex flex-col gap-6 ">
<div id="module-title">
<!-- Training title -->
<svn-pro-text-field
:label="$t('Module title*')"
:error="warningTitle && !title"
v-model="title"
:max-length="70"
counter
class="w-full"
@input="updateLearnModuleTitle"
/>
</div>
<div class="flex justify-start gap-4 flex-col">
<div class="flex gap-1 flex-col">
<svn-pro-title h6 medium>
{{ $t('Estimated duration') }}
</svn-pro-title>
<svn-pro-text subtitle-medium regular>
{{ $t('It’s required to acquire this module.') }}
</svn-pro-text>
</div>
<div class="flex gap-2 w-full">
<!-- Time -->
<div class="w-[140px] flex-items-center">
<svn-pro-text-field
v-model="time"
:label="$t('Value')"
type="number"
min="1"
@input="updateLearnModuleDurationTime"
/>
</div>
<!-- Time unit -->
<div class="w-[220px]">
<svn-pro-select
v-model="timeUnit"
:label="$t('Unity')"
item-title="text"
item-value="value"
:items="timeUnits"
@update:model-value="updateLearnModuleDurationTimeUnit"
/>
</div>
</div>
</div>
<svn-pro-divider color="[#767680]" class="border-opacity-100" />
<div class="flex justify-start gap-4 flex-col">
<div class="flex gap-1 flex-col">
<svn-pro-title h6 medium>
{{ $t('Themes') }}
</svn-pro-title>
<svn-pro-text subtitle-medium regular>
{{ $t('You can describe your module using themes.') }}
</svn-pro-text>
</div>
<!-- Time unit -->
<div v-if="learnThemes?.length" class="min-w-[120px] max-w-[520px]">
<svn-pro-combobox
v-model="entityThemes"
:items="learnThemes"
:key="generation"
item-title="name"
:label="$t('Themes')"
:clearable="false"
multiple
return-object
@update:search="searchThemeText = $event"
@update:model-value="updateOrCreateTheme"
/>
</div>
<!-- Input to add a new theme -->
<div
v-if="entityThemes?.length"
class="flex flex-wrap gap-2"
>
<svn-pro-chip
v-for="theme in entityThemes"
key="entityTag.id"
class=""
:text="theme.name"
is-slot-append="true"
>
<template #append>
<Icon
icon="mingcute:close-line"
width="18"
height="18"
class="ml-2 cursor-pointer"
@click="deleteTheme(theme)"
/>
</template>
</svn-pro-chip>
</div>
</div>
<svn-pro-divider color="[#767680]" class="border-opacity-100" />
<div class="flex justify-start gap-4 flex-col">
<svn-pro-title h6 medium>
{{ $t('Content') }}
</svn-pro-title>
<!-- Rich text editor -->
<svn-tiptap
v-if="editorContentData?.blocks?.length"
:create-image-url="`/api/v1/editor_contents/${editorContentId}/upload_image`"
:html-data="editorContentData?.blocks"
:extension-selection="AllTipTapPlugins"
:extension-slash-command="AllTipTapPlugins"
:extension-left-menu="true"
@on-save="debounceEditorContentUpdate"
/>
</div>
<svn-pro-divider color="[#767680]" class="border-opacity-100" />
<div id="module-evaluation" class="flex justify-start gap-4 flex-col" >
<svn-pro-title h6 medium>
{{ $t('Evaluation') }}
</svn-pro-title>
<svn-pro-card
:class="learnModule.piece_id ? '' : warningPieceClass"
:elevation="learnModule.piece_id ? 2 : 0"
:link="false"
class="flex justify-start p-6 gap-8 flex-col"
>
<div class="flex flex-col gap-3">
<svn-pro-title h6 medium>
{{ $t('Evaluation type') }}
</svn-pro-title>
<v-item-group
v-model="learnModule.submission_type"
mandatory
@update:model-value="changeEvaluationType"
>
<div class="flex flex-col gap-4 lg:grid lg:grid-cols-2">
<svn-pro-extended-radio-button
v-for="evaluationItem in evaluationItems"
:value="evaluationItem?.type"
:icon="evaluationItem?.icon"
:title="evaluationItem?.title"
:subtitle="evaluationItem?.subtitle"
/>
</div>
</v-item-group>
</div>
<!-- Description -->
<svn-pro-text-area
id="module-approval-description"
v-if="learnModule?.has_learn_pieces_approval"
:label="$t('Learning objectives*')"
v-model="learnApprovalInput.text"
:error="warningAprovalDescription && !learnApprovalInput.text"
:rows="6"
:max-rows="6"
class="w-full"
@update:model-value="updateApprovalInputText"
:errorMessages="warningAprovalDescription && !learnApprovalInput.text ? 'Required*' : ''"
/>
<!-- Evaluation type is Face to face -->
<div
v-if="learnModule?.learn_pieces_face_to_face_evaluation?.id && inputsFaceToFace?.length"
v-for="question in inputsFaceToFace"
:key="question.id"
class="flex flex-col gap-8"
>
<template-header-question
v-if="question.type == 'Learn::Inputs::OpenQuestion'"
@delete="removeOpenQuestion(question)"
@move-up="getListAfterDragFaceToFace(question, 'moveUp')"
@move-down="getListAfterDragFaceToFace(question, 'moveDown')"
@copy-bkt="duplicateOpenQuestion(question)"
width="100%"
:isDeleteDisabled="inputsFaceToFace?.length <= 1"
>
<template #body>
<learn-edit-open-question-block
v-if="question.type === LearnInputType.OPEN"
:question="question"
:can-remove-question="inputsFaceToFace.filter(input =>
input.type === LearnInputType.OPEN)?.length > 1"
@update-question="inputChannel?.update"
@duplicate-question="duplicateOpenQuestion(question)"
@remove-question="removeOpenQuestion(question)"
/>
</template>
</template-header-question>
<learn-add-question-block
@click="addFaceToFaceOpenQuestionBlock((question?.position || 0) + 1)"
face-to-face
/>
</div>
<learn-add-question-block
v-if="learnModule?.learn_pieces_face_to_face_evaluation?.id && !inputsFaceToFace?.length"
@click="addFaceToFaceOpenQuestionBlock(1)"
face-to-face
/>
<!-- Evaluation type is Quiz -->
<div
v-if="learnModule?.learn_pieces_quiz?.id && inputs?.length"
v-for="question in inputs"
:key="question.id"
class="flex flex-col gap-8"
>
<template-header-question
v-if="question.type === LearnInputType.CHECKBOX"
@delete="removeInput(question)"
@move-up="getListAfterDrag(question, 'moveUp')"
@move-down="getListAfterDrag(question, 'moveDown')"
@copy-bkt="duplicateOption(question)"
:isDeleteDisabled="inputs?.length <= 1"
width=""
>
<template #body>
<learn-edit-question-block
:input="question"
:can-remove-input="inputs.filter(input => input.type === LearnInputType.CHECKBOX).length > 1"
@update-input="inputChannel?.update"
@remove-option="removeOption($event)"
@add-option="addOption($event)"
@remove-input="removeInput(question)"
@duplicate-option="duplicateOption(question)"
/>
</template>
</template-header-question>
<learn-add-question-block
@click="addQuizQuestionBlock((question?.position || 0) + 1)"
face-to-face
/>
</div>
<learn-add-question-block
v-if="learnModule?.learn_pieces_quiz?.id && !inputs?.length"
@click="addQuizQuestionBlock((question?.position || 0) + 1)"
face-to-face
/>
</svn-pro-card>
</div>
</div>
</div>
<!-- Drawer Large -->
<div
class="h-full overflow-y-auto flex flex-col transition-all relative"
:class="drawerLarge ? 'w-[344px]' : 'w-0'"
>
<a-i-drawer
:is-mobile="false"
:drawer-large="openAiSection"
:response="response"
@ask-a-i-question="askAIQuestion"
@close-drawer="openAiSection = false"
/>
</div>
<!-- Scroll to top button -->
to-top
size="small"
color="primary"
variant="tonal"
:rounded="'lg'"
class="fixed bottom-4 right-4 bg-white"
icon="custom:mingcute:arrow-to-up-line"
/>
<!-- Dialog edit Playlist -->
v-if="learnModule?.learn_trainings?.length"
ref="deleteModuleDialog"
:items="learnModule?.learn_trainings"
:title="$t(`Module will be deleted with their trainings`)"
:description="$t(`If this module is the only content of a training, the training will be deleted. Training(s) containing only this module :`)"
@delete-content="deleteLearnModule"
/>
<svn-pro-dialog-validation
v-else-if="learnModule?.status === 'draft'"
ref="deleteModuleDialog"
icon="noto:warning"F
:action-two-title="$t('Cancel')"
:action-one-title="$t('Delete')"
:title="$t(Module will be deleted
)"
:content-text="$t('This is a permanent action.')"
@click-primary-button="deleteLearnModule"
<template #activator="{ props }"> <div class="hidden"/> </template>
<svn-pro-dialog-validation
v-else
ref="deleteModuleDialog"
icon="noto:warning"F
:action-two-title="$t('Cancel')"
:action-one-title="$t('Delete')"
:title="$t(Module will be deleted
)"
:content-text="$t('Deleted modules are stored for 30 days. After this period, they will be permanently deleted.')"
@click-primary-button="deleteLearnModule"
<template #activator="{ props }"> <div class="hidden"/> </template>
<svn-pro-dialog-validation
ref="publishModuleDialog"
:action-two-title="$t('Cancel')"
:action-one-title="$t('Publish')"
:title="$t(Module will be published
)"
:content-text="$t('Your module will be added to the Catalog and will be visible to everyone.')"
@click-primary-button="publishLearnModule"
:width="412"
<template #activator="{ props }"> <div class="hidden"/> </template>
<!-- Dialog duplicate module -->
ref="duplicateModuleDialog"
:moduleTitle="learnModule.title"
@duplicate-module="duplicateLearnModule"
/>
import {Icon} from "@iconify/vue";
import {onMounted, ref, computed, onBeforeUnmount} from "vue";
import ModalPreviewLearnModule from "@/components/BktPopUp/Modals/learn/ModalPreviewLearnModule.vue";
import LearnAddQuestionBlock from "@/components/learnApp/moduleBlock/createBlock/LearnAddQuestionBlock.vue";
import LearnEditQuestionBlock from "@/components/learnApp/moduleBlock/editBlock/LearnEditQuestionBlock.vue";
import { useLearnModuleStore } from "@/store/learn-module";
import DialogDuplicateModule from '@/components/BktPopUp/Dialogs/learn/DialogDuplicateModule.vue';
import { useLearnThemesStore } from "@/store/learn-themes";
import { useMobileStore } from "@/store/mobile";
import { storeToRefs } from "pinia";
import { useSnackbar } from "@/store/snackbar";
import { useRouter, useRoute } from "vue-router";
import { debounce } from "lodash";
import { useActionCable } from "@/store/cable.js";
import { useLearnTrainingStore } from "@/store/learn-trainings.js";
import { useToastStore } from "@/store/toast.js";
import { useUserStore } from "@/store/user.js";
import i18n from "@/plugins/i18n.js";
import BktImageCoverPosition from "@/components/image/bkt-image-cover-position.vue";
import LearnEditOpenQuestionBlock from "../../../../../components/learnApp/moduleBlock/editBlock/LearnEditOpenQuestionBlock.vue";
import { LearnInputType } from '@/constants/types';
import { AllTipTapPlugins } from 'svn-ui-library/extensions';
import TemplateHeaderQuestion from "@/components/interviewApp/template/Edit/TemplateHeaderQuestion.vue";
import DialogDeleteContent from '@/components/BktPopUp/Dialogs/learn/DialogDeleteContent.vue';
import AIDrawer from '@/components/trainingApp/AIDrawer.vue';
const { addToast } = useToastStore();
const { id: userId } = storeToRefs(useUserStore())
const {
updateModule, publishModule, duplicateModule, addThemeToModule,
removeThemeFromModule,
fetchModule,
deleteModule,
deleteModulePermanently,
fetchInputs,
fetchInputsFaceToFace,
updateInputs,
updateInputsFaceToFace,
postInputs,
postInputsFaceToFace,
postParagraph,
postParagraphFaceToFace,
deleteInputQuestion,
deleteOpenQuestion,
getEditorContent,
updateModuleImage,
resetStates,
changeSubmissionPieceType,
updateApprovalInput
} = useLearnModuleStore()
const {
learnModule,
inputs,
inputsFaceToFace,
editorContentId,
editorContentData,
learnApprovalInput
} = storeToRefs(useLearnModuleStore());
const {cable} = storeToRefs(useActionCable());
const learnThemesStore = useLearnThemesStore()
const { fetchThemes } = learnThemesStore
const { learnThemes } = storeToRefs(learnThemesStore)
const learnTrainingStore = useLearnTrainingStore()
const { learnTraining } = storeToRefs(learnTrainingStore)
const { updateModuleAndPlaylistData, fetchTraining } = learnTrainingStore
const mobileStore = useMobileStore()
const { isMobile } = storeToRefs(mobileStore)
onMounted(async () => {
await resetStates()
try {
await fetchModule(route?.params?.id).then(() => {
entityThemes.value = learnModule.value?.themes
title.value = learnModule.value?.title
if (learnModule.value?.duration) {
time.value = learnModule.value?.duration?.split(' ')?.[0]
timeUnit.value = learnModule.value?.duration?.split(' ')?.[1] === 'h' ? 'hours' : 'minutes'
}
}).then(() => {
updateEditorContent()
})
if (learnModule?.value?.editor_content_id) {
await getEditorContent(learnModule.value.editor_content_id);
}
if (learnModule?.value?.has_learn_pieces_quiz) {
await fetchInputs(learnModule.value?.learn_pieces_quiz?.id);
}
if (learnModule?.value?.has_learn_pieces_face_to_face_evaluation) {
await fetchInputsFaceToFace(learnModule.value?.learn_pieces_face_to_face_evaluation?.id);
}
updateInputQuestion();
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error fetching module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
try {
await fetchThemes()
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error fetching themes')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
})
const title = ref()
const typeKeyes = ref({
'Learn::Pieces::Approval': 'Approval',
'Learn::Pieces::Quiz': 'Quiz',
'Learn::Pieces::FaceToFaceEvaluation': 'FaceToFaceEvaluation',
})
const evaluationItems = ref([
{
key: 'Approval',
type: 'Learn::Pieces::Approval',
icon: 'noto:check-mark-button',
title: 'Self-assessment',
subtitle: 'The learner must confirm they have fully understood the module before proceeding.',
},
{
key: 'Quiz',
type: 'Learn::Pieces::Quiz',
icon: 'noto:red-question-mark',
title: 'Quiz',
subtitle: 'The learner must pass the quiz with a score of 100% correct answers to successfully complete this section.',
},
{
key: 'FaceToFaceEvaluation',
type: 'Learn::Pieces::FaceToFaceEvaluation',
icon: 'noto:busts-in-silhouette',
title: 'Face-to-face evaluation',
subtitle: "The learner's answers will be evaluated in real time by an expert, ensuring accurate assessment and personalized feedback.",
},
]);
const cannotBePublished = computed(() => {
return (
!title.value ||
!time.value
);
});
const time = ref()
const isDragged = ref(false)
const timeUnit = ref('minutes')
const timeUnits = ref([
'minutes',
'hours',
])
const variant = ref('plain')
const generation = ref(0);
const openAiSection = ref(false);
const mode = ref('edit')
const newTheme = ref(false)
const entityThemes = ref([])
const searchThemeText = ref('')
const warningPieceClass = ref('')
const warningTitle = ref(false)
const warningAprovalDescription = ref(false)
const drag = ref(false)
const snackbar = useSnackbar()
const router = useRouter()
const route = useRoute()
const inputChannel = ref(null)
const editorContentChannel = ref(null)
const duplicateModuleDialog = ref(false)
const deleteModuleDialog = ref(false)
const publishModuleDialog = ref(false)
const menuItems = ref([
{
title: i18n.global.t('Duplicate module'),
value: 'duplicate_module',
onClick: () => openDialogDuplicateModule(learnModule?.value?.id),
},
{
title: i18n.global.t('Delete module'),
value: 'delete_module',
error: true,
onClick: () => openDialogDeleteModude(learnModule?.value?.id),
},
]);
const modalPreviewModule = ref(false);
const coordinates = ref({
left: learnModule?.cover_offset_left,
top: learnModule?.cover_offset_top,
});
const toggleDragState = (even) => {
isDragged.value = even
}
const updateApprovalInputText = debounce(async(even) => {
await updateApprovalInput({text: even})
}, 300)
const addTheme = async () => {
if (searchThemeText.value) {
try {
await addThemeToModule(learnModule?.value?.id, searchThemeText.value)
snackbar.setBgColor('onSurface')
snackbar.setMsg('Theme added to module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
entityThemes.value = learnModule.value.themes
generation.value = generation.value + 1
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error adding theme to module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
} finally {
searchThemeText.value = ''
}
}
}
const deleteTheme = async (theme) => {
try {
await removeThemeFromModule(learnModule?.value?.id, theme?.name);
snackbar.setBgColor('onSurface');
snackbar.setMsg('Theme removed from module');
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]');
snackbar.displaySnackBar();
entityThemes.value = learnModule.value.themes;
generation.value = generation.value + 1
} catch (error) {
snackbar.setBgColor('error');
snackbar.setMsg('Error removing theme from module');
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]');
snackbar.displaySnackBar();
}
}
const updateLearnModuleThemes = async () => {
try {
const theme = findDifferentValues(entityThemes.value, learnModule.value.themes)
if (theme) {
if (entityThemes.value.length > learnModule.value.themes.length) {
await addThemeToModule(learnModule?.value?.id, theme)
entityThemes.value = learnModule.value.themes
generation.value = generation.value + 1
snackbar.setBgColor('onSurface')
snackbar.setMsg('Theme added to module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
}
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error updating')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
}
const updateOrCreateTheme = async (themes) => {
let themeToAdd = themes.find(theme => typeof theme === 'string')
if (themeToAdd) {
await addTheme(themeToAdd)
} else {
updateLearnModuleThemes()
}
};
const clearAll = () => {
return null
}
const findDifferentValues = (array1, array2) => {
const entityThemeNames = array1.map(theme => theme.name);
const learnModuleThemeNames = array2.map(theme => theme.name);
// Find the different values
const differentValues = entityThemeNames.filter(name => !learnModuleThemeNames.includes(name));
return differentValues[differentValues.length - 1];
}
const publishLearnModule = async () => {
try {
await publishModule(learnModule?.value?.id)
snackbar.setBgColor('onSurface')
snackbar.setMsg('Module published!')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-10')
snackbar.displaySnackBar()
if (route.query?.training_id && route.query?.training_id != 'NaN') {
await fetchTraining(route.query?.training_id)
const list = learnTraining.value?.learn_contents
list.push({
contentable_id: learnModule?.value?.id,
contentable_type: "Learn::Module"
})
await updateModuleAndPlaylistData(list)
router.push({name: "training_edit", params: {id: learnTraining.value.id}})
} else {
router.push({name: "module_show", params: {id: learnModule?.value?.id}})
}
} catch (error) {
snackbarMethod('Error publishing module!')
}
}
const openDialogDuplicateModule = () => {
duplicateModuleDialog.value.dialogDuplicateModule = true
}
const openDialogDeleteModude = () => {
if (learnModule?.value?.learn_trainings?.length) {
deleteModuleDialog.value.deleteDialog = true
} else {
deleteModuleDialog.value.dialogRef.dialog = true
}
}
const openPublishModuleDialog = () => {
let canPublish = true
if (!learnModule.value.piece_id) {
canPublish = false
warningPieceClass.value = "!border !border-[#BA1A1A]/100"
setTimeout(() => {
document.getElementById('module-evaluation').scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100)
snackbarMethod('Evaluation type must be set.')
}
if (!title.value) {
canPublish = false
warningTitle.value = true
setTimeout(() => {
document.getElementById('module-title').scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100)
snackbarMethod('Please fill required fields (*).')
}
if (learnModule.value?.has_learn_pieces_approval && !learnApprovalInput.value.text) {
canPublish = false
warningAprovalDescription.value = true
setTimeout(() => {
document.getElementById('module-approval-description').scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100)
snackbarMethod('Please fill required fields (*).')
}
if(!canPublish) return
publishModuleDialog.value.dialogRef.dialog = true
}
const changeVariant = () => {
if (variant.value === 'plain') {
variant.value = 'outlined'
} else {
variant.value = 'plain'
}
}
const updateLearnModuleTitle = debounce(async () => {
try {
await updateModule({
id: learnModule?.value?.id,
title: title.value
})
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error updating module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
}, 300)
const updateLearnModuleDurationTime = debounce(async () => {
try {
await updateModule({
id: learnModule?.value?.id,
title: title.value,
duration: (time.value + ' ' + timeUnit.value)
})
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error updating module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
}, 300)
const updateLearnModuleDurationTimeUnit = debounce(async () => {
try {
await updateModule({
id: learnModule?.value?.id,
title: title?.value,
duration: ((time?.value !== learnModule?.value?.duration?.split(' ')?.[0] ?
time.value : learnModule?.value?.duration?.split(' ')?.[0]) + ' ' + timeUnit?.value)
})
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error updating module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
}, 300)
const uploadImage = async (blob) => {
try {
await updateModuleImage(route?.params?.id, blob)
snackbar.setBgColor('onSurface')
snackbar.setMsg('Module image changed successfully')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error uploading module image')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
}
const snackbarMethod = async (text) => {
snackbar.setBgColor('onSurface')
snackbar.setMsg(text)
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-10')
snackbar.displaySnackBar()
}
const addQuizQuestionBlock = debounce(async(position) => {
const data = {
proposals: [{proposal: "", correct: false}],
title: null,
type: LearnInputType.CHECKBOX,
position: position
}
if (learnModule?.value?.piece_id) {
try {
await postInputs(learnModule.value?.piece_id, data);
} catch (error) {
console.log(error) ;
}
}
}, 200);
const addFaceToFaceOpenQuestionBlock = debounce(async(position) => {
const data = {
title: null,
description: null,
position: position
}
if (learnModule?.value?.piece_id) {
try {
await postInputsFaceToFace(learnModule?.value?.piece_id, data)
} catch (error) {
console.log(error);
}
}
}, 200);
const addQuizFreeContentBlock = debounce(async(faceToFace = false) => {
const data = {
title: '',
type: LearnInputType.PARAGRAPH,
}
if (learnModule?.value?.learn_pieces_quiz?.id) {
try {
await postParagraph(learnModule?.value?.learn_pieces_quiz?.id, data)
} catch (error) {
console.log(error);
}
}
if (learnModule?.value?.learn_pieces_face_to_face_evaluation?.id) {
try {
await postParagraphFaceToFace(learnModule?.value?.learn_pieces_face_to_face_evaluation?.id, data);
} catch (error) {
console.log(error);
}
}
}, 200)
const duplicateLearnModule = async (title) => {
try {
const duplicated = await duplicateModule(learnModule?.value?.id, title)
snackbar.setBgColor('onSurface')
snackbar.setMsg('Module duplicated.')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[42px]')
snackbar.displaySnackBar()
duplicateModuleDialog.value.dialogDuplicateModule = false
setTimeout(() => {
router.push({name: 'module_show', params: {id: duplicated.id}})
}, 200);
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error duplicating module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[42px]')
snackbar.displaySnackBar()
}
}
const deleteLearnModule = async () => {
try {
if (learnModule?.value.status === 'draft') {
await deleteModulePermanently(learnModule?.value?.id)
} else {
await deleteModule(learnModule?.value?.id)
}
snackbar.setBgColor('onSurface')
snackbar.setMsg('Module has been deleted successfully.')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[42px]')
snackbar.displaySnackBar()
deleteModuleDialog.value.deleteDialog = false
setTimeout(() => {
router.push({name: 'catalog'})
}, 200);
} catch (error) {
snackbar.setBgColor('error')
snackbar.setMsg('Error deleting module')
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[42px]')
snackbar.displaySnackBar()
}
}
const getListAfterDrag = async (question, action) => {
let position
if (action === 'moveUp') {
position = question.position - 1
} else if (action === 'moveDown') {
position = question.position + 1
}
await updateInputs(learnModule.value?.learn_pieces_quiz?.id, question.id, { position: position })
await fetchInputs(learnModule?.value?.piece_id)
};
const getListAfterDragFaceToFace = async (question, action) => {
let position
if (action === 'moveUp') {
position = question.position - 1
} else if (action === 'moveDown') {
position = question.position + 1
}
await updateInputsFaceToFace(learnModule.value?.learn_pieces_face_to_face_evaluation?.id, question.id, { position: position })
await fetchInputsFaceToFace(learnModule?.value?.piece_id)
};
const updateInputQuestion = debounce(async () => {
const subscribeOptions =
{
channel: "Learn::InputChannel",
piece_id: learnModule?.value?.submission_type === 'Learn::Pieces::Quiz' ?
learnModule.value?.learn_pieces_quiz?.id : learnModule?.value?.learn_pieces_face_to_face_evaluation?.id
}
inputChannel.value = cable.value.subscriptions.create(subscribeOptions, {
connected: function () {
// Called when the subscription is ready for use on the server
},
disconnected: function () {
// Called when the subscription has been terminated by the server
},
received: function (data) {
if (data.status === "update") {
switch (data.entity.type) {
case LearnInputType.CHECKBOX:
const index = inputs.value.findIndex(x => x.id === data.entity.id);
inputs.value[index].title = data.entity.title
inputs.value[index].proposals = data.entity.proposals
inputs.value[index].position = data.entity.position
break;
case LearnInputType.OPEN:
const indexFaceToFace = inputsFaceToFace.value.findIndex(x => x.id === data.entity.id);
inputsFaceToFace.value[indexFaceToFace].title = data.entity.title
inputsFaceToFace.value[indexFaceToFace].description = data.entity.description
inputsFaceToFace.value[indexFaceToFace].position = data.entity.position
break;
}
}
},
update: async function (event) {
let data = {};
switch (event?.type) {
case LearnInputType.PARAGRAPH:
data = {}
break;
case LearnInputType.CHECKBOX:
data = {
input_id: event?.id,
title: event?.title,
proposals: event?.proposals,
}
break;
case LearnInputType.OPEN:
data = {
input_id: event?.id,
title: event?.title,
description: event?.description,
}
break;
}
inputChannel.value.perform('update', data);
},
});
}, 300)
const updateEditorContent = debounce(async () => {
const subscribeOptions =
{
channel: "EditorContentChannel", id: learnModule.value.editor_content_id
}
editorContentChannel.value = cable.value.subscriptions.create(subscribeOptions, {
connected: function () {
// Called when the subscription is ready for use on the server
},
disconnected: function () {
// Called when the subscription has been terminated by the server
},
received: function (data) {
if (data.status === "update" && data.current_user.id !== userId.value && learnModule.value.editor_content_id === data.editor_content_id) {
addToast(
'info',
i18n.global.t(`This module has just been updated !`),
i18n.global.t(`The lastest version of this content will be visible if you reload this page.`),
false,
{
name: i18n.global.t(`Reload this page`),
link: '/learns/module/' + learnModule.value.id + '/edit'
}
)
} else {
editorContentData.value.blocks = data.entity.blocks
}
},
update: async function (event) {
const data = {
blocks: event
}
editorContentChannel.value.perform('update', { data });
},
});
}, 100)
const debounceEditorContentUpdate = debounce(e => editorContentChannel?.value?.update(e), 300)
const removeOption = debounce(async (data) => {
data.input.proposals.splice(data.index, 1);
inputChannel.value?.update(data.input);
}, 200);
const removeInput = debounce(async (input) => {
try {
await deleteInputQuestion(learnModule?.value?.learn_pieces_quiz?.id, input.id)
} catch (error) {
console.log(error)
}
}, 200);
const removeFreeContentFaceToFace = debounce(async (input) => {
try {
await deleteOpenQuestion(learnModule?.value?.learn_pieces_face_to_face_evaluation?.id, input.id)
} catch (error) {
console.log(error)
}
}, 200);
const removeOpenQuestion = debounce(async (input) => {
try {
await deleteOpenQuestion(learnModule?.value?.learn_pieces_face_to_face_evaluation?.id, input?.id)
} catch (error) {
console.log(error)
}
}, 200)
const duplicateOption = debounce(async (input) => {
try {
await postInputs(learnModule?.value?.learn_pieces_quiz?.id, input)
} catch (error) {
console.log(error)
}
}, 200);
const duplicateOpenQuestion = debounce(async (input) => {
try {
await postInputsFaceToFace(learnModule?.value?.learn_pieces_face_to_face_evaluation?.id, input)
} catch (error) {
console.log(error)
}
}, 200);
const addOption = debounce((input) => {
input.proposals.push({
correct: false,
proposal: ""
})
inputChannel.value?.update(input)
}, 200);
const changeEvaluationType = debounce(async(submissionType) => {
try {
const type = typeKeyes.value[submissionType]
await changeSubmissionPieceType(type);
const hasCheckboxQuestion = learnModule.value.learn_pieces_quiz?.has_custom_inputs
const hasOpenQuestion = learnModule.value.learn_pieces_face_to_face_evaluation?.has_custom_inputs
if (type === 'Quiz') {
inputsFaceToFace.value = []
if (hasCheckboxQuestion) {
await fetchInputs(learnModule?.value?.piece_id)
} else {
addQuizQuestionBlock();
}
} else if (type === 'FaceToFaceEvaluation') {
inputs.value = []
if (hasOpenQuestion) {
await fetchInputsFaceToFace(learnModule?.value?.piece_id)
} else {
addFaceToFaceOpenQuestionBlock();
}
}
} catch (error) {
console.log(error)
}
updateInputQuestion();
}, 200);
onBeforeUnmount(() => {
inputChannel?.value?.unsubscribe();
editorContentChannel?.value?.unsubscribe();
})
const handleClose = () => {
router.back();
// if (learnModule.value?.status === 'draft') {
// snackbar.setBgColor('onSurface')
// snackbar.setMsg('Your module has been saved in your drafts.')
// snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[42px]')
// snackbar.displaySnackBar()
// }
}
const errorUploading = (msg) => {
snackbar.setBgColor('error')
snackbar.setMsg(msg)
snackbar.setCustomClass(isMobile.value ? 'mb-[80px]' : 'mb-[25px]')
snackbar.displaySnackBar()
}
const response = ref('');
const loading = ref(false);
const askAIQuestion = async (text) => {
if (!text) {
alert('Please enter a text!');
return;
}
loading.value = true; // Show loading state
response.value = ''; // Clear previous responses
try {
const authHeaders = JSON.parse(window.localStorage.getItem('bktAccess'))
const url = /api/v1/learn/modules/${learnModule.value.id}/ais
; // Replace with your POST endpoint
// Using fetch for streaming
const res = await fetch(url, {
method: 'POST', // Use POST method
headers: {
'access-token': authHeaders['access-token'],
'token-type': authHeaders['token-type'],
'client': authHeaders['client'],
'expiry': authHeaders['expiry'],
'uid': authHeaders['uid'],
'Content-Type': 'application/json', // Sending JSON data
},
body: JSON.stringify({ title: text }) // Send text as part of the request body
});
const reader = res.body.getReader();
const decoder = new TextDecoder('utf-8');
let done = false;
while (!done) {
const { value, done: readerDone } = await reader.read();
done = readerDone;
if (value) {
const chunk = decoder.decode(value, { stream: !readerDone });
response.value += chunk; // Append the new token to response
}
}
} catch (error) {
console.error('Error fetching data:', error);
} finally {
loading.value = false; // Hide loading state
}
};
This content originally appeared on DEV Community and was authored by Hama
Hama | Sciencx (2024-10-19T12:25:51+00:00) sqdsqd. Retrieved from https://www.scien.cx/2024/10/19/sqdsqd/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.