diff --git a/assets/vue/App.vue b/assets/vue/App.vue index b548bc84dd2..ebc37837a38 100644 --- a/assets/vue/App.vue +++ b/assets/vue/App.vue @@ -225,6 +225,43 @@ const isEmbeddedContext = computed(() => { return isPickerContext.value || isIframeContext.value || isDialogContext.value }) +const isTruthyQueryValue = (value) => { + return ["1", "true", "yes", "on"].includes(String(value || "").toLowerCase()) +} + +const isLearnpathEmbeddedRoute = computed(() => { + const qp = queryParams.value + const origin = String(qp.get("origin") || "").toLowerCase() + const lpAction = String(qp.get("action") || "").toLowerCase() + const hasLpId = qp.has("lp_id") + + // LP player/runtime screens are rendered inside the learning path player. + // They must use EmptyLayout to avoid duplicated Chamilo header/sidebar. + if ( + hasLpId && + ("view" === lpAction || isTruthyQueryValue(qp.get("embedded")) || isTruthyQueryValue(qp.get("isStudentView"))) + ) { + return true + } + + if ("learnpath" !== origin) { + return false + } + + // Authoring screens launched from the LP add-item screen must keep the full + // course layout. This is used by Exercise, Forum and Survey creation flows. + if (isTruthyQueryValue(qp.get("returnToLp"))) { + return false + } + + return ( + qp.has("lp_init") || + qp.has("learnpath_id") || + qp.has("learnpath_item_id") || + qp.has("learnpath_item_view_id") + ) +}) + const layout = computed(() => { if (showAccessUrlChosserLayout.value) { return AccessUrlChooserLayout @@ -242,12 +279,7 @@ const layout = computed(() => { return EmptyLayout } - const lpAction = String(qp.get("action") || "").toLowerCase() - const isLearningPathPlayerContext = - qp.has("lp_id") && - ("view" === lpAction || "1" === qp.get("embedded") || "true" === String(qp.get("isStudentView") || "")) - - if (isLearningPathPlayerContext) { + if (isLearnpathEmbeddedRoute.value) { return EmptyLayout } diff --git a/assets/vue/components/basecomponents/BaseCheckbox.vue b/assets/vue/components/basecomponents/BaseCheckbox.vue index 2d9d0bf5b73..532e33498db 100644 --- a/assets/vue/components/basecomponents/BaseCheckbox.vue +++ b/assets/vue/components/basecomponents/BaseCheckbox.vue @@ -3,13 +3,14 @@ @@ -40,5 +41,9 @@ defineProps({ type: [String, Number, Object], default: undefined, }, + disabled: { + type: Boolean, + default: false, + }, }) diff --git a/assets/vue/components/basecomponents/ChamiloIcons.js b/assets/vue/components/basecomponents/ChamiloIcons.js index eea388bd96a..1e5b7f4038a 100644 --- a/assets/vue/components/basecomponents/ChamiloIcons.js +++ b/assets/vue/components/basecomponents/ChamiloIcons.js @@ -16,6 +16,7 @@ export const chamiloIconToClass = { "back": "mdi mdi-arrow-left-bold-box", "bug-check": "", "bug-outline": "", + "broom": "mdi mdi-broom", "catalogue-add": "mdi mdi-book-plus", "catalogue-remove": "mdi mdi-book-minus-outline", "calendar-plus": "mdi mdi-calendar-plus", @@ -102,8 +103,10 @@ export const chamiloIconToClass = { "reply": "mdi mdi-reply", "reply-all": "mdi mdi-reply-all", "restart": "mdi mdi-restart", - "autolunch": "mdi mdi-rocket-launch", - "autolunch-off": "mdi mdi-rocket-launch-outline", + "rocket-launch": "mdi mdi-rocket-launch", + "rocket-launch-outline": "mdi mdi-rocket-launch-outline", + "autolaunch": "mdi mdi-rocket-launch", + "autolaunch-off": "mdi mdi-rocket-launch-outline", "select-all": "mdi mdi-select-group", "send": "mdi mdi-send", "sent": "mdi mdi-send-check", @@ -150,6 +153,7 @@ export const chamiloIconToClass = { "account-cancel": "mdi mdi-account-cancel", "zip-pack": "mdi mdi-archive-arrow-down", "zip-unpack": "mdi mdi-archive-arrow-up", + "clean-all": "mdi mdi-broom", "clear-all": "mdi mdi-broom", "qrcode": "mdi mdi-qrcode", "minus": "mdi mdi-minus", diff --git a/assets/vue/components/course/CourseTool.vue b/assets/vue/components/course/CourseTool.vue index b7793ebc47a..7fde3f34a1f 100644 --- a/assets/vue/components/course/CourseTool.vue +++ b/assets/vue/components/course/CourseTool.vue @@ -3,8 +3,8 @@ {{ $t(tool.tool.titleToShow) }} @@ -80,7 +80,7 @@ import { useCidReqStore } from "../../store/cidReq" const platformConfigStore = usePlatformConfig() const cidReqStore = useCidReqStore() -const { session } = storeToRefs(cidReqStore) +const { course, session } = storeToRefs(cidReqStore) const { getSetting } = storeToRefs(platformConfigStore) const isSorting = inject("isSorting") @@ -126,5 +126,64 @@ const titleCustomClass = computed(() => { } return "" }) -const isVisible = computed(() => props.tool.resourceNode.resourceLinks[0].visibility === 2) + +function getCourseResourceNodeId() { + return Number(cidReqStore.course?.resourceNode?.id) || Number(course.value?.resourceNode?.id) || 0 +} + +const isExerciseTool = computed(() => { + const values = [ + props.tool?.title, + props.tool?.titleToShow, + props.tool?.name, + props.tool?.tool?.title, + props.tool?.tool?.titleToShow, + props.tool?.tool?.name, + props.tool?.url, + ] + .filter(Boolean) + .join(" ") + .toLowerCase() + + return ( + values.includes("quiz") || + values.includes("exercise/exercise.php") || + values.includes("/main/exercise/exercise.php") || + values.includes("tests") || + values.includes("exercises") + ) +}) + +const exerciseToolRoute = computed(() => { + if (!isExerciseTool.value) { + return null + } + + const nodeId = getCourseResourceNodeId() + const courseId = Number(course.value?.id) || Number(cidReqStore.course?.id) || 0 + + if (nodeId <= 0 || courseId <= 0) { + return null + } + + return { + name: "ExerciseList", + params: { node: nodeId }, + query: { + cid: courseId, + sid: Number(session.value?.id) || 0, + gid: 0, + }, + } +}) + +const resolvedToolTo = computed(() => { + return exerciseToolRoute.value || props.tool.to || null +}) + +const resolvedToolUrl = computed(() => { + return exerciseToolRoute.value ? null : props.tool.url || null +}) + +const isVisible = computed(() => props.tool?.resourceNode?.resourceLinks?.[0]?.visibility === 2) diff --git a/assets/vue/router/exercise.js b/assets/vue/router/exercise.js new file mode 100644 index 00000000000..a467988d5e1 --- /dev/null +++ b/assets/vue/router/exercise.js @@ -0,0 +1,163 @@ +export default { + path: "/resources/exercise/:node/", + meta: { + requiresAuth: true, + showBreadcrumb: true, + tool: "exercise", + breadcrumb: "Exercises", + }, + name: "exercise", + component: () => import("../components/layout/SimpleRouterViewLayout.vue"), + redirect: { name: "ExerciseList" }, + children: [ + { + name: "ExerciseList", + path: "", + meta: { breadcrumb: "Exercises" }, + component: () => import("../views/exercise/ExerciseListView.vue"), + }, + { + name: "ExerciseCreate", + path: "create", + meta: { breadcrumb: "Create exercise" }, + component: () => import("../views/exercise/ExerciseConfigurationView.vue"), + }, + { + name: "ExerciseCategories", + path: "categories", + meta: { breadcrumb: "Exercise categories" }, + component: () => import("../views/exercise/ExerciseCategoryManagerView.vue"), + props: { categoryType: "exercise" }, + }, + { + name: "ExerciseQuestionCategories", + path: "question-categories", + meta: { breadcrumb: "Question categories" }, + component: () => import("../views/exercise/ExerciseCategoryManagerView.vue"), + props: { categoryType: "question" }, + }, + { + name: "ExerciseImportAiken", + path: "import/aiken", + meta: { breadcrumb: "Import Aiken quiz" }, + component: () => import("../views/exercise/ExerciseQuestionImportView.vue"), + props: { importType: "aiken" }, + }, + { + name: "ExerciseImportExcel", + path: "import/excel", + meta: { breadcrumb: "Import quiz from Excel" }, + component: () => import("../views/exercise/ExerciseQuestionImportView.vue"), + props: { importType: "excel" }, + }, + { + name: "ExerciseImportQti2", + path: "import/qti2", + meta: { breadcrumb: "Import exercises QTI2" }, + component: () => import("../views/exercise/ExerciseQuestionImportView.vue"), + props: { importType: "qti2" }, + }, + { + name: "ExerciseAiAikenGenerator", + path: "aiken-generator", + meta: { breadcrumb: "AI Aiken generator" }, + component: () => import("../views/exercise/ExerciseAiAikenGeneratorView.vue"), + }, + { + name: "ExerciseOverview", + path: ":exerciseId/overview", + meta: { breadcrumb: "Exercise overview" }, + component: () => import("../views/exercise/ExerciseOverviewView.vue"), + }, + { + name: "ExerciseEdit", + path: ":exerciseId/edit", + meta: { breadcrumb: "Edit exercise" }, + component: () => import("../views/exercise/ExerciseConfigurationView.vue"), + }, + { + name: "ExercisePlayer", + path: ":exerciseId/player", + meta: { breadcrumb: "Exercise player" }, + component: () => import("../views/exercise/ExercisePlayerView.vue"), + }, + { + name: "ExerciseResult", + path: ":exerciseId/result/:attemptId", + meta: { breadcrumb: "Exercise result" }, + component: () => import("../views/exercise/ExerciseResultView.vue"), + }, + { + name: "ExerciseReport", + path: ":exerciseId/report", + meta: { breadcrumb: "Learner score" }, + component: () => import("../views/exercise/ExerciseReportView.vue"), + }, + { + name: "ExerciseQuestionStats", + path: ":exerciseId/question-stats", + meta: { breadcrumb: "Question stats" }, + component: () => import("../views/exercise/ExerciseQuestionStatsView.vue"), + }, + { + name: "ExerciseReportByQuestion", + path: ":exerciseId/report-by-question", + meta: { breadcrumb: "Report by question" }, + component: () => import("../views/exercise/ExerciseReportByQuestionView.vue"), + }, + { + name: "ExerciseLiveResults", + path: ":exerciseId/live-results", + meta: { breadcrumb: "Live results" }, + component: () => import("../views/exercise/ExerciseLiveResultsView.vue"), + }, + { + name: "ExerciseGlobalQuestionSelector", + path: "questions/create", + meta: { breadcrumb: "Add a question" }, + component: () => import("../views/exercise/ExerciseGlobalQuestionSelectorView.vue"), + }, + { + name: "ExerciseGlobalQuestionCreate", + path: "questions/create/:questionType", + meta: { breadcrumb: "Create question" }, + component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"), + }, + { + name: "ExerciseQuestionPool", + path: "questions/recycle", + meta: { breadcrumb: "Recycle existing questions" }, + component: () => import("../views/exercise/ExerciseQuestionBankView.vue"), + }, + { + name: "ExerciseGlobalQuestionEdit", + path: "questions/:questionId/edit", + meta: { breadcrumb: "Edit question" }, + component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"), + }, + { + name: "ExerciseQuestions", + path: ":exerciseId/questions", + meta: { breadcrumb: "Questions" }, + component: () => import("../views/exercise/ExerciseQuestionSelectorView.vue"), + }, + { + name: "ExerciseQuestionBank", + path: ":exerciseId/question-bank", + meta: { breadcrumb: "Recycle existing questions" }, + component: () => import("../views/exercise/ExerciseQuestionBankView.vue"), + }, + { + name: "ExerciseQuestionCreate", + path: ":exerciseId/questions/create/:questionType", + meta: { breadcrumb: "Create question" }, + component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"), + }, + { + name: "ExerciseQuestionEdit", + path: ":exerciseId/questions/:questionId/edit", + meta: { breadcrumb: "Edit question" }, + component: () => import("../views/exercise/ExerciseQuestionEditorView.vue"), + }, + ], +} diff --git a/assets/vue/router/index.js b/assets/vue/router/index.js index de5db919a6f..887b8567b12 100644 --- a/assets/vue/router/index.js +++ b/assets/vue/router/index.js @@ -26,6 +26,7 @@ import assignments from "./assignments" import links from "./links" import forum from "./forum" import survey from "./survey" +import exercise from "./exercise" import glossary from "./glossary" import attendance from "./attendance" import lpRoutes from "./lp" @@ -292,6 +293,11 @@ async function courseHomeBeforeEnter(to) { const courseSettingsStore = useCourseSettings() const sid = sessionId ? `&sid=${sessionId}` : "" + const courseContextQuery = { + cid: courseId, + sid: sessionId || 0, + gid: 0, + } // Document auto-launch const documentAutoLaunch = parseInt(courseSettingsStore.getSetting("enable_document_auto_launch"), 10) || 0 @@ -305,20 +311,26 @@ async function courseHomeBeforeEnter(to) { // Exercise auto-launch const exerciseAutoLaunch = parseInt(courseSettingsStore.getSetting("enable_exercise_auto_launch"), 10) || 0 - - if (exerciseAutoLaunch === 2) { + const exerciseResourceNodeId = cidReqStore.course?.resourceNode?.id + if (exerciseAutoLaunch === 2 && exerciseResourceNodeId) { sessionStorage.setItem(autoLaunchKey, "true") - window.location.href = `/main/exercise/exercise.php?cid=${courseId}` + sid - return false + return { + name: "ExerciseList", + params: { node: exerciseResourceNodeId }, + query: courseContextQuery, + } } else if (exerciseAutoLaunch === 1) { const exerciseId = await courseService.getAutoLaunchExerciseId(courseId, sessionId) - if (exerciseId) { + if (exerciseId && exerciseResourceNodeId) { sessionStorage.setItem(autoLaunchKey, "true") - window.location.href = `/main/exercise/overview.php?exerciseId=${exerciseId}&cid=${courseId}` + sid - return false + return { + name: "ExerciseOverview", + params: { node: exerciseResourceNodeId, exerciseId }, + query: courseContextQuery, + } } } @@ -486,6 +498,7 @@ const router = createRouter({ links, forum, survey, + exercise, glossary, attendance, lpRoutes, diff --git a/assets/vue/services/exerciseService.js b/assets/vue/services/exerciseService.js new file mode 100644 index 00000000000..8888684ec6f --- /dev/null +++ b/assets/vue/services/exerciseService.js @@ -0,0 +1,363 @@ +import baseService from "./baseService" + +function cleanParams(params = {}) { + const query = {} + + for (const [key, value] of Object.entries(params)) { + if (value !== undefined && value !== null && String(value) !== "") { + query[key] = value + } + } + + return query +} + +function buildQueryString(params = {}) { + const query = new URLSearchParams() + + for (const [key, value] of Object.entries(cleanParams(params))) { + query.set(key, String(value)) + } + + const queryString = query.toString() + + return queryString ? `?${queryString}` : "" +} + +function exerciseRequestConfig(config = {}) { + return { + ...config, + } +} + +export default { + async getExerciseList(params = {}) { + return await baseService.get("/api/exercise/list", cleanParams(params), exerciseRequestConfig()) + }, + + async saveExerciseListAction(payload = {}, params = {}) { + const queryString = buildQueryString(params) + + return await baseService.post(`/api/exercise/list/action${queryString}`, payload, {}, exerciseRequestConfig()) + }, + + async getExerciseOverview(params = {}, exerciseId) { + return await baseService.get(`/api/exercise/overview/${exerciseId}`, cleanParams(params), exerciseRequestConfig()) + }, + + async getExerciseCategories(categoryType, params = {}) { + return await baseService.get(`/api/exercise/categories/${categoryType}`, cleanParams(params), exerciseRequestConfig()) + }, + + async saveExerciseCategoryAction(categoryType, payload = {}, params = {}) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/categories/${categoryType}/action${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async getExerciseConfiguration(params = {}, exerciseId = null) { + const endpoint = exerciseId ? `/api/exercise/configuration/${exerciseId}` : "/api/exercise/configuration" + + return await baseService.get(endpoint, cleanParams(params), exerciseRequestConfig()) + }, + + + async getExerciseQuestions(params = {}, exerciseId) { + return await baseService.get(`/api/exercise/questions/${exerciseId}`, cleanParams(params), exerciseRequestConfig()) + }, + + async getExerciseGlobalQuestionTypes(params = {}) { + return await baseService.get('/api/exercise/questions/global', cleanParams(params), exerciseRequestConfig()) + }, + + async getExerciseRuntime(params = {}, exerciseId) { + return await baseService.get(`/api/exercise/runtime/${exerciseId}`, cleanParams(params), exerciseRequestConfig()) + }, + + async startExerciseAttempt(payload = {}, params = {}, exerciseId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async saveExerciseRuntimeAnswer(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/answer${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + + async uploadExerciseRuntimeAnswer(formData, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.postForm( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/upload-answer${queryString}`, + formData, + exerciseRequestConfig(), + ) + }, + + async finishExerciseRuntimeAttempt(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/finish${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async getExerciseRuntimeResult(params = {}, exerciseId, attemptId) { + return await baseService.get( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/result`, + cleanParams(params), + exerciseRequestConfig(), + ) + }, + + async getExerciseRuntimeReport(params = {}, exerciseId) { + return await baseService.get( + `/api/exercise/runtime/${exerciseId}/attempts`, + cleanParams(params), + exerciseRequestConfig(), + ) + }, + + async runExerciseRuntimeReportBulkAction(payload = {}, params = {}, exerciseId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempts/action${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async getExerciseQuestionStats(params = {}, exerciseId) { + return await baseService.get( + `/api/exercise/runtime/${exerciseId}/question-stats`, + cleanParams(params), + exerciseRequestConfig(), + ) + }, + + async getExerciseReportByQuestion(params = {}, exerciseId) { + return await baseService.get( + `/api/exercise/runtime/${exerciseId}/report-by-question`, + cleanParams(params), + exerciseRequestConfig(), + ) + }, + + async getExerciseLiveResults(params = {}, exerciseId) { + return await baseService.get( + `/api/exercise/runtime/${exerciseId}/live-results`, + cleanParams(params), + exerciseRequestConfig(), + ) + }, + + async deleteExerciseRuntimeAttempt(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/delete${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async closeExerciseRuntimeAttempt(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/close${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async recalculateExerciseRuntimeAttempt(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/recalculate${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async emailExerciseRuntimeAttempt(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/email${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async emailExerciseRuntimeReportAttempts(payload = {}, params = {}, exerciseId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempts/email${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + buildExerciseRuntimeAttemptPdfUrl(params = {}, exerciseId, attemptId) { + return `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/pdf${buildQueryString(params)}` + }, + + async saveExerciseRuntimeCorrection(payload = {}, params = {}, exerciseId, attemptId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/runtime/${exerciseId}/attempt/${attemptId}/correction${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async getExerciseGlobalQuestionEditor(params = {}, questionType = null) { + const queryParams = { ...params } + + if (questionType !== null && questionType !== undefined) { + queryParams.type = questionType + } + + const endpoint = questionType !== null && questionType !== undefined + ? '/api/exercise/questions/global/editor' + : `/api/exercise/questions/global/editor/${params.questionId}` + + delete queryParams.questionId + + return await baseService.get(endpoint, cleanParams(queryParams), exerciseRequestConfig()) + }, + + async saveExerciseGlobalQuestion(payload, params = {}, questionId = null) { + const queryString = buildQueryString(params) + const endpoint = questionId + ? `/api/exercise/questions/global/editor/${questionId}${queryString}` + : `/api/exercise/questions/global/editor${queryString}` + + return await baseService.post(endpoint, payload, {}, exerciseRequestConfig()) + }, + + async getExerciseQuestionEditor(params = {}, exerciseId, questionId = null, questionType = null) { + const queryParams = { ...params } + + if (questionType !== null && questionType !== undefined) { + queryParams.type = questionType + } + + const endpoint = questionId + ? `/api/exercise/questions/${exerciseId}/editor/${questionId}` + : `/api/exercise/questions/${exerciseId}/editor` + + return await baseService.get(endpoint, cleanParams(queryParams), exerciseRequestConfig()) + }, + + async saveExerciseQuestion(payload, params = {}, exerciseId, questionId = null) { + const queryString = buildQueryString(params) + const endpoint = questionId + ? `/api/exercise/questions/${exerciseId}/editor/${questionId}${queryString}` + : `/api/exercise/questions/${exerciseId}/editor${queryString}` + + return await baseService.post(endpoint, payload, {}, exerciseRequestConfig()) + }, + + async saveExerciseQuestionAction(payload, params = {}, exerciseId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/questions/${exerciseId}/action${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async attachExerciseToLearningPath(payload = {}, params = {}, exerciseId) { + const queryString = buildQueryString(params) + + return await baseService.post( + `/api/exercise/questions/${exerciseId}/learning-path-item${queryString}`, + payload, + {}, + exerciseRequestConfig(), + ) + }, + + async getExerciseQuestionBank(params = {}, exerciseId = null) { + const endpoint = exerciseId ? `/api/exercise/questions/${exerciseId}/bank` : '/api/exercise/questions/bank' + + return await baseService.get(endpoint, cleanParams(params), exerciseRequestConfig()) + }, + + async saveExerciseQuestionBankAction(payload, params = {}, exerciseId = null) { + const queryString = buildQueryString(params) + const endpoint = exerciseId + ? `/api/exercise/questions/${exerciseId}/bank/action${queryString}` + : `/api/exercise/questions/bank/action${queryString}` + + return await baseService.post(endpoint, payload, {}, exerciseRequestConfig()) + }, + + async getExerciseAiAikenGenerator(params = {}) { + return await baseService.get("/api/exercise/ai-aiken-generator", cleanParams(params), exerciseRequestConfig()) + }, + + async generateExerciseAikenFromTopic(payload = {}) { + return await baseService.post("/ai/generate_aiken", payload, {}, exerciseRequestConfig()) + }, + + async generateExerciseAikenFromDocument(payload = {}) { + return await baseService.post("/ai/generate_aiken_from_document", payload, {}, exerciseRequestConfig()) + }, + + async getExerciseQuestionImport(importType, params = {}) { + return await baseService.get(`/api/exercise/import/${importType}`, cleanParams(params), exerciseRequestConfig()) + }, + + async importExerciseQuestions(importType, formData, params = {}) { + const queryString = buildQueryString(params) + + return await baseService.postForm(`/api/exercise/import/${importType}${queryString}`, formData, exerciseRequestConfig()) + }, + + async saveExerciseConfiguration(payload, params = {}, exerciseId = null) { + const queryString = buildQueryString(params) + + if (exerciseId) { + return await baseService.put(`/api/exercise/configuration/${exerciseId}${queryString}`, payload, exerciseRequestConfig()) + } + + return await baseService.post(`/api/exercise/configuration${queryString}`, payload, {}, exerciseRequestConfig()) + }, +} diff --git a/assets/vue/views/course/CourseHome.vue b/assets/vue/views/course/CourseHome.vue index 101949c3c81..767c83d65b7 100644 --- a/assets/vue/views/course/CourseHome.vue +++ b/assets/vue/views/course/CourseHome.vue @@ -368,6 +368,14 @@ const courseSettingsStore = useCourseSettings() const TOOL_VISIBILITY_VISIBLE = 2 +function normalizeToolNavigation(tool) { + if (routerTools.includes(tool.title)) { + tool.to = tool.url + } + + return tool +} + function getToolVisibility(tool) { return tool?.resourceNode?.resourceLinks?.[0]?.visibility } @@ -437,11 +445,7 @@ async function loadCourseTools(showSkeleton = true) { const cTools = await courseService.loadCTools(course.value.id, session.value?.id) const normalizedTools = cTools.map((rawTool) => { - const tool = { ...rawTool } - - if (routerTools.includes(tool.title)) { - tool.to = tool.url - } + const tool = normalizeToolNavigation({ ...rawTool }) // Convenience flag for UI states (e.g. customize mode) tool.isEnabled = diff --git a/assets/vue/views/exercise/ExerciseAiAikenGeneratorView.vue b/assets/vue/views/exercise/ExerciseAiAikenGeneratorView.vue new file mode 100644 index 00000000000..bb2f895d3c3 --- /dev/null +++ b/assets/vue/views/exercise/ExerciseAiAikenGeneratorView.vue @@ -0,0 +1,693 @@ +
+ +
+ +
+ +
+

+ {{ t("AI Aiken generator") }} +

+

+ {{ t("Generate an Aiken quiz from a topic or from one course document.") }} +

+
+ + + +
+

{{ successMessage }}

+

+ {{ t("Imported questions") }}: {{ importedQuestionCount }} +

+
+ + +
+
+ +
+ {{ t("Loading") }}... +
+ +
+ {{ t(config.message || "No AI text providers configured.") }} +
+ +