@hed-hog/lms 0.0.361 → 0.0.365
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +66 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +91 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
- package/dist/class-group/class-group.controller.d.ts +16 -16
- package/dist/class-group/class-group.service.d.ts +12 -12
- package/dist/course/course-audio-transcription.service.d.ts +3 -2
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +49 -8
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
- package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12-worker.service.js +109 -0
- package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
- package/dist/course/course-export-scorm12.service.d.ts +42 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12.service.js +628 -0
- package/dist/course/course-export-scorm12.service.js.map +1 -0
- package/dist/course/course-export.service.d.ts +84 -0
- package/dist/course/course-export.service.d.ts.map +1 -0
- package/dist/course/course-export.service.js +237 -0
- package/dist/course/course-export.service.js.map +1 -0
- package/dist/course/course-lesson.controller.d.ts +4 -0
- package/dist/course/course-lesson.controller.d.ts.map +1 -1
- package/dist/course/course-lesson.controller.js +10 -0
- package/dist/course/course-lesson.controller.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +24 -9
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +30 -3
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +25 -3
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +234 -24
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +8 -0
- package/dist/course/course-video-conversion.service.d.ts.map +1 -1
- package/dist/course/course-video-conversion.service.js +87 -51
- package/dist/course/course-video-conversion.service.js.map +1 -1
- package/dist/course/course-video-hls.service.d.ts +57 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -0
- package/dist/course/course-video-hls.service.js +767 -0
- package/dist/course/course-video-hls.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +115 -11
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +66 -28
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.mcp-tools.js +1 -1
- package/dist/course/course.mcp-tools.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +13 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +112 -11
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +682 -72
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
- package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
- package/dist/course/dto/cleanup-course-storage.dto.js +34 -0
- package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
- package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
- package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
- package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
- package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +5 -0
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-bulk-job.dto.js +26 -0
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
- package/dist/course/dto/create-course-export.dto.d.ts +14 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-export.dto.js +71 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts +54 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.js +537 -0
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts +32 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.js +301 -0
- package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
- package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
- package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload.constants.js +7 -0
- package/dist/course/lms-bulk-upload.constants.js.map +1 -0
- package/dist/course/lms-bulk-upload.controller.d.ts +144 -1
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +114 -4
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +153 -3
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +659 -21
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +6 -2
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +25 -8
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/course/scorm12-schemas.d.ts +4 -0
- package/dist/course/scorm12-schemas.d.ts.map +1 -0
- package/dist/course/scorm12-schemas.js +9 -0
- package/dist/course/scorm12-schemas.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +20 -20
- package/dist/enterprise/enterprise.service.d.ts +20 -20
- package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
- package/dist/enterprise/training/training-admin.service.d.ts +11 -11
- package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
- package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
- package/dist/enterprise/training/training-student.controller.d.ts +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +52 -1
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +217 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
- package/dist/evaluation/evaluation.controller.d.ts +8 -8
- package/dist/evaluation/evaluation.service.d.ts +26 -8
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +125 -0
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
- package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-standalone-question.dto.js +70 -0
- package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
- package/dist/exam/exam.module.d.ts.map +1 -1
- package/dist/exam/exam.module.js +2 -1
- package/dist/exam/exam.module.js.map +1 -1
- package/dist/exam/exam.service.d.ts +21 -0
- package/dist/exam/exam.service.d.ts.map +1 -1
- package/dist/exam/exam.service.js +80 -0
- package/dist/exam/exam.service.js.map +1 -1
- package/dist/exam/question.controller.d.ts +27 -0
- package/dist/exam/question.controller.d.ts.map +1 -0
- package/dist/exam/question.controller.js +53 -0
- package/dist/exam/question.controller.js.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +30 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +440 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
- package/dist/lms-commerce-access.subscriber.d.ts +11 -0
- package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
- package/dist/lms-commerce-access.subscriber.js +74 -0
- package/dist/lms-commerce-access.subscriber.js.map +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +21 -5
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
- package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
- package/dist/platforma/dto/update-profile.dto.js +87 -0
- package/dist/platforma/dto/update-profile.dto.js.map +1 -0
- package/dist/platforma/platforma-video.service.d.ts +39 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -0
- package/dist/platforma/platforma-video.service.js +301 -0
- package/dist/platforma/platforma-video.service.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +182 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +243 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/platforma/platforma.service.d.ts +27 -0
- package/dist/platforma/platforma.service.d.ts.map +1 -0
- package/dist/platforma/platforma.service.js +274 -0
- package/dist/platforma/platforma.service.js.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
- package/dist/student-xp/student-xp.controller.d.ts +56 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
- package/dist/student-xp/student-xp.controller.js +138 -0
- package/dist/student-xp/student-xp.controller.js.map +1 -0
- package/dist/student-xp/student-xp.module.d.ts +3 -0
- package/dist/student-xp/student-xp.module.d.ts.map +1 -0
- package/dist/student-xp/student-xp.module.js +25 -0
- package/dist/student-xp/student-xp.module.js.map +1 -0
- package/dist/student-xp/student-xp.service.d.ts +81 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -0
- package/dist/student-xp/student-xp.service.js +247 -0
- package/dist/student-xp/student-xp.service.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
- package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
- package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-area.controller.js +105 -0
- package/dist/xp-catalog/xp-area.controller.js.map +1 -0
- package/dist/xp-catalog/xp-area.service.d.ts +35 -0
- package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-area.service.js +168 -0
- package/dist/xp-catalog/xp-area.service.js.map +1 -0
- package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
- package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
- package/dist/xp-catalog/xp-catalog.module.js +29 -0
- package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
- package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
- package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
- package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
- package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
- package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-learning-type.service.js +146 -0
- package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
- package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
- package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-skill.controller.js +113 -0
- package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
- package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
- package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-skill.service.js +174 -0
- package/dist/xp-catalog/xp-skill.service.js.map +1 -0
- package/hedhog/data/evaluation_topic.yaml +17 -0
- package/hedhog/data/menu.yaml +91 -7
- package/hedhog/data/queue_definition.yaml +48 -0
- package/hedhog/data/route.yaml +511 -29
- package/hedhog/data/setting_group.yaml +20 -20
- package/hedhog/data/xp_area.yaml +164 -0
- package/hedhog/data/xp_learning_type.yaml +131 -0
- package/hedhog/data/xp_skill.yaml +1834 -0
- package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
- package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1749 -0
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
- package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +73 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1172 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +87 -46
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +618 -480
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +672 -737
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +24 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +53 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +80 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
- package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
- package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
- package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +445 -230
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
- package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
- package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
- package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
- package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
- package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
- package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
- package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
- package/hedhog/frontend/messages/en.json +412 -31
- package/hedhog/frontend/messages/pt.json +412 -31
- package/hedhog/table/course_export.yaml +62 -0
- package/hedhog/table/lesson_xp_map.yaml +50 -0
- package/hedhog/table/lesson_xp_segment.yaml +40 -0
- package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
- package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
- package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
- package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
- package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
- package/hedhog/table/student_area_xp.yaml +30 -0
- package/hedhog/table/student_learning_type_xp.yaml +30 -0
- package/hedhog/table/student_skill_xp.yaml +30 -0
- package/hedhog/table/student_xp_event.yaml +34 -0
- package/hedhog/table/xp_area.yaml +39 -0
- package/hedhog/table/xp_learning_type.yaml +34 -0
- package/hedhog/table/xp_skill.yaml +39 -0
- package/package.json +13 -8
- package/src/bitcode-wallet/bitcode-wallet.service.ts +152 -0
- package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
- package/src/course/course-audio-transcription.service.ts +58 -21
- package/src/course/course-export-scorm12-worker.service.ts +124 -0
- package/src/course/course-export-scorm12.service.ts +668 -0
- package/src/course/course-export.service.ts +280 -0
- package/src/course/course-lesson.controller.ts +6 -1
- package/src/course/course-structure.controller.ts +23 -1
- package/src/course/course-structure.service.ts +273 -7
- package/src/course/course-video-conversion.service.ts +113 -75
- package/src/course/course-video-hls.service.ts +946 -0
- package/src/course/course.controller.ts +54 -21
- package/src/course/course.mcp-tools.ts +1 -1
- package/src/course/course.module.ts +13 -0
- package/src/course/course.service.ts +906 -76
- package/src/course/dto/cleanup-course-storage.dto.ts +23 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
- package/src/course/dto/create-course-bulk-job.dto.ts +10 -0
- package/src/course/dto/create-course-export.dto.ts +56 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
- package/src/course/lms-bulk-upload-automation.service.ts +707 -0
- package/src/course/lms-bulk-upload-infra.service.ts +360 -0
- package/src/course/lms-bulk-upload.constants.ts +5 -0
- package/src/course/lms-bulk-upload.controller.ts +110 -4
- package/src/course/lms-bulk-upload.service.ts +1092 -204
- package/src/course/lms-setting.controller.ts +26 -8
- package/src/course/scorm12-schemas.ts +9 -0
- package/src/enterprise/training/training-student.service.ts +221 -2
- package/src/evaluation/evaluation.service.ts +123 -0
- package/src/exam/dto/create-standalone-question.dto.ts +66 -0
- package/src/exam/exam.module.ts +2 -1
- package/src/exam/exam.service.ts +86 -0
- package/src/exam/question.controller.ts +28 -0
- package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
- package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
- package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
- package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
- package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +570 -0
- package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
- package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
- package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
- package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
- package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
- package/src/lms-commerce-access.subscriber.ts +88 -0
- package/src/lms.module.ts +21 -5
- package/src/platforma/dto/update-profile.dto.ts +59 -0
- package/src/platforma/platforma-video.service.ts +346 -0
- package/src/platforma/platforma.controller.ts +152 -3
- package/src/platforma/platforma.service.ts +268 -0
- package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
- package/src/student-xp/student-xp.controller.ts +92 -0
- package/src/student-xp/student-xp.module.ts +12 -0
- package/src/student-xp/student-xp.service.ts +318 -0
- package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
- package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
- package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
- package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
- package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
- package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
- package/src/xp-catalog/xp-area.controller.ts +64 -0
- package/src/xp-catalog/xp-area.service.ts +196 -0
- package/src/xp-catalog/xp-catalog.module.ts +16 -0
- package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
- package/src/xp-catalog/xp-learning-type.service.ts +170 -0
- package/src/xp-catalog/xp-skill.controller.ts +71 -0
- package/src/xp-catalog/xp-skill.service.ts +205 -0
- package/hedhog/data/video_resolution_profile.yaml +0 -7
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
- package/hedhog/table/course_video_resolution_profile.yaml +0 -22
- package/hedhog/table/video_resolution_profile.yaml +0 -18
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
- package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
- package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
ChevronUp,
|
|
9
9
|
CircleDot,
|
|
10
10
|
CircleOff,
|
|
11
|
-
ClipboardList,
|
|
12
11
|
Clock,
|
|
13
12
|
Download,
|
|
14
13
|
ExternalLink,
|
|
@@ -20,6 +19,7 @@ import {
|
|
|
20
19
|
ListChecks,
|
|
21
20
|
Loader2,
|
|
22
21
|
Lock,
|
|
22
|
+
Mic,
|
|
23
23
|
Pencil,
|
|
24
24
|
Play,
|
|
25
25
|
Plus,
|
|
@@ -40,7 +40,6 @@ import { z } from 'zod';
|
|
|
40
40
|
|
|
41
41
|
import { CopyButton } from '@/components/copy-button';
|
|
42
42
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
43
|
-
import { Badge } from '@/components/ui/badge';
|
|
44
43
|
import { Button } from '@/components/ui/button';
|
|
45
44
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
46
45
|
import {
|
|
@@ -78,6 +77,7 @@ import {
|
|
|
78
77
|
SheetHeader,
|
|
79
78
|
SheetTitle,
|
|
80
79
|
} from '@/components/ui/sheet';
|
|
80
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
81
81
|
import { Switch } from '@/components/ui/switch';
|
|
82
82
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
83
83
|
import { Textarea } from '@/components/ui/textarea';
|
|
@@ -113,12 +113,16 @@ import {
|
|
|
113
113
|
getQueueJob,
|
|
114
114
|
updateLessonFrame,
|
|
115
115
|
uploadFile,
|
|
116
|
+
type ApiQuestion,
|
|
116
117
|
type QueueJobResponse,
|
|
117
118
|
type QueueJobStatus,
|
|
118
119
|
} from '../_data/services/course-structure.service';
|
|
119
120
|
import {
|
|
121
|
+
useCreateQuestionMutation,
|
|
120
122
|
useDeleteLessonMutation,
|
|
123
|
+
useQuestionsQuery,
|
|
121
124
|
useUpdateLessonMutation,
|
|
125
|
+
useUpdateResourceTypeMutation,
|
|
122
126
|
} from '../_data/use-course-structure-mutations';
|
|
123
127
|
import {
|
|
124
128
|
courseStructureQueryKey,
|
|
@@ -126,9 +130,11 @@ import {
|
|
|
126
130
|
} from '../_data/use-course-structure-query';
|
|
127
131
|
import { useLmsSettingsQuery } from '../_data/use-lms-settings-query';
|
|
128
132
|
import {
|
|
133
|
+
useStartTranscriptionMutation,
|
|
129
134
|
useTranscriptionSegmentsQuery,
|
|
130
135
|
useUpdateTranscriptionSegmentsMutation,
|
|
131
136
|
} from '../_data/use-transcription-segments';
|
|
137
|
+
import { LessonXpTab } from './detail-lesson-xp-tab';
|
|
132
138
|
import { IconActionTooltip } from './icon-action-tooltip';
|
|
133
139
|
import { useStructureStore } from './store';
|
|
134
140
|
import type {
|
|
@@ -182,10 +188,6 @@ function getInstructorAvatarUrl(avatarId?: number | null): string | undefined {
|
|
|
182
188
|
: `/person/avatar/${avatarId}`;
|
|
183
189
|
}
|
|
184
190
|
|
|
185
|
-
function videoProfileResourceType(profileId: number): string {
|
|
186
|
-
return `video_profile:${profileId}`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
191
|
const MAX_VIDEO_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024;
|
|
190
192
|
|
|
191
193
|
type LessonTypeLabelKey = `types.${LessonType}`;
|
|
@@ -200,7 +202,8 @@ type VideoJobEventMessageKey = `lessonForm.videoJobEvents.${VideoJobEventType}`;
|
|
|
200
202
|
type VideoJobProgressPhase =
|
|
201
203
|
| 'download_original'
|
|
202
204
|
| 'probe_duration'
|
|
203
|
-
| '
|
|
205
|
+
| 'hls_encode'
|
|
206
|
+
| 'hls_upload'
|
|
204
207
|
| 'extract_frames'
|
|
205
208
|
| 'extract_frames_done'
|
|
206
209
|
| 'extract_audio'
|
|
@@ -219,6 +222,43 @@ type TranslateFn = (
|
|
|
219
222
|
key: string,
|
|
220
223
|
values?: Record<string, string | number>
|
|
221
224
|
) => string;
|
|
225
|
+
type GenericResourceType =
|
|
226
|
+
| 'supplementary_material'
|
|
227
|
+
| 'student_download'
|
|
228
|
+
| 'attachment'
|
|
229
|
+
| 'document'
|
|
230
|
+
| 'lesson_audio';
|
|
231
|
+
|
|
232
|
+
const GENERIC_RESOURCE_TYPES: GenericResourceType[] = [
|
|
233
|
+
'supplementary_material',
|
|
234
|
+
'student_download',
|
|
235
|
+
'attachment',
|
|
236
|
+
'document',
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const GENERIC_RESOURCE_TYPE_LABEL_KEYS: Record<GenericResourceType, string> = {
|
|
240
|
+
supplementary_material: 'lessonForm.resourceTypes.supplementary_material',
|
|
241
|
+
student_download: 'lessonForm.resourceTypes.student_download',
|
|
242
|
+
attachment: 'lessonForm.resourceTypes.attachment',
|
|
243
|
+
document: 'lessonForm.resourceTypes.document',
|
|
244
|
+
lesson_audio: 'lessonForm.resourceTypes.lesson_audio',
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
function humanizeResourceType(type: string): string {
|
|
248
|
+
return type
|
|
249
|
+
.split(/[_:./-]+/)
|
|
250
|
+
.filter(Boolean)
|
|
251
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
252
|
+
.join(' ');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getResourceTypeLabel(t: TranslateFn, type: string): string {
|
|
256
|
+
if (type in GENERIC_RESOURCE_TYPE_LABEL_KEYS) {
|
|
257
|
+
return t(GENERIC_RESOURCE_TYPE_LABEL_KEYS[type as GenericResourceType]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return humanizeResourceType(type);
|
|
261
|
+
}
|
|
222
262
|
|
|
223
263
|
type LessonEditorTab =
|
|
224
264
|
| 'dados'
|
|
@@ -227,7 +267,8 @@ type LessonEditorTab =
|
|
|
227
267
|
| 'imagens'
|
|
228
268
|
| 'transcricao'
|
|
229
269
|
| 'audios'
|
|
230
|
-
| 'recursos'
|
|
270
|
+
| 'recursos'
|
|
271
|
+
| 'xp';
|
|
231
272
|
|
|
232
273
|
const ACTIVE_VIDEO_JOB_STATUSES: QueueJobStatus[] = [
|
|
233
274
|
'pending',
|
|
@@ -274,7 +315,8 @@ const VIDEO_JOB_STATUS_COLORS: Record<QueueJobStatus, string> = {
|
|
|
274
315
|
const VIDEO_JOB_PROGRESS_PHASES = new Set<VideoJobProgressPhase>([
|
|
275
316
|
'download_original',
|
|
276
317
|
'probe_duration',
|
|
277
|
-
'
|
|
318
|
+
'hls_encode',
|
|
319
|
+
'hls_upload',
|
|
278
320
|
'extract_frames',
|
|
279
321
|
'extract_frames_done',
|
|
280
322
|
'extract_audio',
|
|
@@ -367,19 +409,6 @@ function getVideoJobProgressMessage(
|
|
|
367
409
|
const phase = getVideoJobProgressPhase(event);
|
|
368
410
|
const metadata = event.metadata ?? {};
|
|
369
411
|
|
|
370
|
-
if (phase === 'convert_profile') {
|
|
371
|
-
const profileName =
|
|
372
|
-
typeof metadata.profileName === 'string' && metadata.profileName.trim()
|
|
373
|
-
? metadata.profileName.trim()
|
|
374
|
-
: typeof metadata.profileId === 'number'
|
|
375
|
-
? `#${metadata.profileId}`
|
|
376
|
-
: '—';
|
|
377
|
-
|
|
378
|
-
return t('lessonForm.videoJobProgress.convert_profile', {
|
|
379
|
-
profileName,
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
|
|
383
412
|
if (phase === 'extract_frames_done') {
|
|
384
413
|
const count =
|
|
385
414
|
typeof metadata.frames === 'number' && Number.isFinite(metadata.frames)
|
|
@@ -461,12 +490,6 @@ const TYPE_CONFIG: Record<
|
|
|
461
490
|
bg: 'bg-amber-500/10',
|
|
462
491
|
labelKey: 'types.questao',
|
|
463
492
|
},
|
|
464
|
-
exercicio: {
|
|
465
|
-
icon: ClipboardList,
|
|
466
|
-
color: 'text-purple-500',
|
|
467
|
-
bg: 'bg-purple-500/10',
|
|
468
|
-
labelKey: 'types.exercicio',
|
|
469
|
-
},
|
|
470
493
|
};
|
|
471
494
|
|
|
472
495
|
const STATUS_COLORS: Record<LessonStatus, string> = {
|
|
@@ -510,39 +533,15 @@ function generateAltId(): string {
|
|
|
510
533
|
return Math.random().toString(36).slice(2, 9);
|
|
511
534
|
}
|
|
512
535
|
|
|
513
|
-
|
|
514
|
-
{
|
|
515
|
-
id:
|
|
516
|
-
title:
|
|
517
|
-
type: 'multiple_choice',
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
texto: 'Uma biblioteca JavaScript para interfaces',
|
|
523
|
-
correta: true,
|
|
524
|
-
},
|
|
525
|
-
{ id: 'a2', texto: 'Um framework CSS', correta: false },
|
|
526
|
-
{ id: 'a3', texto: 'Um banco de dados', correta: false },
|
|
527
|
-
],
|
|
528
|
-
},
|
|
529
|
-
{
|
|
530
|
-
id: 'q2',
|
|
531
|
-
title: 'Defina componente funcional',
|
|
532
|
-
type: 'essay',
|
|
533
|
-
points: 2,
|
|
534
|
-
},
|
|
535
|
-
{
|
|
536
|
-
id: 'q3',
|
|
537
|
-
title: 'TypeScript é um superset de JavaScript?',
|
|
538
|
-
type: 'true_false',
|
|
539
|
-
points: 1,
|
|
540
|
-
alternatives: [
|
|
541
|
-
{ id: 'true', texto: 'Verdadeiro', correta: true },
|
|
542
|
-
{ id: 'false', texto: 'Falso', correta: false },
|
|
543
|
-
],
|
|
544
|
-
},
|
|
545
|
-
];
|
|
536
|
+
function apiQuestionToMock(q: ApiQuestion): MockQuestion {
|
|
537
|
+
return {
|
|
538
|
+
id: String(q.id),
|
|
539
|
+
title: q.statement.replace(/<[^>]+>/g, '').slice(0, 80),
|
|
540
|
+
type: (q.questionType as QuestionType) ?? 'multiple_choice',
|
|
541
|
+
statement: q.statement,
|
|
542
|
+
points: q.points,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
546
545
|
|
|
547
546
|
type FormValues = {
|
|
548
547
|
code: string;
|
|
@@ -557,7 +556,7 @@ type FormValues = {
|
|
|
557
556
|
videoUrl?: string;
|
|
558
557
|
transcription?: string;
|
|
559
558
|
postContent?: string;
|
|
560
|
-
|
|
559
|
+
linkedExam?: string | null;
|
|
561
560
|
};
|
|
562
561
|
|
|
563
562
|
type EditableTranscriptionSegment = {
|
|
@@ -749,17 +748,23 @@ function SortableAlternativa({
|
|
|
749
748
|
|
|
750
749
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
751
750
|
|
|
751
|
+
const LESSON_TABS: LessonEditorTab[] = [
|
|
752
|
+
'dados',
|
|
753
|
+
'conteudo',
|
|
754
|
+
'videos',
|
|
755
|
+
'imagens',
|
|
756
|
+
'transcricao',
|
|
757
|
+
'audios',
|
|
758
|
+
'recursos',
|
|
759
|
+
'xp',
|
|
760
|
+
];
|
|
761
|
+
|
|
752
762
|
interface EditorLessonProps {
|
|
753
763
|
lessonId: string;
|
|
764
|
+
defaultTab?: string;
|
|
765
|
+
onTabChange?: (tab: string) => void;
|
|
754
766
|
}
|
|
755
767
|
|
|
756
|
-
type VideoProfileOption = {
|
|
757
|
-
id: number;
|
|
758
|
-
name: string;
|
|
759
|
-
ffmpeg_params: string;
|
|
760
|
-
status: string;
|
|
761
|
-
};
|
|
762
|
-
|
|
763
768
|
type FramePreviewSource = {
|
|
764
769
|
key: string;
|
|
765
770
|
label: string;
|
|
@@ -776,7 +781,7 @@ type FrameAssetMetadata = {
|
|
|
776
781
|
sizeLabel: string;
|
|
777
782
|
};
|
|
778
783
|
|
|
779
|
-
export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
784
|
+
export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLessonProps) {
|
|
780
785
|
const t = useTranslations('lms.CoursesPage.StructurePage');
|
|
781
786
|
const tabAudiosLabel = (() => {
|
|
782
787
|
try {
|
|
@@ -788,6 +793,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
788
793
|
const lesson = useStructureStore((s) =>
|
|
789
794
|
s.lessons.find((l) => l.id === lessonId)
|
|
790
795
|
);
|
|
796
|
+
const updateLessonInStore = useStructureStore((s) => s.updateLesson);
|
|
791
797
|
const sessions = useStructureStore((s) => s.sessions);
|
|
792
798
|
const persistedVideoProvider: VideoProvider | undefined =
|
|
793
799
|
lesson?.videoProvider === 'youtube' || lesson?.videoProvider === 'vimeo'
|
|
@@ -800,6 +806,9 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
800
806
|
const updateTranscriptionSegments = useUpdateTranscriptionSegmentsMutation(
|
|
801
807
|
lesson?.id ?? null
|
|
802
808
|
);
|
|
809
|
+
const { mutate: startTranscription, isPending: isStartingTranscription } =
|
|
810
|
+
useStartTranscriptionMutation(lesson?.id ?? null);
|
|
811
|
+
const updateResourceTypeMutation = useUpdateResourceTypeMutation();
|
|
803
812
|
const deleteLesson = useDeleteLessonMutation();
|
|
804
813
|
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
805
814
|
const courseId = useStructureStore((s) => s.courseId);
|
|
@@ -834,11 +843,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
834
843
|
fill_blank: t('questionEditor.types.fillBlank'),
|
|
835
844
|
matching: t('questionEditor.types.matching'),
|
|
836
845
|
};
|
|
837
|
-
const isVideoConversionEnabled = lmsSettings.videoConversionEnabled;
|
|
838
846
|
const schema = z.object({
|
|
839
847
|
code: z.string().min(1, t('questionEditor.validation.codeRequired')),
|
|
840
848
|
title: z.string().min(1, t('questionEditor.validation.titleRequired')),
|
|
841
|
-
type: z.enum(['video', 'post', 'questao'
|
|
849
|
+
type: z.enum(['video', 'post', 'questao'] as const),
|
|
842
850
|
duration: z.coerce.number().min(0),
|
|
843
851
|
status: z.enum([
|
|
844
852
|
'preparada',
|
|
@@ -856,7 +864,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
856
864
|
videoUrl: z.string().optional(),
|
|
857
865
|
transcription: z.string().optional(),
|
|
858
866
|
postContent: z.string().optional(),
|
|
859
|
-
|
|
867
|
+
linkedExam: z.string().nullable().optional(),
|
|
860
868
|
});
|
|
861
869
|
|
|
862
870
|
const instructorPool =
|
|
@@ -880,7 +888,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
880
888
|
videoUrl: lesson?.videoUrl ?? '',
|
|
881
889
|
transcription: lesson?.transcription ?? '',
|
|
882
890
|
postContent: lesson?.postContent ?? '',
|
|
883
|
-
|
|
891
|
+
linkedExam: lesson?.linkedExam ?? null,
|
|
884
892
|
});
|
|
885
893
|
|
|
886
894
|
const form = useForm<FormValues>({
|
|
@@ -891,6 +899,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
891
899
|
const { isDirty } = form.formState;
|
|
892
900
|
const watchedType = useWatch({ control: form.control, name: 'type' });
|
|
893
901
|
const watchedStatus = useWatch({ control: form.control, name: 'status' });
|
|
902
|
+
const watchedPublished = useWatch({
|
|
903
|
+
control: form.control,
|
|
904
|
+
name: 'published',
|
|
905
|
+
});
|
|
894
906
|
const watchedVideoProvider = useWatch({
|
|
895
907
|
control: form.control,
|
|
896
908
|
name: 'videoProvider',
|
|
@@ -906,12 +918,21 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
906
918
|
() => lesson?.resources ?? []
|
|
907
919
|
);
|
|
908
920
|
const [jobTimerNowMs, setJobTimerNowMs] = useState<number>(() => Date.now());
|
|
909
|
-
const [activeTab, setActiveTab] = useState<LessonEditorTab>(
|
|
921
|
+
const [activeTab, setActiveTab] = useState<LessonEditorTab>(() =>
|
|
922
|
+
defaultTab && LESSON_TABS.includes(defaultTab as LessonEditorTab)
|
|
923
|
+
? (defaultTab as LessonEditorTab)
|
|
924
|
+
: 'dados'
|
|
925
|
+
);
|
|
910
926
|
const [isJobFeedbackCollapsed, setIsJobFeedbackCollapsed] = useState<boolean>(
|
|
911
927
|
() => readVideoJobFeedbackCollapsedPreference()
|
|
912
928
|
);
|
|
913
929
|
const [jobDetailOpen, setJobDetailOpen] = useState(false);
|
|
914
930
|
const [resourcesDirty, setResourcesDirty] = useState(false);
|
|
931
|
+
// 🆕 Filtro de tipo de recurso (estado de sessão)
|
|
932
|
+
const [resourceTypeFilter, setResourceTypeFilter] = useState<string>('all');
|
|
933
|
+
const [isUpdatingResourceType, setIsUpdatingResourceType] = useState<
|
|
934
|
+
string | null
|
|
935
|
+
>(null);
|
|
915
936
|
const [conversionJobId, setConversionJobId] = useState<number | null>(null);
|
|
916
937
|
const [videoUploadError, setVideoUploadError] = useState<string | null>(null);
|
|
917
938
|
const [dragOver, setDragOver] = useState(false);
|
|
@@ -921,9 +942,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
921
942
|
>(null);
|
|
922
943
|
const [isRequeueingOriginalVideo, setIsRequeueingOriginalVideo] =
|
|
923
944
|
useState(false);
|
|
924
|
-
const [profileUploadProgress, setProfileUploadProgress] = useState<
|
|
925
|
-
Record<number, number>
|
|
926
|
-
>({});
|
|
927
945
|
const [isResolvingVideoPreview, setIsResolvingVideoPreview] = useState(false);
|
|
928
946
|
const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
|
|
929
947
|
const [videoPreviewResource, setVideoPreviewResource] =
|
|
@@ -1056,26 +1074,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1056
1074
|
>(() => toEditableTranscriptionSegments());
|
|
1057
1075
|
const [transcriptionDirty, setTranscriptionDirty] = useState(false);
|
|
1058
1076
|
|
|
1059
|
-
const { data: fetchedTranscriptionSegments = [] } =
|
|
1060
|
-
useTranscriptionSegmentsQuery(lesson?.id ?? null);
|
|
1061
|
-
|
|
1062
1077
|
const {
|
|
1063
|
-
data:
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
refetch: refetchCourseVideoProfiles,
|
|
1067
|
-
} = useQuery<VideoProfileOption[]>({
|
|
1068
|
-
queryKey: ['lms-course-video-resolution-profiles', courseId],
|
|
1069
|
-
queryFn: async () => {
|
|
1070
|
-
const response = await request<VideoProfileOption[]>({
|
|
1071
|
-
url: `/lms/courses/${courseId}/video-resolution-profiles`,
|
|
1072
|
-
method: 'GET',
|
|
1073
|
-
});
|
|
1074
|
-
return response.data ?? [];
|
|
1075
|
-
},
|
|
1076
|
-
enabled: Boolean(courseId),
|
|
1077
|
-
initialData: [],
|
|
1078
|
-
});
|
|
1078
|
+
data: fetchedTranscriptionSegments = [],
|
|
1079
|
+
isLoading: isLoadingTranscription,
|
|
1080
|
+
} = useTranscriptionSegmentsQuery(lesson?.id ?? null);
|
|
1079
1081
|
|
|
1080
1082
|
const {
|
|
1081
1083
|
data: conversionJob,
|
|
@@ -1126,13 +1128,19 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1126
1128
|
);
|
|
1127
1129
|
|
|
1128
1130
|
// ── Question sheet state ────────────────────────────────────────────────────
|
|
1131
|
+
const { data: questionsData, isLoading: questionsLoading } = useQuestionsQuery();
|
|
1132
|
+
const createQuestionMutation = useCreateQuestionMutation();
|
|
1129
1133
|
const [questionSheetOpen, setQuestionSheetOpen] = useState(false);
|
|
1130
1134
|
const [editingQuestion, setEditingQuestion] = useState<MockQuestion | null>(
|
|
1131
1135
|
null
|
|
1132
1136
|
);
|
|
1133
|
-
const [selectedQuestion, setSelectedQuestion] = useState<MockQuestion | null>(
|
|
1134
|
-
|
|
1135
|
-
)
|
|
1137
|
+
const [selectedQuestion, setSelectedQuestion] = useState<MockQuestion | null>(null);
|
|
1138
|
+
|
|
1139
|
+
useEffect(() => {
|
|
1140
|
+
if (!lesson?.linkedExam || !questionsData) return;
|
|
1141
|
+
const found = questionsData.data.find((q) => String(q.id) === lesson.linkedExam);
|
|
1142
|
+
if (found) setSelectedQuestion(apiQuestionToMock(found));
|
|
1143
|
+
}, [lesson?.linkedExam, questionsData]);
|
|
1136
1144
|
const [qSheetStatement, setQSheetStatement] = useState('');
|
|
1137
1145
|
const [qSheetType, setQSheetType] = useState<QuestionType>('multiple_choice');
|
|
1138
1146
|
const [qSheetPoints, setQSheetPoints] = useState(1);
|
|
@@ -1164,7 +1172,12 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1164
1172
|
toEditableTranscriptionSegments(fetchedTranscriptionSegments)
|
|
1165
1173
|
);
|
|
1166
1174
|
setTranscriptionDirty(false);
|
|
1167
|
-
|
|
1175
|
+
if (lesson?.id) {
|
|
1176
|
+
updateLessonInStore(lesson.id, {
|
|
1177
|
+
transcriptionSegments: fetchedTranscriptionSegments,
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}, [lesson?.id, fetchedTranscriptionSegments, updateLessonInStore]);
|
|
1168
1181
|
|
|
1169
1182
|
useEffect(() => {
|
|
1170
1183
|
const frameIds = new Set(videoFrames.map((frame) => frame.id));
|
|
@@ -1420,11 +1433,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1420
1433
|
String(originalVideoResource.fileId ?? originalVideoResource.id)
|
|
1421
1434
|
)
|
|
1422
1435
|
: false;
|
|
1423
|
-
const profileVideoResources = new Map(
|
|
1424
|
-
localResources
|
|
1425
|
-
.filter((res) => res.type.startsWith('video_profile:'))
|
|
1426
|
-
.map((res) => [Number(res.type.replace('video_profile:', '')), res])
|
|
1427
|
-
);
|
|
1428
1436
|
const audioResources = localResources.filter(
|
|
1429
1437
|
(res) => res.type === 'lesson_audio'
|
|
1430
1438
|
);
|
|
@@ -1432,9 +1440,26 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1432
1440
|
(res) =>
|
|
1433
1441
|
res.type !== 'video_original' && !res.type.startsWith('video_profile:')
|
|
1434
1442
|
);
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1443
|
+
// Todos os recursos genéricos aparecem na aba Recursos
|
|
1444
|
+
const supplementaryResources = genericResources;
|
|
1445
|
+
|
|
1446
|
+
// 🆕 Recursos filtrados baseado no tipo selecionado
|
|
1447
|
+
const filteredSupplementaryResources =
|
|
1448
|
+
resourceTypeFilter === 'all'
|
|
1449
|
+
? supplementaryResources
|
|
1450
|
+
: supplementaryResources.filter((res) => res.type === resourceTypeFilter);
|
|
1451
|
+
|
|
1452
|
+
// 🆕 Tipos únicos presentes nos recursos atuais
|
|
1453
|
+
const availableResourceTypes = Array.from(
|
|
1454
|
+
new Set(supplementaryResources.map((res) => res.type).filter(Boolean))
|
|
1455
|
+
).sort();
|
|
1456
|
+
|
|
1457
|
+
const resourceTypeFilterOptions = availableResourceTypes.map((type) => ({
|
|
1458
|
+
type,
|
|
1459
|
+
count: supplementaryResources.filter((res) => res.type === type).length,
|
|
1460
|
+
label: getResourceTypeLabel(t, type),
|
|
1461
|
+
}));
|
|
1462
|
+
|
|
1438
1463
|
const isConversionJobActive = conversionJob
|
|
1439
1464
|
? ACTIVE_VIDEO_JOB_STATUSES.includes(conversionJob.status)
|
|
1440
1465
|
: false;
|
|
@@ -1509,7 +1534,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1509
1534
|
persistedVideoProvider !== 'file_storage');
|
|
1510
1535
|
const canRequeueSavedOriginalVideo =
|
|
1511
1536
|
Boolean(originalVideoResource?.fileId) && !isOriginalVideoUploadBlocked;
|
|
1512
|
-
const isProfileVideoUploadBlocked = isConversionJobActive;
|
|
1513
1537
|
const currentQueueJobId =
|
|
1514
1538
|
focusedPipelineJob?.id ?? transcriptionJobId ?? conversionJobId;
|
|
1515
1539
|
const isTranscriptionJobActive = transcriptionJob
|
|
@@ -1550,18 +1574,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1550
1574
|
},
|
|
1551
1575
|
]
|
|
1552
1576
|
: []),
|
|
1553
|
-
...courseVideoProfiles.flatMap((profile) => {
|
|
1554
|
-
const resource = profileVideoResources.get(profile.id);
|
|
1555
|
-
if (!resource) return [];
|
|
1556
|
-
|
|
1557
|
-
return [
|
|
1558
|
-
{
|
|
1559
|
-
key: `profile:${profile.id}`,
|
|
1560
|
-
label: profile.name,
|
|
1561
|
-
resource,
|
|
1562
|
-
},
|
|
1563
|
-
];
|
|
1564
|
-
}),
|
|
1565
1577
|
];
|
|
1566
1578
|
const activeFramePreviewSource =
|
|
1567
1579
|
framePreviewSources.find(
|
|
@@ -1785,6 +1797,24 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1785
1797
|
}
|
|
1786
1798
|
}
|
|
1787
1799
|
|
|
1800
|
+
// 🆕 Handler para atualizar tipo de recurso
|
|
1801
|
+
function handleUpdateResourceType(resourceId: string, newType: string) {
|
|
1802
|
+
if (!lesson?.id) return;
|
|
1803
|
+
|
|
1804
|
+
setIsUpdatingResourceType(resourceId);
|
|
1805
|
+
updateResourceTypeMutation.mutate(
|
|
1806
|
+
{
|
|
1807
|
+
lessonId: lesson.id,
|
|
1808
|
+
resourceId,
|
|
1809
|
+
newType,
|
|
1810
|
+
newTypeLabel: getResourceTypeLabel(t, newType),
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
onSettled: () => setIsUpdatingResourceType(null),
|
|
1814
|
+
}
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1788
1818
|
async function openVideoPreview(res: Resource) {
|
|
1789
1819
|
const requestId = ++videoPreviewRequestIdRef.current;
|
|
1790
1820
|
setVideoPreviewResource(res);
|
|
@@ -2302,63 +2332,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2302
2332
|
return frameAssetMetadataById[frame.id]?.sizeLabel ?? '—';
|
|
2303
2333
|
}
|
|
2304
2334
|
|
|
2305
|
-
async function handleVideoProfileFile(profileId: number, file: File) {
|
|
2306
|
-
if (file.size > MAX_VIDEO_UPLOAD_SIZE_BYTES) {
|
|
2307
|
-
const message = t('lessonForm.videoUploadMaxSizeError', {
|
|
2308
|
-
size: '100MB',
|
|
2309
|
-
});
|
|
2310
|
-
setVideoUploadError(message);
|
|
2311
|
-
toast.error(message);
|
|
2312
|
-
return;
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
setVideoUploadError(null);
|
|
2316
|
-
setProfileUploadProgress((prev) => ({ ...prev, [profileId]: 0 }));
|
|
2317
|
-
try {
|
|
2318
|
-
const uploaded = await uploadFile(request, file, 'lms/lessons/videos', {
|
|
2319
|
-
onUploadProgress: (event) => {
|
|
2320
|
-
const total = event.total ?? 0;
|
|
2321
|
-
const progress =
|
|
2322
|
-
total > 0 ? Math.round((event.loaded / total) * 100) : 0;
|
|
2323
|
-
setProfileUploadProgress((prev) => ({
|
|
2324
|
-
...prev,
|
|
2325
|
-
[profileId]: progress,
|
|
2326
|
-
}));
|
|
2327
|
-
},
|
|
2328
|
-
});
|
|
2329
|
-
const type = videoProfileResourceType(profileId);
|
|
2330
|
-
const resource: Resource = {
|
|
2331
|
-
id: `new-${uploaded.id}`,
|
|
2332
|
-
fileId: uploaded.id,
|
|
2333
|
-
name: file.name,
|
|
2334
|
-
size: formatFileSize(file.size),
|
|
2335
|
-
type,
|
|
2336
|
-
public: false,
|
|
2337
|
-
uploadedAt: new Date().toISOString(),
|
|
2338
|
-
url: undefined,
|
|
2339
|
-
};
|
|
2340
|
-
setLocalResources((prev) => [
|
|
2341
|
-
...prev.filter((item) => item.type !== type),
|
|
2342
|
-
resource,
|
|
2343
|
-
]);
|
|
2344
|
-
setResourcesDirty(true);
|
|
2345
|
-
} catch {
|
|
2346
|
-
toast.error(t('questionEditor.videoUploadFailed', { count: 1 }));
|
|
2347
|
-
} finally {
|
|
2348
|
-
setProfileUploadProgress((prev) => {
|
|
2349
|
-
const next = { ...prev };
|
|
2350
|
-
delete next[profileId];
|
|
2351
|
-
return next;
|
|
2352
|
-
});
|
|
2353
|
-
}
|
|
2354
|
-
}
|
|
2355
|
-
|
|
2356
2335
|
async function handleOriginalVideoFile(file: File) {
|
|
2357
|
-
if (!isVideoConversionEnabled) {
|
|
2358
|
-
toast.error(t('lessonForm.videoConversionFailed'));
|
|
2359
|
-
return;
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
2336
|
if (
|
|
2363
2337
|
watchedVideoProvider === 'file_storage' &&
|
|
2364
2338
|
persistedVideoProvider !== 'file_storage'
|
|
@@ -2485,6 +2459,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2485
2459
|
videoConversionJobId: conversionJobId ?? lesson?.videoConversionJobId,
|
|
2486
2460
|
resources: localResources,
|
|
2487
2461
|
instructorIds: selectedInstructorIds.map(Number),
|
|
2462
|
+
linkedExam: values.linkedExam ?? undefined,
|
|
2488
2463
|
},
|
|
2489
2464
|
},
|
|
2490
2465
|
{
|
|
@@ -2602,28 +2577,43 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2602
2577
|
setQSheetErrors(errors);
|
|
2603
2578
|
return;
|
|
2604
2579
|
}
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
title: qSheetStatement.replace(/<[^>]+>/g, '').slice(0, 80),
|
|
2608
|
-
type: qSheetType,
|
|
2580
|
+
|
|
2581
|
+
const dto = {
|
|
2609
2582
|
statement: qSheetStatement,
|
|
2583
|
+
questionType: qSheetType,
|
|
2610
2584
|
points: qSheetPoints,
|
|
2611
2585
|
alternatives:
|
|
2612
2586
|
qSheetType === 'multiple_choice' || qSheetType === 'true_false'
|
|
2613
|
-
? qSheetAlts
|
|
2587
|
+
? qSheetAlts.map((a) => ({ text: a.texto, isCorrect: a.correta }))
|
|
2614
2588
|
: undefined,
|
|
2615
2589
|
fillBlankAnswers:
|
|
2616
|
-
qSheetType === 'fill_blank'
|
|
2617
|
-
|
|
2590
|
+
qSheetType === 'fill_blank'
|
|
2591
|
+
? qSheetFillBlanks.map((f) => ({
|
|
2592
|
+
answer: f.answer,
|
|
2593
|
+
alternatives: f.alternativesText
|
|
2594
|
+
? f.alternativesText.split(',').map((s) => s.trim()).filter(Boolean)
|
|
2595
|
+
: undefined,
|
|
2596
|
+
}))
|
|
2597
|
+
: undefined,
|
|
2598
|
+
matchingPairs:
|
|
2599
|
+
qSheetType === 'matching'
|
|
2600
|
+
? qSheetPairs.map((p) => ({ id: p.id, leftText: p.leftText, rightText: p.rightText }))
|
|
2601
|
+
: undefined,
|
|
2618
2602
|
};
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2603
|
+
|
|
2604
|
+
createQuestionMutation.mutate(dto, {
|
|
2605
|
+
onSuccess: (result) => {
|
|
2606
|
+
const mock = apiQuestionToMock(result);
|
|
2607
|
+
setSelectedQuestion(mock);
|
|
2608
|
+
form.setValue('linkedExam', String(result.id), { shouldDirty: true });
|
|
2609
|
+
setQuestionSheetOpen(false);
|
|
2610
|
+
toast.success(
|
|
2611
|
+
editingQuestion
|
|
2612
|
+
? t('questionEditor.updated')
|
|
2613
|
+
: t('questionEditor.created')
|
|
2614
|
+
);
|
|
2615
|
+
},
|
|
2616
|
+
});
|
|
2627
2617
|
}
|
|
2628
2618
|
|
|
2629
2619
|
function handleDelete() {
|
|
@@ -2713,7 +2703,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2713
2703
|
{/* ── Tabs ─────────────────────────────────────────────────────────── */}
|
|
2714
2704
|
<Tabs
|
|
2715
2705
|
value={activeTab}
|
|
2716
|
-
onValueChange={(value) =>
|
|
2706
|
+
onValueChange={(value) => {
|
|
2707
|
+
setActiveTab(value as LessonEditorTab);
|
|
2708
|
+
onTabChange?.(value);
|
|
2709
|
+
}}
|
|
2717
2710
|
className="flex flex-col flex-1 min-h-0 min-w-0"
|
|
2718
2711
|
>
|
|
2719
2712
|
<TabsList className="mt-0 h-auto w-full justify-start shrink-0 rounded-none border-b bg-muted/50 px-2 py-1 overflow-x-auto overflow-y-hidden whitespace-nowrap sm:px-3">
|
|
@@ -2767,6 +2760,12 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2767
2760
|
>
|
|
2768
2761
|
{t('lessonForm.tabResources')}
|
|
2769
2762
|
</TabsTrigger>
|
|
2763
|
+
<TabsTrigger
|
|
2764
|
+
value="xp"
|
|
2765
|
+
className="h-6 px-2 text-[11px] shrink-0 sm:h-7 sm:px-2.5 sm:text-xs"
|
|
2766
|
+
>
|
|
2767
|
+
XP
|
|
2768
|
+
</TabsTrigger>
|
|
2770
2769
|
</TabsList>
|
|
2771
2770
|
|
|
2772
2771
|
{/* ── Tab Dados ────────────────────────────────────────────────── */}
|
|
@@ -2890,48 +2889,57 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2890
2889
|
</CardTitle>
|
|
2891
2890
|
</CardHeader>
|
|
2892
2891
|
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
2893
|
-
<div
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
{
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2892
|
+
<div
|
|
2893
|
+
className={cn(
|
|
2894
|
+
'grid grid-cols-1 gap-2',
|
|
2895
|
+
watchedType === 'video' && !watchedPublished
|
|
2896
|
+
? 'sm:grid-cols-2'
|
|
2897
|
+
: 'sm:grid-cols-1'
|
|
2898
|
+
)}
|
|
2899
|
+
>
|
|
2900
|
+
{watchedType === 'video' && !watchedPublished && (
|
|
2901
|
+
<FormField
|
|
2902
|
+
control={form.control}
|
|
2903
|
+
name="status"
|
|
2904
|
+
render={({ field }) => (
|
|
2905
|
+
<FormItem>
|
|
2906
|
+
<FormLabel className="text-xs">
|
|
2907
|
+
{t('questionEditor.productionStatus')}
|
|
2908
|
+
</FormLabel>
|
|
2909
|
+
<Select
|
|
2910
|
+
value={field.value}
|
|
2911
|
+
onValueChange={field.onChange}
|
|
2912
|
+
>
|
|
2913
|
+
<FormControl>
|
|
2914
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
2915
|
+
<SelectValue />
|
|
2916
|
+
</SelectTrigger>
|
|
2917
|
+
</FormControl>
|
|
2918
|
+
<SelectContent>
|
|
2919
|
+
{(
|
|
2920
|
+
Object.entries(statusLabels) as [
|
|
2921
|
+
LessonStatus,
|
|
2922
|
+
string,
|
|
2923
|
+
][]
|
|
2924
|
+
).map(([val, lbl]) => (
|
|
2925
|
+
<SelectItem key={val} value={val}>
|
|
2926
|
+
<span
|
|
2927
|
+
className={cn(
|
|
2928
|
+
'text-xs px-1.5 py-0.5 rounded',
|
|
2929
|
+
STATUS_COLORS[val]
|
|
2930
|
+
)}
|
|
2931
|
+
>
|
|
2932
|
+
{lbl}
|
|
2933
|
+
</span>
|
|
2934
|
+
</SelectItem>
|
|
2935
|
+
))}
|
|
2936
|
+
</SelectContent>
|
|
2937
|
+
</Select>
|
|
2938
|
+
<FormMessage className="text-xs" />
|
|
2939
|
+
</FormItem>
|
|
2940
|
+
)}
|
|
2941
|
+
/>
|
|
2942
|
+
)}
|
|
2935
2943
|
|
|
2936
2944
|
<FormField
|
|
2937
2945
|
control={form.control}
|
|
@@ -3211,20 +3219,25 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3211
3219
|
<CardContent className="px-3 pb-2 flex flex-col gap-2">
|
|
3212
3220
|
<FormField
|
|
3213
3221
|
control={form.control}
|
|
3214
|
-
name="
|
|
3222
|
+
name="linkedExam"
|
|
3215
3223
|
render={({ field }) => (
|
|
3216
3224
|
<FormItem>
|
|
3217
3225
|
<FormControl>
|
|
3218
|
-
<EntityPicker<
|
|
3226
|
+
<EntityPicker<ApiQuestion>
|
|
3219
3227
|
value={field.value ?? null}
|
|
3220
3228
|
onChange={(val) => {
|
|
3221
3229
|
field.onChange(val);
|
|
3222
3230
|
const found =
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3231
|
+
(questionsData?.data ?? []).find(
|
|
3232
|
+
(q) => String(q.id) === val
|
|
3233
|
+
) ?? null;
|
|
3234
|
+
setSelectedQuestion(found ? apiQuestionToMock(found) : null);
|
|
3226
3235
|
}}
|
|
3227
|
-
placeholder={
|
|
3236
|
+
placeholder={
|
|
3237
|
+
questionsLoading
|
|
3238
|
+
? t('questionEditor.loadingQuestions')
|
|
3239
|
+
: t('questionEditor.selectQuestion')
|
|
3240
|
+
}
|
|
3228
3241
|
searchPlaceholder={t(
|
|
3229
3242
|
'questionEditor.searchQuestion'
|
|
3230
3243
|
)}
|
|
@@ -3232,11 +3245,14 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3232
3245
|
'questionEditor.noQuestionsFound'
|
|
3233
3246
|
)}
|
|
3234
3247
|
entityLabel={t('questionEditor.questionEntity')}
|
|
3235
|
-
options={
|
|
3236
|
-
getOptionValue={(o) => o.id}
|
|
3237
|
-
getOptionLabel={(o) =>
|
|
3248
|
+
options={questionsData?.data ?? []}
|
|
3249
|
+
getOptionValue={(o) => String(o.id)}
|
|
3250
|
+
getOptionLabel={(o) =>
|
|
3251
|
+
o.statement.replace(/<[^>]+>/g, '').slice(0, 80)
|
|
3252
|
+
}
|
|
3238
3253
|
getOptionDescription={(o) =>
|
|
3239
|
-
questionTypeLabels[o.
|
|
3254
|
+
questionTypeLabels[o.questionType as QuestionType] ??
|
|
3255
|
+
o.questionType
|
|
3240
3256
|
}
|
|
3241
3257
|
/>
|
|
3242
3258
|
</FormControl>
|
|
@@ -3361,181 +3377,140 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3361
3377
|
</p>
|
|
3362
3378
|
) : null}
|
|
3363
3379
|
|
|
3364
|
-
|
|
3365
|
-
<
|
|
3366
|
-
<
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
<
|
|
3372
|
-
<div className="
|
|
3373
|
-
<div className="flex items-
|
|
3374
|
-
<
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
<div className="
|
|
3378
|
-
<div className="
|
|
3379
|
-
<
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
{
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
<
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
'lessonForm.playVideoAria',
|
|
3417
|
-
{
|
|
3418
|
-
name: originalVideoResource.name,
|
|
3419
|
-
}
|
|
3420
|
-
)}
|
|
3421
|
-
>
|
|
3422
|
-
{isResolvingVideoPreview ? (
|
|
3423
|
-
<Loader2 className="size-3 animate-spin" />
|
|
3424
|
-
) : (
|
|
3425
|
-
<Play className="size-3" />
|
|
3426
|
-
)}
|
|
3427
|
-
</Button>
|
|
3428
|
-
</IconActionTooltip>
|
|
3429
|
-
<IconActionTooltip
|
|
3430
|
-
label={t(
|
|
3431
|
-
'lessonForm.downloadVideoAria',
|
|
3380
|
+
<Card className="bg-muted/20 py-2 gap-2 order-3">
|
|
3381
|
+
<CardHeader className="px-3 pt-2 pb-1">
|
|
3382
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
3383
|
+
{t('lessonForm.originalVideoTitle')}
|
|
3384
|
+
</CardTitle>
|
|
3385
|
+
</CardHeader>
|
|
3386
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
3387
|
+
<div className="rounded-lg border bg-background/90 p-3 shadow-sm">
|
|
3388
|
+
<div className="flex items-start gap-3">
|
|
3389
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-blue-600">
|
|
3390
|
+
<Video className="size-4" />
|
|
3391
|
+
</div>
|
|
3392
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
3393
|
+
<div className="flex items-start justify-between gap-2">
|
|
3394
|
+
<div className="min-w-0">
|
|
3395
|
+
<p className="truncate text-sm font-medium">
|
|
3396
|
+
{originalVideoResource
|
|
3397
|
+
? originalVideoResource.name
|
|
3398
|
+
: t('lessonForm.originalVideoTitle')}
|
|
3399
|
+
</p>
|
|
3400
|
+
<p className="text-xs text-muted-foreground">
|
|
3401
|
+
{conversionJobId
|
|
3402
|
+
? t('lessonForm.videoConversionJob', {
|
|
3403
|
+
id: conversionJobId,
|
|
3404
|
+
})
|
|
3405
|
+
: t('lessonForm.originalVideoHint')}
|
|
3406
|
+
</p>
|
|
3407
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
3408
|
+
{t('lessonForm.originalVideoPurpose')}
|
|
3409
|
+
</p>
|
|
3410
|
+
</div>
|
|
3411
|
+
{originalVideoResource && (
|
|
3412
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
3413
|
+
<IconActionTooltip
|
|
3414
|
+
label={t('lessonForm.playVideoAria', {
|
|
3415
|
+
name: originalVideoResource.name,
|
|
3416
|
+
})}
|
|
3417
|
+
asWrapper={isResolvingVideoPreview}
|
|
3418
|
+
>
|
|
3419
|
+
<Button
|
|
3420
|
+
type="button"
|
|
3421
|
+
variant="ghost"
|
|
3422
|
+
size="icon"
|
|
3423
|
+
className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-emerald-600"
|
|
3424
|
+
disabled={isResolvingVideoPreview}
|
|
3425
|
+
onClick={() =>
|
|
3426
|
+
void openVideoPreview(
|
|
3427
|
+
originalVideoResource
|
|
3428
|
+
)
|
|
3429
|
+
}
|
|
3430
|
+
aria-label={t(
|
|
3431
|
+
'lessonForm.playVideoAria',
|
|
3432
3432
|
{
|
|
3433
3433
|
name: originalVideoResource.name,
|
|
3434
3434
|
}
|
|
3435
3435
|
)}
|
|
3436
|
-
asWrapper={isDownloadingOriginalVideo}
|
|
3437
3436
|
>
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
)
|
|
3450
|
-
}
|
|
3451
|
-
aria-label={t(
|
|
3452
|
-
'lessonForm.downloadVideoAria',
|
|
3453
|
-
{
|
|
3454
|
-
name: originalVideoResource.name,
|
|
3455
|
-
}
|
|
3456
|
-
)}
|
|
3457
|
-
>
|
|
3458
|
-
{isDownloadingOriginalVideo ? (
|
|
3459
|
-
<Loader2 className="size-3 animate-spin" />
|
|
3460
|
-
) : (
|
|
3461
|
-
<Download className="size-3" />
|
|
3462
|
-
)}
|
|
3463
|
-
</Button>
|
|
3464
|
-
</IconActionTooltip>
|
|
3465
|
-
<IconActionTooltip
|
|
3466
|
-
label={t('lessonForm.openVideoAria', {
|
|
3437
|
+
{isResolvingVideoPreview ? (
|
|
3438
|
+
<Loader2 className="size-3 animate-spin" />
|
|
3439
|
+
) : (
|
|
3440
|
+
<Play className="size-3" />
|
|
3441
|
+
)}
|
|
3442
|
+
</Button>
|
|
3443
|
+
</IconActionTooltip>
|
|
3444
|
+
<IconActionTooltip
|
|
3445
|
+
label={t(
|
|
3446
|
+
'lessonForm.downloadVideoAria',
|
|
3447
|
+
{
|
|
3467
3448
|
name: originalVideoResource.name,
|
|
3468
|
-
}
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
variant="ghost"
|
|
3473
|
-
size="icon"
|
|
3474
|
-
className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
|
|
3475
|
-
onClick={() =>
|
|
3476
|
-
void openResource(
|
|
3477
|
-
originalVideoResource
|
|
3478
|
-
)
|
|
3479
|
-
}
|
|
3480
|
-
aria-label={t(
|
|
3481
|
-
'lessonForm.openVideoAria',
|
|
3482
|
-
{
|
|
3483
|
-
name: originalVideoResource.name,
|
|
3484
|
-
}
|
|
3485
|
-
)}
|
|
3486
|
-
>
|
|
3487
|
-
<ExternalLink className="size-3" />
|
|
3488
|
-
</Button>
|
|
3489
|
-
</IconActionTooltip>
|
|
3490
|
-
</div>
|
|
3491
|
-
)}
|
|
3492
|
-
</div>
|
|
3493
|
-
{originalVideoResource?.size ? (
|
|
3494
|
-
<div className="inline-flex rounded-full bg-muted px-2 py-0.5 text-[0.65rem] font-medium text-muted-foreground">
|
|
3495
|
-
{originalVideoResource.size}
|
|
3496
|
-
</div>
|
|
3497
|
-
) : null}
|
|
3498
|
-
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
3499
|
-
{originalVideoResource ? (
|
|
3500
|
-
<>
|
|
3449
|
+
}
|
|
3450
|
+
)}
|
|
3451
|
+
asWrapper={isDownloadingOriginalVideo}
|
|
3452
|
+
>
|
|
3501
3453
|
<Button
|
|
3502
3454
|
type="button"
|
|
3503
|
-
variant="
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
}
|
|
3455
|
+
variant="ghost"
|
|
3456
|
+
size="icon"
|
|
3457
|
+
className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-amber-600"
|
|
3458
|
+
disabled={isDownloadingOriginalVideo}
|
|
3508
3459
|
onClick={() =>
|
|
3509
|
-
|
|
3460
|
+
void handleResourceDownload(
|
|
3461
|
+
originalVideoResource
|
|
3462
|
+
)
|
|
3510
3463
|
}
|
|
3464
|
+
aria-label={t(
|
|
3465
|
+
'lessonForm.downloadVideoAria',
|
|
3466
|
+
{
|
|
3467
|
+
name: originalVideoResource.name,
|
|
3468
|
+
}
|
|
3469
|
+
)}
|
|
3511
3470
|
>
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3471
|
+
{isDownloadingOriginalVideo ? (
|
|
3472
|
+
<Loader2 className="size-3 animate-spin" />
|
|
3473
|
+
) : (
|
|
3474
|
+
<Download className="size-3" />
|
|
3515
3475
|
)}
|
|
3516
3476
|
</Button>
|
|
3477
|
+
</IconActionTooltip>
|
|
3478
|
+
<IconActionTooltip
|
|
3479
|
+
label={t('lessonForm.openVideoAria', {
|
|
3480
|
+
name: originalVideoResource.name,
|
|
3481
|
+
})}
|
|
3482
|
+
>
|
|
3517
3483
|
<Button
|
|
3518
3484
|
type="button"
|
|
3519
|
-
variant="
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
!canRequeueSavedOriginalVideo
|
|
3523
|
-
}
|
|
3485
|
+
variant="ghost"
|
|
3486
|
+
size="icon"
|
|
3487
|
+
className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
|
|
3524
3488
|
onClick={() =>
|
|
3525
|
-
void
|
|
3489
|
+
void openResource(
|
|
3490
|
+
originalVideoResource
|
|
3491
|
+
)
|
|
3526
3492
|
}
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
)}
|
|
3533
|
-
{t(
|
|
3534
|
-
'lessonForm.retryConversionWithSavedOriginal'
|
|
3493
|
+
aria-label={t(
|
|
3494
|
+
'lessonForm.openVideoAria',
|
|
3495
|
+
{
|
|
3496
|
+
name: originalVideoResource.name,
|
|
3497
|
+
}
|
|
3535
3498
|
)}
|
|
3499
|
+
>
|
|
3500
|
+
<ExternalLink className="size-3" />
|
|
3536
3501
|
</Button>
|
|
3537
|
-
|
|
3538
|
-
|
|
3502
|
+
</IconActionTooltip>
|
|
3503
|
+
</div>
|
|
3504
|
+
)}
|
|
3505
|
+
</div>
|
|
3506
|
+
{originalVideoResource?.size ? (
|
|
3507
|
+
<div className="inline-flex rounded-full bg-muted px-2 py-0.5 text-[0.65rem] font-medium text-muted-foreground">
|
|
3508
|
+
{originalVideoResource.size}
|
|
3509
|
+
</div>
|
|
3510
|
+
) : null}
|
|
3511
|
+
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
3512
|
+
{originalVideoResource ? (
|
|
3513
|
+
<>
|
|
3539
3514
|
<Button
|
|
3540
3515
|
type="button"
|
|
3541
3516
|
variant="secondary"
|
|
@@ -3547,50 +3522,83 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3547
3522
|
>
|
|
3548
3523
|
<UploadCloud className="size-3.5 mr-1" />
|
|
3549
3524
|
{t(
|
|
3550
|
-
'lessonForm.
|
|
3525
|
+
'lessonForm.replaceOriginalForConversion'
|
|
3551
3526
|
)}
|
|
3552
3527
|
</Button>
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3528
|
+
<Button
|
|
3529
|
+
type="button"
|
|
3530
|
+
variant="outline"
|
|
3531
|
+
className="h-8 px-3 text-xs"
|
|
3532
|
+
disabled={!canRequeueSavedOriginalVideo}
|
|
3533
|
+
onClick={() =>
|
|
3534
|
+
void handleRequeueOriginalVideo()
|
|
3535
|
+
}
|
|
3536
|
+
>
|
|
3537
|
+
{isRequeueingOriginalVideo ? (
|
|
3538
|
+
<Loader2 className="size-3.5 mr-1 animate-spin" />
|
|
3539
|
+
) : (
|
|
3540
|
+
<RefreshCw className="size-3.5 mr-1" />
|
|
3541
|
+
)}
|
|
3542
|
+
{t(
|
|
3543
|
+
'lessonForm.retryConversionWithSavedOriginal'
|
|
3544
|
+
)}
|
|
3545
|
+
</Button>
|
|
3546
|
+
</>
|
|
3547
|
+
) : (
|
|
3548
|
+
<Button
|
|
3549
|
+
type="button"
|
|
3550
|
+
variant="secondary"
|
|
3551
|
+
className="h-8 px-3 text-xs"
|
|
3552
|
+
disabled={isOriginalVideoUploadBlocked}
|
|
3553
|
+
onClick={() =>
|
|
3554
|
+
originalVideoInputRef.current?.click()
|
|
3555
|
+
}
|
|
3556
|
+
>
|
|
3557
|
+
<UploadCloud className="size-3.5 mr-1" />
|
|
3558
|
+
{t(
|
|
3559
|
+
'lessonForm.uploadOriginalForConversion'
|
|
3560
|
+
)}
|
|
3561
|
+
</Button>
|
|
3562
|
+
)}
|
|
3563
|
+
<span className="text-[0.65rem] text-muted-foreground">
|
|
3564
|
+
{isConversionJobActive
|
|
3565
|
+
? t(
|
|
3566
|
+
'lessonForm.videoUploadBlockedWhileProcessing'
|
|
3567
|
+
)
|
|
3568
|
+
: isConversionJobStatusResolving
|
|
3569
|
+
? t('lessonForm.videoJobStateLoading')
|
|
3570
|
+
: t('lessonForm.originalVideoHint')}
|
|
3571
|
+
</span>
|
|
3575
3572
|
</div>
|
|
3573
|
+
{originalUploadProgress !== null ? (
|
|
3574
|
+
<div className="space-y-1 pt-1">
|
|
3575
|
+
<Progress
|
|
3576
|
+
value={originalUploadProgress}
|
|
3577
|
+
className="h-1.5"
|
|
3578
|
+
/>
|
|
3579
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
3580
|
+
{originalUploadProgress}%
|
|
3581
|
+
</p>
|
|
3582
|
+
</div>
|
|
3583
|
+
) : null}
|
|
3576
3584
|
</div>
|
|
3577
|
-
<input
|
|
3578
|
-
ref={originalVideoInputRef}
|
|
3579
|
-
type="file"
|
|
3580
|
-
accept="video/*"
|
|
3581
|
-
className="hidden"
|
|
3582
|
-
onChange={(event) => {
|
|
3583
|
-
const file = event.target.files?.[0];
|
|
3584
|
-
if (file && !isOriginalVideoUploadBlocked) {
|
|
3585
|
-
void handleOriginalVideoFile(file);
|
|
3586
|
-
}
|
|
3587
|
-
event.target.value = '';
|
|
3588
|
-
}}
|
|
3589
|
-
/>
|
|
3590
3585
|
</div>
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3586
|
+
<input
|
|
3587
|
+
ref={originalVideoInputRef}
|
|
3588
|
+
type="file"
|
|
3589
|
+
accept="video/*"
|
|
3590
|
+
className="hidden"
|
|
3591
|
+
onChange={(event) => {
|
|
3592
|
+
const file = event.target.files?.[0];
|
|
3593
|
+
if (file && !isOriginalVideoUploadBlocked) {
|
|
3594
|
+
void handleOriginalVideoFile(file);
|
|
3595
|
+
}
|
|
3596
|
+
event.target.value = '';
|
|
3597
|
+
}}
|
|
3598
|
+
/>
|
|
3599
|
+
</div>
|
|
3600
|
+
</CardContent>
|
|
3601
|
+
</Card>
|
|
3594
3602
|
|
|
3595
3603
|
{conversionJobId && !shouldHidePipelineCard ? (
|
|
3596
3604
|
<Card className="bg-muted/20 py-2 gap-2 order-4">
|
|
@@ -3947,259 +3955,43 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3947
3955
|
<Card className="bg-muted/20 py-2 gap-2 order-2">
|
|
3948
3956
|
<CardHeader className="px-3 pt-2 pb-1">
|
|
3949
3957
|
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
3950
|
-
{t('lessonForm.
|
|
3958
|
+
{t('lessonForm.hlsStatusTitle')}
|
|
3951
3959
|
</CardTitle>
|
|
3952
3960
|
</CardHeader>
|
|
3953
|
-
<CardContent className="px-3 pb-2
|
|
3954
|
-
{
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
<>
|
|
3982
|
-
{isConversionJobActive ? (
|
|
3961
|
+
<CardContent className="px-3 pb-2">
|
|
3962
|
+
{(() => {
|
|
3963
|
+
const hlsResource = localResources.find(
|
|
3964
|
+
(r) => r.type === 'video_hls'
|
|
3965
|
+
);
|
|
3966
|
+
if (hlsResource) {
|
|
3967
|
+
return (
|
|
3968
|
+
<div className="flex items-center gap-2">
|
|
3969
|
+
<div className="size-2 rounded-full bg-emerald-500 shrink-0" />
|
|
3970
|
+
<p className="text-xs text-emerald-700 dark:text-emerald-400">
|
|
3971
|
+
{t('lessonForm.hlsStatusReady')}
|
|
3972
|
+
</p>
|
|
3973
|
+
</div>
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
if (isConversionJobActive) {
|
|
3977
|
+
return (
|
|
3978
|
+
<div className="flex items-center gap-2">
|
|
3979
|
+
<Loader2 className="size-3.5 animate-spin text-muted-foreground shrink-0" />
|
|
3980
|
+
<p className="text-xs text-muted-foreground">
|
|
3981
|
+
{t('lessonForm.hlsStatusProcessing')}
|
|
3982
|
+
</p>
|
|
3983
|
+
</div>
|
|
3984
|
+
);
|
|
3985
|
+
}
|
|
3986
|
+
return (
|
|
3987
|
+
<div className="flex items-center gap-2">
|
|
3988
|
+
<div className="size-2 rounded-full bg-muted-foreground/40 shrink-0" />
|
|
3983
3989
|
<p className="text-xs text-muted-foreground">
|
|
3984
|
-
{t(
|
|
3985
|
-
'lessonForm.videoProfilesLockedWhileProcessing'
|
|
3986
|
-
)}
|
|
3990
|
+
{t('lessonForm.hlsStatusPending')}
|
|
3987
3991
|
</p>
|
|
3988
|
-
) : null}
|
|
3989
|
-
<div className="flex flex-col gap-1">
|
|
3990
|
-
{courseVideoProfiles.map((profile) => {
|
|
3991
|
-
const res = profileVideoResources.get(
|
|
3992
|
-
profile.id
|
|
3993
|
-
);
|
|
3994
|
-
const hasVideo = Boolean(res);
|
|
3995
|
-
const isDownloadingResource = res
|
|
3996
|
-
? downloadingResourceKeys.has(
|
|
3997
|
-
String(res.fileId ?? res.id)
|
|
3998
|
-
)
|
|
3999
|
-
: false;
|
|
4000
|
-
const currentUploadProgress =
|
|
4001
|
-
profileUploadProgress[profile.id];
|
|
4002
|
-
const inputId = `lesson-video-profile-${profile.id}`;
|
|
4003
|
-
|
|
4004
|
-
return (
|
|
4005
|
-
<div
|
|
4006
|
-
key={profile.id}
|
|
4007
|
-
className="flex items-center gap-2 rounded-md border bg-background/80 px-2.5 py-2"
|
|
4008
|
-
>
|
|
4009
|
-
<Video
|
|
4010
|
-
className={cn(
|
|
4011
|
-
'size-3.5 shrink-0',
|
|
4012
|
-
hasVideo
|
|
4013
|
-
? 'text-emerald-600'
|
|
4014
|
-
: 'text-blue-600'
|
|
4015
|
-
)}
|
|
4016
|
-
/>
|
|
4017
|
-
<div className="flex-1 min-w-0">
|
|
4018
|
-
<p className="text-xs truncate font-medium">
|
|
4019
|
-
{profile.name}
|
|
4020
|
-
</p>
|
|
4021
|
-
{res ? (
|
|
4022
|
-
(() => {
|
|
4023
|
-
const metadata =
|
|
4024
|
-
resolveResourceMetadata(res);
|
|
4025
|
-
|
|
4026
|
-
return (
|
|
4027
|
-
<p className="text-[0.65rem] text-muted-foreground">
|
|
4028
|
-
{`${metadata.sizeLabel} · ${metadata.uploadedAtLabel}`}
|
|
4029
|
-
</p>
|
|
4030
|
-
);
|
|
4031
|
-
})()
|
|
4032
|
-
) : (
|
|
4033
|
-
<Badge className="mt-1 inline-flex items-center gap-1 border border-blue-200 bg-blue-100 text-blue-700 hover:bg-blue-100 dark:border-blue-800 dark:bg-blue-950/40 dark:text-blue-200 dark:hover:bg-blue-950/50">
|
|
4034
|
-
{isConversionJobActive ? (
|
|
4035
|
-
<Loader2 className="size-3 animate-spin" />
|
|
4036
|
-
) : (
|
|
4037
|
-
<Clock className="size-3" />
|
|
4038
|
-
)}
|
|
4039
|
-
{t('lessonForm.awaitingConversion')}
|
|
4040
|
-
</Badge>
|
|
4041
|
-
)}
|
|
4042
|
-
{currentUploadProgress !== undefined ? (
|
|
4043
|
-
<div className="mt-1 space-y-1">
|
|
4044
|
-
<Progress
|
|
4045
|
-
value={currentUploadProgress}
|
|
4046
|
-
className="h-1.5"
|
|
4047
|
-
/>
|
|
4048
|
-
<p className="text-[0.65rem] text-muted-foreground">
|
|
4049
|
-
{currentUploadProgress}%
|
|
4050
|
-
</p>
|
|
4051
|
-
</div>
|
|
4052
|
-
) : null}
|
|
4053
|
-
</div>
|
|
4054
|
-
<input
|
|
4055
|
-
id={inputId}
|
|
4056
|
-
type="file"
|
|
4057
|
-
accept="video/*"
|
|
4058
|
-
className="hidden"
|
|
4059
|
-
onChange={(event) => {
|
|
4060
|
-
const file = event.target.files?.[0];
|
|
4061
|
-
if (
|
|
4062
|
-
file &&
|
|
4063
|
-
!isProfileVideoUploadBlocked
|
|
4064
|
-
) {
|
|
4065
|
-
void handleVideoProfileFile(
|
|
4066
|
-
profile.id,
|
|
4067
|
-
file
|
|
4068
|
-
);
|
|
4069
|
-
}
|
|
4070
|
-
event.target.value = '';
|
|
4071
|
-
}}
|
|
4072
|
-
/>
|
|
4073
|
-
<Button
|
|
4074
|
-
type="button"
|
|
4075
|
-
variant="outline"
|
|
4076
|
-
size="sm"
|
|
4077
|
-
className="h-7 px-2 text-xs"
|
|
4078
|
-
disabled={
|
|
4079
|
-
currentUploadProgress !== undefined ||
|
|
4080
|
-
isProfileVideoUploadBlocked
|
|
4081
|
-
}
|
|
4082
|
-
onClick={() =>
|
|
4083
|
-
document
|
|
4084
|
-
.getElementById(inputId)
|
|
4085
|
-
?.click()
|
|
4086
|
-
}
|
|
4087
|
-
>
|
|
4088
|
-
<UploadCloud className="size-3 mr-1" />
|
|
4089
|
-
{res
|
|
4090
|
-
? t('lessonForm.replaceVideo')
|
|
4091
|
-
: t('lessonForm.upload')}
|
|
4092
|
-
</Button>
|
|
4093
|
-
{res && (
|
|
4094
|
-
<>
|
|
4095
|
-
<IconActionTooltip
|
|
4096
|
-
label={t(
|
|
4097
|
-
'lessonForm.playVideoAria',
|
|
4098
|
-
{ name: res.name }
|
|
4099
|
-
)}
|
|
4100
|
-
asWrapper={isResolvingVideoPreview}
|
|
4101
|
-
>
|
|
4102
|
-
<Button
|
|
4103
|
-
type="button"
|
|
4104
|
-
variant="ghost"
|
|
4105
|
-
size="icon"
|
|
4106
|
-
className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-emerald-600"
|
|
4107
|
-
disabled={isResolvingVideoPreview}
|
|
4108
|
-
onClick={() =>
|
|
4109
|
-
void openVideoPreview(res)
|
|
4110
|
-
}
|
|
4111
|
-
aria-label={t(
|
|
4112
|
-
'lessonForm.playVideoAria',
|
|
4113
|
-
{ name: res.name }
|
|
4114
|
-
)}
|
|
4115
|
-
>
|
|
4116
|
-
{isResolvingVideoPreview ? (
|
|
4117
|
-
<Loader2 className="size-3 animate-spin" />
|
|
4118
|
-
) : (
|
|
4119
|
-
<Play className="size-3" />
|
|
4120
|
-
)}
|
|
4121
|
-
</Button>
|
|
4122
|
-
</IconActionTooltip>
|
|
4123
|
-
<IconActionTooltip
|
|
4124
|
-
label={t(
|
|
4125
|
-
'lessonForm.openVideoAria',
|
|
4126
|
-
{ name: res.name }
|
|
4127
|
-
)}
|
|
4128
|
-
>
|
|
4129
|
-
<Button
|
|
4130
|
-
type="button"
|
|
4131
|
-
variant="ghost"
|
|
4132
|
-
size="icon"
|
|
4133
|
-
className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
|
|
4134
|
-
onClick={() =>
|
|
4135
|
-
void openResource(res)
|
|
4136
|
-
}
|
|
4137
|
-
aria-label={t(
|
|
4138
|
-
'lessonForm.openVideoAria',
|
|
4139
|
-
{ name: res.name }
|
|
4140
|
-
)}
|
|
4141
|
-
>
|
|
4142
|
-
<ExternalLink className="size-3" />
|
|
4143
|
-
</Button>
|
|
4144
|
-
</IconActionTooltip>
|
|
4145
|
-
<IconActionTooltip
|
|
4146
|
-
label={t(
|
|
4147
|
-
'lessonForm.downloadVideoAria',
|
|
4148
|
-
{ name: res.name }
|
|
4149
|
-
)}
|
|
4150
|
-
asWrapper={isDownloadingResource}
|
|
4151
|
-
>
|
|
4152
|
-
<Button
|
|
4153
|
-
type="button"
|
|
4154
|
-
variant="ghost"
|
|
4155
|
-
size="icon"
|
|
4156
|
-
className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-amber-600"
|
|
4157
|
-
disabled={isDownloadingResource}
|
|
4158
|
-
onClick={() =>
|
|
4159
|
-
void handleResourceDownload(res)
|
|
4160
|
-
}
|
|
4161
|
-
aria-label={t(
|
|
4162
|
-
'lessonForm.downloadVideoAria',
|
|
4163
|
-
{ name: res.name }
|
|
4164
|
-
)}
|
|
4165
|
-
>
|
|
4166
|
-
{isDownloadingResource ? (
|
|
4167
|
-
<Loader2 className="size-3 animate-spin" />
|
|
4168
|
-
) : (
|
|
4169
|
-
<Download className="size-3" />
|
|
4170
|
-
)}
|
|
4171
|
-
</Button>
|
|
4172
|
-
</IconActionTooltip>
|
|
4173
|
-
<IconActionTooltip
|
|
4174
|
-
label={t(
|
|
4175
|
-
'lessonForm.removeVideoAria',
|
|
4176
|
-
{ name: res.name }
|
|
4177
|
-
)}
|
|
4178
|
-
>
|
|
4179
|
-
<Button
|
|
4180
|
-
type="button"
|
|
4181
|
-
variant="ghost"
|
|
4182
|
-
size="icon"
|
|
4183
|
-
className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-destructive"
|
|
4184
|
-
onClick={() =>
|
|
4185
|
-
void removeResource(res.id)
|
|
4186
|
-
}
|
|
4187
|
-
aria-label={t(
|
|
4188
|
-
'lessonForm.removeVideoAria',
|
|
4189
|
-
{ name: res.name }
|
|
4190
|
-
)}
|
|
4191
|
-
>
|
|
4192
|
-
<X className="size-3" />
|
|
4193
|
-
</Button>
|
|
4194
|
-
</IconActionTooltip>
|
|
4195
|
-
</>
|
|
4196
|
-
)}
|
|
4197
|
-
</div>
|
|
4198
|
-
);
|
|
4199
|
-
})}
|
|
4200
3992
|
</div>
|
|
4201
|
-
|
|
4202
|
-
)}
|
|
3993
|
+
);
|
|
3994
|
+
})()}
|
|
4203
3995
|
</CardContent>
|
|
4204
3996
|
</Card>
|
|
4205
3997
|
</>
|
|
@@ -4474,29 +4266,79 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
4474
4266
|
<CardHeader className="px-3 pt-2 pb-1">
|
|
4475
4267
|
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center justify-between gap-2">
|
|
4476
4268
|
<span>{t('lessonForm.transcriptionSegments')}</span>
|
|
4477
|
-
<
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
{
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4269
|
+
<div className="flex items-center gap-1.5">
|
|
4270
|
+
<Button
|
|
4271
|
+
type="button"
|
|
4272
|
+
variant="outline"
|
|
4273
|
+
size="sm"
|
|
4274
|
+
className="h-6 text-xs px-2"
|
|
4275
|
+
disabled={isStartingTranscription}
|
|
4276
|
+
onClick={() =>
|
|
4277
|
+
startTranscription({
|
|
4278
|
+
onSuccess: () => {
|
|
4279
|
+
toast.success('Transcrição iniciada!');
|
|
4280
|
+
queryClient.invalidateQueries({
|
|
4281
|
+
queryKey: [
|
|
4282
|
+
'lesson-transcription-segments',
|
|
4283
|
+
lesson?.id,
|
|
4284
|
+
],
|
|
4285
|
+
});
|
|
4286
|
+
},
|
|
4287
|
+
onError: (err) => {
|
|
4288
|
+
toast.error(
|
|
4289
|
+
err.message ||
|
|
4290
|
+
'Erro ao iniciar a transcrição.'
|
|
4291
|
+
);
|
|
4292
|
+
},
|
|
4293
|
+
})
|
|
4294
|
+
}
|
|
4295
|
+
>
|
|
4296
|
+
{isStartingTranscription ? (
|
|
4297
|
+
<Loader2 className="size-3 mr-1 animate-spin" />
|
|
4298
|
+
) : (
|
|
4299
|
+
<Mic className="size-3 mr-1" />
|
|
4300
|
+
)}
|
|
4301
|
+
Iniciar transcrição
|
|
4302
|
+
</Button>
|
|
4303
|
+
<Button
|
|
4304
|
+
type="button"
|
|
4305
|
+
variant="outline"
|
|
4306
|
+
size="sm"
|
|
4307
|
+
className="h-6 text-xs px-2"
|
|
4308
|
+
onClick={() =>
|
|
4309
|
+
updateTranscriptionSegmentsState((prev) => [
|
|
4310
|
+
...prev,
|
|
4311
|
+
{
|
|
4312
|
+
id: segmentId(),
|
|
4313
|
+
start: '00:00',
|
|
4314
|
+
end: '00:15',
|
|
4315
|
+
text: '',
|
|
4316
|
+
},
|
|
4317
|
+
])
|
|
4318
|
+
}
|
|
4319
|
+
>
|
|
4320
|
+
<Plus className="size-3 mr-1" />
|
|
4321
|
+
{t('lessonForm.newTranscriptionSegment')}
|
|
4322
|
+
</Button>
|
|
4323
|
+
</div>
|
|
4497
4324
|
</CardTitle>
|
|
4498
4325
|
</CardHeader>
|
|
4499
4326
|
<CardContent className="px-3 pb-2 flex flex-col gap-2">
|
|
4327
|
+
{isLoadingTranscription
|
|
4328
|
+
? Array.from({ length: 3 }).map((_, i) => (
|
|
4329
|
+
<div
|
|
4330
|
+
key={i}
|
|
4331
|
+
className="rounded-md border bg-background/80 p-2"
|
|
4332
|
+
>
|
|
4333
|
+
<div className="grid grid-cols-1 md:grid-cols-[92px_92px_1fr_auto] gap-2 items-start">
|
|
4334
|
+
<Skeleton className="h-8 w-full" />
|
|
4335
|
+
<Skeleton className="h-8 w-full" />
|
|
4336
|
+
<Skeleton className="h-8 w-full" />
|
|
4337
|
+
<Skeleton className="h-8 w-8 rounded-md" />
|
|
4338
|
+
</div>
|
|
4339
|
+
</div>
|
|
4340
|
+
))
|
|
4341
|
+
: null}
|
|
4500
4342
|
{transcriptionSegments.map((segment, index) => (
|
|
4501
4343
|
<div
|
|
4502
4344
|
key={segment.id}
|
|
@@ -4733,18 +4575,60 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
4733
4575
|
</p>
|
|
4734
4576
|
)}
|
|
4735
4577
|
|
|
4578
|
+
{/* 🆕 Filtro de tipo de recurso */}
|
|
4579
|
+
{supplementaryResources.length > 0 && (
|
|
4580
|
+
<div className="flex items-center gap-2 px-1">
|
|
4581
|
+
<label
|
|
4582
|
+
htmlFor="resource-type-filter"
|
|
4583
|
+
className="text-xs font-medium"
|
|
4584
|
+
>
|
|
4585
|
+
{t('lessonForm.filterBy')}:
|
|
4586
|
+
</label>
|
|
4587
|
+
<Select
|
|
4588
|
+
value={resourceTypeFilter}
|
|
4589
|
+
onValueChange={setResourceTypeFilter}
|
|
4590
|
+
>
|
|
4591
|
+
<SelectTrigger
|
|
4592
|
+
id="resource-type-filter"
|
|
4593
|
+
className="h-8 w-56 text-xs"
|
|
4594
|
+
>
|
|
4595
|
+
<SelectValue />
|
|
4596
|
+
</SelectTrigger>
|
|
4597
|
+
<SelectContent>
|
|
4598
|
+
<SelectItem value="all">
|
|
4599
|
+
{t('common.all')} ({supplementaryResources.length})
|
|
4600
|
+
</SelectItem>
|
|
4601
|
+
{resourceTypeFilterOptions.map(
|
|
4602
|
+
({ type, count, label }) => (
|
|
4603
|
+
<SelectItem key={type} value={type}>
|
|
4604
|
+
{label} ({count})
|
|
4605
|
+
</SelectItem>
|
|
4606
|
+
)
|
|
4607
|
+
)}
|
|
4608
|
+
</SelectContent>
|
|
4609
|
+
</Select>
|
|
4610
|
+
</div>
|
|
4611
|
+
)}
|
|
4612
|
+
|
|
4736
4613
|
{/* Resource list */}
|
|
4737
|
-
{
|
|
4614
|
+
{filteredSupplementaryResources.length === 0 ? (
|
|
4738
4615
|
<p className="text-center text-xs text-muted-foreground py-1">
|
|
4739
4616
|
{t('questionEditor.noLinkedResources')}
|
|
4740
4617
|
</p>
|
|
4741
4618
|
) : (
|
|
4742
4619
|
<div className="flex flex-col gap-1">
|
|
4743
|
-
{
|
|
4620
|
+
{filteredSupplementaryResources.map((res) => {
|
|
4744
4621
|
const metadata = resolveResourceMetadata(res);
|
|
4745
4622
|
const isDownloadingResource = downloadingResourceKeys.has(
|
|
4746
4623
|
String(res.fileId ?? res.id)
|
|
4747
4624
|
);
|
|
4625
|
+
const resourceTypeOptions =
|
|
4626
|
+
!res.type ||
|
|
4627
|
+
GENERIC_RESOURCE_TYPES.includes(
|
|
4628
|
+
res.type as GenericResourceType
|
|
4629
|
+
)
|
|
4630
|
+
? GENERIC_RESOURCE_TYPES
|
|
4631
|
+
: [...GENERIC_RESOURCE_TYPES, res.type];
|
|
4748
4632
|
|
|
4749
4633
|
return (
|
|
4750
4634
|
<div
|
|
@@ -4767,26 +4651,69 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
4767
4651
|
</div>
|
|
4768
4652
|
</div>
|
|
4769
4653
|
|
|
4770
|
-
<div className="flex items-center gap-1.5 pr-1">
|
|
4771
|
-
<
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4780
|
-
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4654
|
+
<div className="flex shrink-0 items-center gap-1.5 pr-1">
|
|
4655
|
+
<div className="flex items-center gap-1">
|
|
4656
|
+
<label
|
|
4657
|
+
htmlFor={`type-${res.id}`}
|
|
4658
|
+
className="sr-only"
|
|
4659
|
+
>
|
|
4660
|
+
{t('lessonForm.resourceType')}
|
|
4661
|
+
</label>
|
|
4662
|
+
<Select
|
|
4663
|
+
value={res.type || undefined}
|
|
4664
|
+
disabled={isUpdatingResourceType === res.id}
|
|
4665
|
+
onValueChange={(value) =>
|
|
4666
|
+
handleUpdateResourceType(res.id, value)
|
|
4667
|
+
}
|
|
4668
|
+
>
|
|
4669
|
+
<SelectTrigger
|
|
4670
|
+
id={`type-${res.id}`}
|
|
4671
|
+
className="h-4 w-32 px-1.5 py-0 text-[0.56rem] leading-none [&>svg]:size-2.5"
|
|
4672
|
+
>
|
|
4673
|
+
<SelectValue
|
|
4674
|
+
placeholder={t(
|
|
4675
|
+
'lessonForm.typePlaceholder'
|
|
4676
|
+
)}
|
|
4677
|
+
/>
|
|
4678
|
+
</SelectTrigger>
|
|
4679
|
+
<SelectContent>
|
|
4680
|
+
{resourceTypeOptions.map((type) => (
|
|
4681
|
+
<SelectItem
|
|
4682
|
+
key={type}
|
|
4683
|
+
value={type}
|
|
4684
|
+
className="text-xs"
|
|
4685
|
+
>
|
|
4686
|
+
{getResourceTypeLabel(t, type)}
|
|
4687
|
+
</SelectItem>
|
|
4688
|
+
))}
|
|
4689
|
+
</SelectContent>
|
|
4690
|
+
</Select>
|
|
4691
|
+
{isUpdatingResourceType === res.id && (
|
|
4692
|
+
<Loader2 className="size-2.5 animate-spin text-muted-foreground" />
|
|
4693
|
+
)}
|
|
4694
|
+
</div>
|
|
4695
|
+
|
|
4696
|
+
<div className="flex items-center gap-1">
|
|
4697
|
+
<Switch
|
|
4698
|
+
checked={Boolean(res.public)}
|
|
4699
|
+
onCheckedChange={(checked) => {
|
|
4700
|
+
setLocalResources((prev) =>
|
|
4701
|
+
prev.map((item) =>
|
|
4702
|
+
item.id === res.id
|
|
4703
|
+
? { ...item, public: checked }
|
|
4704
|
+
: item
|
|
4705
|
+
)
|
|
4706
|
+
);
|
|
4707
|
+
setResourcesDirty(true);
|
|
4708
|
+
}}
|
|
4709
|
+
aria-label={t('lessonForm.public')}
|
|
4710
|
+
/>
|
|
4711
|
+
<span className="text-[0.65rem] text-muted-foreground whitespace-nowrap">
|
|
4712
|
+
{res.public
|
|
4713
|
+
? t('lessonForm.public')
|
|
4714
|
+
: t('lessonForm.private')}
|
|
4715
|
+
</span>
|
|
4716
|
+
</div>
|
|
4790
4717
|
</div>
|
|
4791
4718
|
|
|
4792
4719
|
<IconActionTooltip
|
|
@@ -4857,6 +4784,18 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
4857
4784
|
</div>
|
|
4858
4785
|
</ScrollArea>
|
|
4859
4786
|
</TabsContent>
|
|
4787
|
+
|
|
4788
|
+
{/* ── Tab XP ───────────────────────────────────────────────────── */}
|
|
4789
|
+
<TabsContent
|
|
4790
|
+
value="xp"
|
|
4791
|
+
className="flex-1 min-h-0 mt-0 overflow-y-auto p-2 sm:p-3"
|
|
4792
|
+
>
|
|
4793
|
+
<LessonXpTab
|
|
4794
|
+
lessonId={lessonId}
|
|
4795
|
+
activeTab={activeTab}
|
|
4796
|
+
hasTranscription={transcriptionSegments.length > 0}
|
|
4797
|
+
/>
|
|
4798
|
+
</TabsContent>
|
|
4860
4799
|
</Tabs>
|
|
4861
4800
|
|
|
4862
4801
|
<Dialog
|
|
@@ -4870,11 +4809,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
4870
4809
|
<DialogTitle className="text-base">
|
|
4871
4810
|
{videoPreviewResource?.name ?? t('lessonForm.tabVideos')}
|
|
4872
4811
|
</DialogTitle>
|
|
4873
|
-
<DialogDescription>
|
|
4874
|
-
{videoPreviewResource
|
|
4875
|
-
? t('lessonForm.fileStorageVideosByResolution')
|
|
4876
|
-
: t('lessonForm.tabVideos')}
|
|
4877
|
-
</DialogDescription>
|
|
4812
|
+
<DialogDescription>{t('lessonForm.tabVideos')}</DialogDescription>
|
|
4878
4813
|
</DialogHeader>
|
|
4879
4814
|
<div className="px-5 pb-5">
|
|
4880
4815
|
{videoPreviewError ? (
|
|
@@ -4893,7 +4828,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
4893
4828
|
) : (
|
|
4894
4829
|
<div className="flex min-h-72 items-center justify-center rounded-lg border border-dashed bg-muted/30 px-6 text-center text-sm text-muted-foreground">
|
|
4895
4830
|
{isResolvingVideoPreview
|
|
4896
|
-
? t('lessonForm.
|
|
4831
|
+
? t('lessonForm.videoJobLoading')
|
|
4897
4832
|
: t('questionEditor.resourceOpenError')}
|
|
4898
4833
|
</div>
|
|
4899
4834
|
)}
|