@hed-hog/lms 0.0.349 → 0.0.351
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/achievement/achievement.controller.d.ts +62 -0
- package/dist/achievement/achievement.controller.d.ts.map +1 -0
- package/dist/achievement/achievement.controller.js +90 -0
- package/dist/achievement/achievement.controller.js.map +1 -0
- package/dist/achievement/achievement.mcp-tools.d.ts +19 -0
- package/dist/achievement/achievement.mcp-tools.d.ts.map +1 -0
- package/dist/achievement/achievement.mcp-tools.js +157 -0
- package/dist/achievement/achievement.mcp-tools.js.map +1 -0
- package/dist/achievement/achievement.module.d.ts +3 -0
- package/dist/achievement/achievement.module.d.ts.map +1 -0
- package/dist/achievement/achievement.module.js +26 -0
- package/dist/achievement/achievement.module.js.map +1 -0
- package/dist/achievement/achievement.service.d.ts +72 -0
- package/dist/achievement/achievement.service.d.ts.map +1 -0
- package/dist/achievement/achievement.service.js +200 -0
- package/dist/achievement/achievement.service.js.map +1 -0
- package/dist/achievement/dto/create-achievement.dto.d.ts +12 -0
- package/dist/achievement/dto/create-achievement.dto.d.ts.map +1 -0
- package/dist/achievement/dto/create-achievement.dto.js +60 -0
- package/dist/achievement/dto/create-achievement.dto.js.map +1 -0
- package/dist/achievement/dto/update-achievement.dto.d.ts +11 -0
- package/dist/achievement/dto/update-achievement.dto.d.ts.map +1 -0
- package/dist/achievement/dto/update-achievement.dto.js +57 -0
- package/dist/achievement/dto/update-achievement.dto.js.map +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.controller.d.ts +114 -0
- package/dist/bitcode-wallet/bitcode-wallet.controller.d.ts.map +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.controller.js +102 -0
- package/dist/bitcode-wallet/bitcode-wallet.controller.js.map +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.d.ts +25 -0
- package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.d.ts.map +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.js +160 -0
- package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.js.map +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.module.d.ts +3 -0
- package/dist/bitcode-wallet/bitcode-wallet.module.d.ts.map +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.module.js +26 -0
- package/dist/bitcode-wallet/bitcode-wallet.module.js.map +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +127 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.js +264 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -0
- package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.d.ts +8 -0
- package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.d.ts.map +1 -0
- package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.js +33 -0
- package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.js.map +1 -0
- package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.d.ts +4 -0
- package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.d.ts.map +1 -0
- package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.js +22 -0
- package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.js.map +1 -0
- package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.d.ts +7 -0
- package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.d.ts.map +1 -0
- package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.js +35 -0
- package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.js.map +1 -0
- package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.d.ts +4 -0
- package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.d.ts.map +1 -0
- package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.js +23 -0
- package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.js.map +1 -0
- package/dist/certificate/certificate.controller.d.ts +24 -2
- package/dist/certificate/certificate.controller.d.ts.map +1 -1
- package/dist/certificate/certificate.controller.js +20 -6
- package/dist/certificate/certificate.controller.js.map +1 -1
- package/dist/certificate/certificate.mcp-tools.d.ts +24 -0
- package/dist/certificate/certificate.mcp-tools.d.ts.map +1 -0
- package/dist/certificate/certificate.mcp-tools.js +188 -0
- package/dist/certificate/certificate.mcp-tools.js.map +1 -0
- package/dist/certificate/certificate.module.d.ts.map +1 -1
- package/dist/certificate/certificate.module.js +2 -1
- package/dist/certificate/certificate.module.js.map +1 -1
- package/dist/certificate/certificate.service.d.ts +30 -4
- package/dist/certificate/certificate.service.d.ts.map +1 -1
- package/dist/certificate/certificate.service.js +157 -8
- package/dist/certificate/certificate.service.js.map +1 -1
- package/dist/certificate/dto/update-certificate-public-access.dto.d.ts +4 -0
- package/dist/certificate/dto/update-certificate-public-access.dto.d.ts.map +1 -0
- package/dist/certificate/dto/update-certificate-public-access.dto.js +21 -0
- package/dist/certificate/dto/update-certificate-public-access.dto.js.map +1 -0
- package/dist/class-group/class-group.mcp-tools.d.ts +87 -0
- package/dist/class-group/class-group.mcp-tools.d.ts.map +1 -0
- package/dist/class-group/class-group.mcp-tools.js +553 -0
- package/dist/class-group/class-group.mcp-tools.js.map +1 -0
- package/dist/class-group/class-group.module.d.ts.map +1 -1
- package/dist/class-group/class-group.module.js +2 -1
- package/dist/class-group/class-group.module.js.map +1 -1
- package/dist/class-group/class-group.service.d.ts +3 -1
- package/dist/class-group/class-group.service.d.ts.map +1 -1
- package/dist/class-group/class-group.service.js +45 -2
- package/dist/class-group/class-group.service.js.map +1 -1
- package/dist/course/course-operations-integration.service.d.ts +40 -0
- package/dist/course/course-operations-integration.service.d.ts.map +1 -0
- package/dist/course/course-operations-integration.service.js +372 -0
- package/dist/course/course-operations-integration.service.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +67 -14
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +45 -2
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +58 -9
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +260 -62
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +37 -0
- package/dist/course/course-video-conversion.service.d.ts.map +1 -0
- package/dist/course/course-video-conversion.service.js +308 -0
- package/dist/course/course-video-conversion.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +29 -0
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +23 -0
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.mcp-tools.d.ts +90 -0
- package/dist/course/course.mcp-tools.d.ts.map +1 -0
- package/dist/course/course.mcp-tools.js +520 -0
- package/dist/course/course.mcp-tools.js.map +1 -0
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +23 -3
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +30 -1
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +159 -70
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +5 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +16 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +2 -0
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +16 -0
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/course/dto/update-course-resources.dto.d.ts +11 -0
- package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-resources.dto.js +51 -0
- package/dist/course/dto/update-course-resources.dto.js.map +1 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.controller.d.ts +23 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.controller.d.ts.map +1 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.controller.js +78 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.controller.js.map +1 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.d.ts +22 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.d.ts.map +1 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.js +120 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.js.map +1 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.module.d.ts +3 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.module.d.ts.map +1 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.module.js +26 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.module.js.map +1 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.service.d.ts +49 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.service.d.ts.map +1 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.service.js +272 -0
- package/dist/course-lesson-discussion/course-lesson-discussion.service.js.map +1 -0
- package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.d.ts +6 -0
- package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.d.ts.map +1 -0
- package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.js +33 -0
- package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.js.map +1 -0
- package/dist/course-lesson-note/course-lesson-note.controller.d.ts +53 -0
- package/dist/course-lesson-note/course-lesson-note.controller.d.ts.map +1 -0
- package/dist/course-lesson-note/course-lesson-note.controller.js +93 -0
- package/dist/course-lesson-note/course-lesson-note.controller.js.map +1 -0
- package/dist/course-lesson-note/course-lesson-note.mcp-tools.d.ts +27 -0
- package/dist/course-lesson-note/course-lesson-note.mcp-tools.d.ts.map +1 -0
- package/dist/course-lesson-note/course-lesson-note.mcp-tools.js +145 -0
- package/dist/course-lesson-note/course-lesson-note.mcp-tools.js.map +1 -0
- package/dist/course-lesson-note/course-lesson-note.module.d.ts +3 -0
- package/dist/course-lesson-note/course-lesson-note.module.d.ts.map +1 -0
- package/dist/course-lesson-note/course-lesson-note.module.js +26 -0
- package/dist/course-lesson-note/course-lesson-note.module.js.map +1 -0
- package/dist/course-lesson-note/course-lesson-note.service.d.ts +59 -0
- package/dist/course-lesson-note/course-lesson-note.service.d.ts.map +1 -0
- package/dist/course-lesson-note/course-lesson-note.service.js +195 -0
- package/dist/course-lesson-note/course-lesson-note.service.js.map +1 -0
- package/dist/course-lesson-note/dto/create-course-lesson-note.dto.d.ts +6 -0
- package/dist/course-lesson-note/dto/create-course-lesson-note.dto.d.ts.map +1 -0
- package/dist/course-lesson-note/dto/create-course-lesson-note.dto.js +33 -0
- package/dist/course-lesson-note/dto/create-course-lesson-note.dto.js.map +1 -0
- package/dist/course-lesson-note/dto/update-course-lesson-note.dto.d.ts +6 -0
- package/dist/course-lesson-note/dto/update-course-lesson-note.dto.d.ts.map +1 -0
- package/dist/course-lesson-note/dto/update-course-lesson-note.dto.js +35 -0
- package/dist/course-lesson-note/dto/update-course-lesson-note.dto.js.map +1 -0
- package/dist/dashboard/dashboard.mcp-tools.d.ts +10 -0
- package/dist/dashboard/dashboard.mcp-tools.d.ts.map +1 -0
- package/dist/dashboard/dashboard.mcp-tools.js +46 -0
- package/dist/dashboard/dashboard.mcp-tools.js.map +1 -0
- package/dist/dashboard/dashboard.module.d.ts.map +1 -1
- package/dist/dashboard/dashboard.module.js +2 -1
- package/dist/dashboard/dashboard.module.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +3 -3
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +0 -1
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.mcp-tools.d.ts +82 -0
- package/dist/enterprise/enterprise.mcp-tools.d.ts.map +1 -0
- package/dist/enterprise/enterprise.mcp-tools.js +516 -0
- package/dist/enterprise/enterprise.mcp-tools.js.map +1 -0
- package/dist/enterprise/enterprise.module.d.ts.map +1 -1
- package/dist/enterprise/enterprise.module.js +2 -1
- package/dist/enterprise/enterprise.module.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -3
- package/dist/enterprise/training/enterprise-training.module.d.ts.map +1 -1
- package/dist/enterprise/training/enterprise-training.module.js +11 -1
- package/dist/enterprise/training/enterprise-training.module.js.map +1 -1
- package/dist/enterprise/training/training-admin.mcp-tools.d.ts +79 -0
- package/dist/enterprise/training/training-admin.mcp-tools.d.ts.map +1 -0
- package/dist/enterprise/training/training-admin.mcp-tools.js +620 -0
- package/dist/enterprise/training/training-admin.mcp-tools.js.map +1 -0
- package/dist/enterprise/training/training-instructor.mcp-tools.d.ts +47 -0
- package/dist/enterprise/training/training-instructor.mcp-tools.d.ts.map +1 -0
- package/dist/enterprise/training/training-instructor.mcp-tools.js +275 -0
- package/dist/enterprise/training/training-instructor.mcp-tools.js.map +1 -0
- package/dist/enterprise/training/training-student.controller.d.ts +24 -0
- package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.controller.js +22 -0
- package/dist/enterprise/training/training-student.controller.js.map +1 -1
- package/dist/enterprise/training/training-student.mcp-tools.d.ts +27 -0
- package/dist/enterprise/training/training-student.mcp-tools.d.ts.map +1 -0
- package/dist/enterprise/training/training-student.mcp-tools.js +186 -0
- package/dist/enterprise/training/training-student.mcp-tools.js.map +1 -0
- package/dist/enterprise/training/training-student.service.d.ts +32 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +138 -0
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/evaluation/evaluation.mcp-tools.d.ts +25 -0
- package/dist/evaluation/evaluation.mcp-tools.d.ts.map +1 -0
- package/dist/evaluation/evaluation.mcp-tools.js +220 -0
- package/dist/evaluation/evaluation.mcp-tools.js.map +1 -0
- package/dist/evaluation/evaluation.module.d.ts.map +1 -1
- package/dist/evaluation/evaluation.module.js +2 -1
- package/dist/evaluation/evaluation.module.js.map +1 -1
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +9 -2
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/exam/dto/create-exam-question.dto.d.ts +2 -0
- package/dist/exam/dto/create-exam-question.dto.d.ts.map +1 -1
- package/dist/exam/dto/create-exam-question.dto.js +10 -0
- package/dist/exam/dto/create-exam-question.dto.js.map +1 -1
- package/dist/exam/dto/create-exam.dto.d.ts +2 -0
- package/dist/exam/dto/create-exam.dto.d.ts.map +1 -1
- package/dist/exam/dto/create-exam.dto.js +10 -0
- package/dist/exam/dto/create-exam.dto.js.map +1 -1
- package/dist/exam/dto/create-question-subject.dto.d.ts +5 -0
- package/dist/exam/dto/create-question-subject.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-question-subject.dto.js +28 -0
- package/dist/exam/dto/create-question-subject.dto.js.map +1 -0
- package/dist/exam/exam-attempt.controller.d.ts +4 -0
- package/dist/exam/exam-attempt.controller.d.ts.map +1 -1
- package/dist/exam/exam-attempt.service.d.ts +7 -1
- package/dist/exam/exam-attempt.service.d.ts.map +1 -1
- package/dist/exam/exam-attempt.service.js +47 -17
- package/dist/exam/exam-attempt.service.js.map +1 -1
- package/dist/exam/exam.controller.d.ts +34 -0
- package/dist/exam/exam.controller.d.ts.map +1 -1
- package/dist/exam/exam.controller.js +27 -0
- package/dist/exam/exam.controller.js.map +1 -1
- package/dist/exam/exam.mcp-tools.d.ts +62 -0
- package/dist/exam/exam.mcp-tools.d.ts.map +1 -0
- package/dist/exam/exam.mcp-tools.js +430 -0
- package/dist/exam/exam.mcp-tools.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 +38 -0
- package/dist/exam/exam.service.d.ts.map +1 -1
- package/dist/exam/exam.service.js +114 -17
- package/dist/exam/exam.service.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/instructor/instructor.mcp-tools.d.ts +41 -0
- package/dist/instructor/instructor.mcp-tools.d.ts.map +1 -0
- package/dist/instructor/instructor.mcp-tools.js +326 -0
- package/dist/instructor/instructor.mcp-tools.js.map +1 -0
- package/dist/instructor/instructor.module.d.ts.map +1 -1
- package/dist/instructor/instructor.module.js +2 -1
- package/dist/instructor/instructor.module.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +18 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/realtime/lms-realtime.controller.d.ts +7 -0
- package/dist/realtime/lms-realtime.controller.d.ts.map +1 -0
- package/dist/realtime/lms-realtime.controller.js +34 -0
- package/dist/realtime/lms-realtime.controller.js.map +1 -0
- package/dist/realtime/lms-realtime.module.d.ts +3 -0
- package/dist/realtime/lms-realtime.module.d.ts.map +1 -0
- package/dist/realtime/lms-realtime.module.js +25 -0
- package/dist/realtime/lms-realtime.module.js.map +1 -0
- package/dist/realtime/lms-realtime.service.d.ts +36 -0
- package/dist/realtime/lms-realtime.service.d.ts.map +1 -0
- package/dist/realtime/lms-realtime.service.js +59 -0
- package/dist/realtime/lms-realtime.service.js.map +1 -0
- package/dist/realtime/lms-realtime.subscriber.d.ts +10 -0
- package/dist/realtime/lms-realtime.subscriber.d.ts.map +1 -0
- package/dist/realtime/lms-realtime.subscriber.js +70 -0
- package/dist/realtime/lms-realtime.subscriber.js.map +1 -0
- package/dist/reports/reports.mcp-tools.d.ts +10 -0
- package/dist/reports/reports.mcp-tools.d.ts.map +1 -0
- package/dist/reports/reports.mcp-tools.js +50 -0
- package/dist/reports/reports.mcp-tools.js.map +1 -0
- package/dist/reports/reports.module.d.ts.map +1 -1
- package/dist/reports/reports.module.js +2 -1
- package/dist/reports/reports.module.js.map +1 -1
- package/dist/training/training.mcp-tools.d.ts +20 -0
- package/dist/training/training.mcp-tools.d.ts.map +1 -0
- package/dist/training/training.mcp-tools.js +181 -0
- package/dist/training/training.mcp-tools.js.map +1 -0
- package/dist/training/training.module.d.ts.map +1 -1
- package/dist/training/training.module.js +2 -1
- package/dist/training/training.module.js.map +1 -1
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts +6 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts.map +1 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js +33 -0
- package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js.map +1 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts +6 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts.map +1 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js +33 -0
- package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts +38 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.js +89 -0
- package/dist/video-resolution-profile/video-resolution-profile.controller.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts +26 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js +160 -0
- package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.d.ts +3 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.js +26 -0
- package/dist/video-resolution-profile/video-resolution-profile.module.js.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.d.ts +45 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.d.ts.map +1 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.js +117 -0
- package/dist/video-resolution-profile/video-resolution-profile.service.js.map +1 -0
- package/hedhog/data/integration_event_catalog.yaml +69 -0
- package/hedhog/data/menu.yaml +51 -0
- package/hedhog/data/route.yaml +2484 -0
- package/hedhog/data/video_resolution_profile.yaml +7 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +432 -422
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +200 -67
- package/hedhog/frontend/app/_components/course-picker.tsx.ejs +228 -0
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +7 -4
- package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +34 -4
- package/hedhog/frontend/app/_lib/editor/types.ts.ejs +28 -3
- package/hedhog/frontend/app/_lib/hooks/use-lms-realtime-refresh.ts.ejs +58 -0
- package/hedhog/frontend/app/achievements/page.tsx.ejs +850 -0
- package/hedhog/frontend/app/bitcodes/page.tsx.ejs +1016 -0
- package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +68 -5
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +29 -8
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +14 -0
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +194 -9
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +15 -5
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +16 -5
- package/hedhog/frontend/app/classes/page.tsx.ejs +126 -2105
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +19 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +24 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +51 -11
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +11 -6
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +7 -4
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +24 -96
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +80 -66
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1286 -230
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1334 -153
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +11 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +62 -52
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +106 -4
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +30 -7
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +138 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +16 -2
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +282 -113
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +7 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +8 -4
- package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +10 -4
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +10 -3
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +186 -5
- package/hedhog/frontend/app/exams/page.tsx.ejs +89 -26
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +10 -3
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +190 -17
- package/hedhog/frontend/app/instructors/page.tsx.ejs +1 -0
- package/hedhog/frontend/app/layout.tsx.ejs +5 -1
- package/hedhog/frontend/app/paths/page.tsx.ejs +19 -29
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/training/page.tsx.ejs +19 -29
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +607 -0
- package/hedhog/frontend/messages/en.json +563 -20
- package/hedhog/frontend/messages/pt.json +563 -20
- package/hedhog/query/triggers.sql +53 -0
- package/hedhog/table/achievement.yaml +46 -0
- package/hedhog/table/bitcode_wallet.yaml +18 -0
- package/hedhog/table/bitcode_wallet_transaction.yaml +22 -0
- package/hedhog/table/certificate.yaml +3 -0
- package/hedhog/table/course.yaml +8 -0
- package/hedhog/table/course_file.yaml +23 -0
- package/hedhog/table/course_lesson.yaml +5 -0
- package/hedhog/table/course_lesson_discussion_like.yaml +21 -0
- package/hedhog/table/course_lesson_discussion_topic.yaml +35 -0
- package/hedhog/table/course_lesson_file.yaml +8 -0
- package/hedhog/table/course_lesson_note.yaml +34 -0
- package/hedhog/table/course_video_resolution_profile.yaml +22 -0
- package/hedhog/table/exam.yaml +5 -0
- package/hedhog/table/learning_path_enrollment.yaml +6 -0
- package/hedhog/table/question.yaml +10 -0
- package/hedhog/table/question_subject.yaml +17 -0
- package/hedhog/table/student_activity_streak.yaml +25 -0
- package/hedhog/table/video_resolution_profile.yaml +18 -0
- package/package.json +8 -7
- package/src/achievement/achievement.controller.ts +60 -0
- package/src/achievement/achievement.mcp-tools.ts +108 -0
- package/src/achievement/achievement.module.ts +13 -0
- package/src/achievement/achievement.service.ts +252 -0
- package/src/achievement/dto/create-achievement.dto.ts +50 -0
- package/src/achievement/dto/update-achievement.dto.ts +47 -0
- package/src/bitcode-wallet/bitcode-wallet.controller.ts +69 -0
- package/src/bitcode-wallet/bitcode-wallet.mcp-tools.ts +107 -0
- package/src/bitcode-wallet/bitcode-wallet.module.ts +13 -0
- package/src/bitcode-wallet/bitcode-wallet.service.ts +361 -0
- package/src/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.ts +27 -0
- package/src/bitcode-wallet/dto/create-bitcode-wallet.dto.ts +7 -0
- package/src/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.ts +28 -0
- package/src/bitcode-wallet/dto/update-bitcode-wallet.dto.ts +8 -0
- package/src/certificate/certificate.controller.ts +17 -3
- package/src/certificate/certificate.mcp-tools.ts +131 -0
- package/src/certificate/certificate.module.ts +2 -1
- package/src/certificate/certificate.service.ts +193 -7
- package/src/certificate/dto/update-certificate-public-access.dto.ts +6 -0
- package/src/class-group/class-group.mcp-tools.ts +435 -0
- package/src/class-group/class-group.module.ts +2 -1
- package/src/class-group/class-group.service.ts +51 -1
- package/src/course/course-operations-integration.service.ts +520 -0
- package/src/course/course-structure.controller.ts +46 -10
- package/src/course/course-structure.service.ts +236 -27
- package/src/course/course-video-conversion.service.ts +415 -0
- package/src/course/course.controller.ts +18 -0
- package/src/course/course.mcp-tools.ts +409 -0
- package/src/course/course.module.ts +23 -3
- package/src/course/course.service.ts +178 -29
- package/src/course/dto/create-course-structure-lesson.dto.ts +13 -2
- package/src/course/dto/create-course.dto.ts +16 -0
- package/src/course/dto/update-course-resources.dto.ts +39 -0
- package/src/course-lesson-discussion/course-lesson-discussion.controller.ts +55 -0
- package/src/course-lesson-discussion/course-lesson-discussion.mcp-tools.ts +75 -0
- package/src/course-lesson-discussion/course-lesson-discussion.module.ts +13 -0
- package/src/course-lesson-discussion/course-lesson-discussion.service.ts +354 -0
- package/src/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.ts +16 -0
- package/src/course-lesson-note/course-lesson-note.controller.ts +68 -0
- package/src/course-lesson-note/course-lesson-note.mcp-tools.ts +96 -0
- package/src/course-lesson-note/course-lesson-note.module.ts +13 -0
- package/src/course-lesson-note/course-lesson-note.service.ts +248 -0
- package/src/course-lesson-note/dto/create-course-lesson-note.dto.ts +16 -0
- package/src/course-lesson-note/dto/update-course-lesson-note.dto.ts +18 -0
- package/src/dashboard/dashboard.mcp-tools.ts +23 -0
- package/src/dashboard/dashboard.module.ts +2 -1
- package/src/enterprise/enterprise.controller.ts +0 -1
- package/src/enterprise/enterprise.mcp-tools.ts +403 -0
- package/src/enterprise/enterprise.module.ts +2 -1
- package/src/enterprise/training/enterprise-training.module.ts +11 -1
- package/src/enterprise/training/training-admin.mcp-tools.ts +479 -0
- package/src/enterprise/training/training-instructor.mcp-tools.ts +210 -0
- package/src/enterprise/training/training-student.controller.ts +17 -1
- package/src/enterprise/training/training-student.mcp-tools.ts +136 -0
- package/src/enterprise/training/training-student.service.ts +167 -1
- package/src/evaluation/evaluation.mcp-tools.ts +155 -0
- package/src/evaluation/evaluation.module.ts +2 -1
- package/src/evaluation/evaluation.service.ts +9 -2
- package/src/exam/dto/create-exam-question.dto.ts +8 -0
- package/src/exam/dto/create-exam.dto.ts +8 -0
- package/src/exam/dto/create-question-subject.dto.ts +12 -0
- package/src/exam/exam-attempt.service.ts +46 -14
- package/src/exam/exam.controller.ts +19 -0
- package/src/exam/exam.mcp-tools.ts +337 -0
- package/src/exam/exam.module.ts +2 -1
- package/src/exam/exam.service.ts +121 -0
- package/src/index.ts +10 -0
- package/src/instructor/instructor.mcp-tools.ts +243 -0
- package/src/instructor/instructor.module.ts +2 -1
- package/src/lms.module.ts +18 -1
- package/src/realtime/lms-realtime.controller.ts +12 -0
- package/src/realtime/lms-realtime.module.ts +12 -0
- package/src/realtime/lms-realtime.service.ts +98 -0
- package/src/realtime/lms-realtime.subscriber.ts +61 -0
- package/src/reports/reports.mcp-tools.ts +27 -0
- package/src/reports/reports.module.ts +2 -1
- package/src/training/training.mcp-tools.ts +128 -0
- package/src/training/training.module.ts +2 -1
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +16 -0
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +16 -0
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +62 -0
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +128 -0
- package/src/video-resolution-profile/video-resolution-profile.module.ts +13 -0
- package/src/video-resolution-profile/video-resolution-profile.service.ts +117 -0
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
4
|
-
import { useTranslations } from 'next-intl';
|
|
5
4
|
import {
|
|
6
5
|
CircleDot,
|
|
7
6
|
CircleOff,
|
|
@@ -20,7 +19,9 @@ import {
|
|
|
20
19
|
Loader2,
|
|
21
20
|
Lock,
|
|
22
21
|
Pencil,
|
|
22
|
+
Play,
|
|
23
23
|
Plus,
|
|
24
|
+
RefreshCw,
|
|
24
25
|
Save,
|
|
25
26
|
Trash2,
|
|
26
27
|
Undo2,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
X,
|
|
30
31
|
type LucideIcon,
|
|
31
32
|
} from 'lucide-react';
|
|
33
|
+
import { useTranslations } from 'next-intl';
|
|
32
34
|
import { useEffect, useRef, useState } from 'react';
|
|
33
35
|
import { useForm, useWatch } from 'react-hook-form';
|
|
34
36
|
import { toast } from 'sonner';
|
|
@@ -48,6 +50,8 @@ import {
|
|
|
48
50
|
} from '@/components/ui/form';
|
|
49
51
|
import { Input } from '@/components/ui/input';
|
|
50
52
|
import { Label } from '@/components/ui/label';
|
|
53
|
+
import { Progress } from '@/components/ui/progress';
|
|
54
|
+
import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
|
|
51
55
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
52
56
|
import {
|
|
53
57
|
Select,
|
|
@@ -59,7 +63,6 @@ import {
|
|
|
59
63
|
import { Separator } from '@/components/ui/separator';
|
|
60
64
|
import {
|
|
61
65
|
Sheet,
|
|
62
|
-
SheetContent,
|
|
63
66
|
SheetFooter,
|
|
64
67
|
SheetHeader,
|
|
65
68
|
SheetTitle,
|
|
@@ -86,11 +89,15 @@ import {
|
|
|
86
89
|
import { CSS } from '@dnd-kit/utilities';
|
|
87
90
|
|
|
88
91
|
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
89
|
-
import { useApp } from '@hed-hog/next-app-provider';
|
|
92
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
90
93
|
import { useQueryClient } from '@tanstack/react-query';
|
|
91
94
|
import {
|
|
92
95
|
deleteFile,
|
|
96
|
+
enqueueLessonVideoConversion,
|
|
97
|
+
getQueueJob,
|
|
93
98
|
uploadFile,
|
|
99
|
+
type QueueJobResponse,
|
|
100
|
+
type QueueJobStatus,
|
|
94
101
|
} from '../_data/services/course-structure.service';
|
|
95
102
|
import {
|
|
96
103
|
useDeleteLessonMutation,
|
|
@@ -128,6 +135,68 @@ function formatFileSize(bytes: number): string {
|
|
|
128
135
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
129
136
|
}
|
|
130
137
|
|
|
138
|
+
function videoProfileResourceType(profileId: number): string {
|
|
139
|
+
return `video_profile:${profileId}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const MAX_VIDEO_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024;
|
|
143
|
+
|
|
144
|
+
type LessonEditorTab =
|
|
145
|
+
| 'dados'
|
|
146
|
+
| 'conteudo'
|
|
147
|
+
| 'videos'
|
|
148
|
+
| 'transcricao'
|
|
149
|
+
| 'recursos';
|
|
150
|
+
|
|
151
|
+
const ACTIVE_VIDEO_JOB_STATUSES: QueueJobStatus[] = [
|
|
152
|
+
'pending',
|
|
153
|
+
'scheduled',
|
|
154
|
+
'processing',
|
|
155
|
+
'retrying',
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const TERMINAL_VIDEO_JOB_STATUSES: QueueJobStatus[] = [
|
|
159
|
+
'completed',
|
|
160
|
+
'failed',
|
|
161
|
+
'canceled',
|
|
162
|
+
'dead_letter',
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const VIDEO_JOB_STATUS_COLORS: Record<QueueJobStatus, string> = {
|
|
166
|
+
pending: 'bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300',
|
|
167
|
+
scheduled: 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300',
|
|
168
|
+
processing: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
|
|
169
|
+
retrying:
|
|
170
|
+
'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300',
|
|
171
|
+
completed:
|
|
172
|
+
'bg-emerald-100 text-emerald-700 dark:bg-emerald-900 dark:text-emerald-300',
|
|
173
|
+
failed: 'bg-red-100 text-red-700 dark:bg-red-900 dark:text-red-300',
|
|
174
|
+
canceled: 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
|
|
175
|
+
dead_letter: 'bg-rose-100 text-rose-700 dark:bg-rose-900 dark:text-rose-300',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
function formatDateTimeLabel(value?: string | null): string | null {
|
|
179
|
+
if (!value) return null;
|
|
180
|
+
|
|
181
|
+
const parsed = new Date(value);
|
|
182
|
+
if (Number.isNaN(parsed.getTime())) return null;
|
|
183
|
+
|
|
184
|
+
return new Intl.DateTimeFormat('pt-BR', {
|
|
185
|
+
dateStyle: 'short',
|
|
186
|
+
timeStyle: 'short',
|
|
187
|
+
}).format(parsed);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function formatDurationLabel(durationMs?: number | null): string | null {
|
|
191
|
+
if (durationMs == null) return null;
|
|
192
|
+
if (durationMs < 1000) return `${durationMs} ms`;
|
|
193
|
+
|
|
194
|
+
const seconds = durationMs / 1000;
|
|
195
|
+
if (seconds < 60) return `${seconds.toFixed(1)} s`;
|
|
196
|
+
|
|
197
|
+
return `${(seconds / 60).toFixed(1)} min`;
|
|
198
|
+
}
|
|
199
|
+
|
|
131
200
|
// ── Config maps ───────────────────────────────────────────────────────────────
|
|
132
201
|
|
|
133
202
|
const TYPE_CONFIG: Record<
|
|
@@ -251,6 +320,93 @@ type FormValues = {
|
|
|
251
320
|
questionId?: string | null;
|
|
252
321
|
};
|
|
253
322
|
|
|
323
|
+
type TranscriptionSegment = {
|
|
324
|
+
id: string;
|
|
325
|
+
start: string;
|
|
326
|
+
end: string;
|
|
327
|
+
text: string;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
function segmentId(): string {
|
|
331
|
+
return Math.random().toString(36).slice(2, 9);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function parseTimeToSeconds(input: string): number | null {
|
|
335
|
+
const normalized = input.trim();
|
|
336
|
+
if (!normalized) return null;
|
|
337
|
+
const parts = normalized.split(':').map((part) => Number(part));
|
|
338
|
+
if (parts.some((part) => !Number.isFinite(part) || part < 0)) return null;
|
|
339
|
+
if (parts.length === 2) {
|
|
340
|
+
const mm = parts[0];
|
|
341
|
+
const ss = parts[1];
|
|
342
|
+
if (mm === undefined || ss === undefined) return null;
|
|
343
|
+
if (ss > 59) return null;
|
|
344
|
+
return mm * 60 + ss;
|
|
345
|
+
}
|
|
346
|
+
if (parts.length === 3) {
|
|
347
|
+
const hh = parts[0];
|
|
348
|
+
const mm = parts[1];
|
|
349
|
+
const ss = parts[2];
|
|
350
|
+
if (hh === undefined || mm === undefined || ss === undefined) return null;
|
|
351
|
+
if (mm > 59 || ss > 59) return null;
|
|
352
|
+
return hh * 3600 + mm * 60 + ss;
|
|
353
|
+
}
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function normalizeTimeInput(input: string): string {
|
|
358
|
+
const seconds = parseTimeToSeconds(input);
|
|
359
|
+
if (seconds === null) return input.trim();
|
|
360
|
+
const hh = Math.floor(seconds / 3600);
|
|
361
|
+
const mm = Math.floor((seconds % 3600) / 60);
|
|
362
|
+
const ss = seconds % 60;
|
|
363
|
+
if (hh > 0) {
|
|
364
|
+
return `${String(hh).padStart(2, '0')}:${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
|
|
365
|
+
}
|
|
366
|
+
return `${String(mm).padStart(2, '0')}:${String(ss).padStart(2, '0')}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function parseTranscriptionSegments(raw?: string): TranscriptionSegment[] {
|
|
370
|
+
const lines = String(raw ?? '')
|
|
371
|
+
.split('\n')
|
|
372
|
+
.map((line) => line.trim())
|
|
373
|
+
.filter(Boolean);
|
|
374
|
+
if (lines.length === 0) {
|
|
375
|
+
return [{ id: segmentId(), start: '00:00', end: '00:15', text: '' }];
|
|
376
|
+
}
|
|
377
|
+
return lines.map((line) => {
|
|
378
|
+
const match = line.match(/^\[(.+?)\s*-->\s*(.+?)\]\s*(.*)$/);
|
|
379
|
+
if (match) {
|
|
380
|
+
return {
|
|
381
|
+
id: segmentId(),
|
|
382
|
+
start: normalizeTimeInput(match[1] ?? ''),
|
|
383
|
+
end: normalizeTimeInput(match[2] ?? ''),
|
|
384
|
+
text: match[3] ?? '',
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
id: segmentId(),
|
|
389
|
+
start: '00:00',
|
|
390
|
+
end: '00:15',
|
|
391
|
+
text: line,
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function serializeTranscriptionSegments(
|
|
397
|
+
segments: TranscriptionSegment[]
|
|
398
|
+
): string {
|
|
399
|
+
return segments
|
|
400
|
+
.map((segment) => ({
|
|
401
|
+
start: normalizeTimeInput(segment.start),
|
|
402
|
+
end: normalizeTimeInput(segment.end),
|
|
403
|
+
text: segment.text.trim(),
|
|
404
|
+
}))
|
|
405
|
+
.filter((segment) => segment.text.length > 0)
|
|
406
|
+
.map((segment) => `[${segment.start} --> ${segment.end}] ${segment.text}`)
|
|
407
|
+
.join('\n');
|
|
408
|
+
}
|
|
409
|
+
|
|
254
410
|
// ── SortableAlternativa ───────────────────────────────────────────────────────
|
|
255
411
|
|
|
256
412
|
function SortableAlternativa({
|
|
@@ -327,9 +483,7 @@ function SortableAlternativa({
|
|
|
327
483
|
? 'text-foreground'
|
|
328
484
|
: 'text-muted-foreground hover:text-foreground'
|
|
329
485
|
)}
|
|
330
|
-
aria-label={
|
|
331
|
-
alt.correta ? markIncorrectLabel : markCorrectLabel
|
|
332
|
-
}
|
|
486
|
+
aria-label={alt.correta ? markIncorrectLabel : markCorrectLabel}
|
|
333
487
|
>
|
|
334
488
|
{alt.correta ? (
|
|
335
489
|
<CircleDot className="size-5" />
|
|
@@ -357,6 +511,13 @@ interface EditorLessonProps {
|
|
|
357
511
|
lessonId: string;
|
|
358
512
|
}
|
|
359
513
|
|
|
514
|
+
type VideoProfileOption = {
|
|
515
|
+
id: number;
|
|
516
|
+
name: string;
|
|
517
|
+
ffmpeg_params: string;
|
|
518
|
+
status: string;
|
|
519
|
+
};
|
|
520
|
+
|
|
360
521
|
export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
361
522
|
const t = useTranslations('lms.CoursesPage.StructurePage');
|
|
362
523
|
const lesson = useStructureStore((s) =>
|
|
@@ -378,8 +539,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
378
539
|
const videoProviders: { value: VideoProvider; label: string }[] = [
|
|
379
540
|
{ value: 'youtube', label: 'YouTube' },
|
|
380
541
|
{ value: 'vimeo', label: 'Vimeo' },
|
|
381
|
-
{ value: '
|
|
382
|
-
{ value: 'custom', label: t('providers.custom') },
|
|
542
|
+
{ value: 'file_storage', label: t('providers.fileStorage') },
|
|
383
543
|
];
|
|
384
544
|
const questionTypeLabels: Record<QuestionType, string> = {
|
|
385
545
|
multiple_choice: t('questionEditor.types.multipleChoice'),
|
|
@@ -404,7 +564,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
404
564
|
publicDescription: z.string(),
|
|
405
565
|
privateDescription: z.string(),
|
|
406
566
|
videoProvider: z
|
|
407
|
-
.enum(['youtube', 'vimeo', '
|
|
567
|
+
.enum(['youtube', 'vimeo', 'file_storage'] as const)
|
|
408
568
|
.optional(),
|
|
409
569
|
videoUrl: z.string().optional(),
|
|
410
570
|
transcription: z.string().optional(),
|
|
@@ -426,7 +586,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
426
586
|
visibility: lesson?.visibility ?? 'publico',
|
|
427
587
|
publicDescription: lesson?.publicDescription ?? '',
|
|
428
588
|
privateDescription: lesson?.privateDescription ?? '',
|
|
429
|
-
videoProvider:
|
|
589
|
+
videoProvider:
|
|
590
|
+
lesson?.videoProvider === 'youtube' || lesson?.videoProvider === 'vimeo'
|
|
591
|
+
? lesson.videoProvider
|
|
592
|
+
: 'file_storage',
|
|
430
593
|
videoUrl: lesson?.videoUrl ?? '',
|
|
431
594
|
transcription: lesson?.transcription ?? '',
|
|
432
595
|
postContent: lesson?.postContent ?? '',
|
|
@@ -441,6 +604,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
441
604
|
const { isDirty } = form.formState;
|
|
442
605
|
const watchedType = useWatch({ control: form.control, name: 'type' });
|
|
443
606
|
const watchedStatus = useWatch({ control: form.control, name: 'status' });
|
|
607
|
+
const watchedVideoProvider = useWatch({
|
|
608
|
+
control: form.control,
|
|
609
|
+
name: 'videoProvider',
|
|
610
|
+
});
|
|
444
611
|
|
|
445
612
|
useEffect(() => {
|
|
446
613
|
if (!lesson) return;
|
|
@@ -451,9 +618,61 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
451
618
|
const [localResources, setLocalResources] = useState<Resource[]>(
|
|
452
619
|
() => lesson?.resources ?? []
|
|
453
620
|
);
|
|
621
|
+
const [activeTab, setActiveTab] = useState<LessonEditorTab>('dados');
|
|
622
|
+
const [resourcesDirty, setResourcesDirty] = useState(false);
|
|
623
|
+
const [conversionJobId, setConversionJobId] = useState<number | null>(null);
|
|
624
|
+
const [videoUploadError, setVideoUploadError] = useState<string | null>(null);
|
|
454
625
|
const [dragOver, setDragOver] = useState(false);
|
|
455
626
|
const [isUploading, setIsUploading] = useState(false);
|
|
627
|
+
const [originalUploadProgress, setOriginalUploadProgress] = useState<
|
|
628
|
+
number | null
|
|
629
|
+
>(null);
|
|
630
|
+
const [profileUploadProgress, setProfileUploadProgress] = useState<
|
|
631
|
+
Record<number, number>
|
|
632
|
+
>({});
|
|
633
|
+
const [isResolvingVideoPreview, setIsResolvingVideoPreview] = useState(false);
|
|
456
634
|
const resourceInputRef = useRef<HTMLInputElement>(null);
|
|
635
|
+
const originalVideoInputRef = useRef<HTMLInputElement>(null);
|
|
636
|
+
const lastTerminalJobStatusRef = useRef<string | null>(null);
|
|
637
|
+
const [transcriptionSegments, setTranscriptionSegments] = useState<
|
|
638
|
+
TranscriptionSegment[]
|
|
639
|
+
>(() => parseTranscriptionSegments(lesson?.transcription));
|
|
640
|
+
|
|
641
|
+
const {
|
|
642
|
+
data: courseVideoProfiles = [],
|
|
643
|
+
isFetching: isFetchingCourseVideoProfiles,
|
|
644
|
+
isError: hasCourseVideoProfilesError,
|
|
645
|
+
refetch: refetchCourseVideoProfiles,
|
|
646
|
+
} = useQuery<VideoProfileOption[]>({
|
|
647
|
+
queryKey: ['lms-course-video-resolution-profiles', courseId],
|
|
648
|
+
queryFn: async () => {
|
|
649
|
+
const response = await request<VideoProfileOption[]>({
|
|
650
|
+
url: `/lms/courses/${courseId}/video-resolution-profiles`,
|
|
651
|
+
method: 'GET',
|
|
652
|
+
});
|
|
653
|
+
return response.data ?? [];
|
|
654
|
+
},
|
|
655
|
+
enabled: Boolean(courseId),
|
|
656
|
+
initialData: [],
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const {
|
|
660
|
+
data: conversionJob,
|
|
661
|
+
isFetching: isFetchingConversionJob,
|
|
662
|
+
isError: hasConversionJobError,
|
|
663
|
+
refetch: refetchConversionJob,
|
|
664
|
+
} = useQuery<QueueJobResponse>({
|
|
665
|
+
queryKey: ['queue-job', conversionJobId],
|
|
666
|
+
enabled: Boolean(conversionJobId),
|
|
667
|
+
retry: 1,
|
|
668
|
+
queryFn: async () => getQueueJob(request, conversionJobId!),
|
|
669
|
+
refetchInterval: (query) => {
|
|
670
|
+
const status = (query.state.data as QueueJobResponse | undefined)?.status;
|
|
671
|
+
if (!status) return 3000;
|
|
672
|
+
|
|
673
|
+
return ACTIVE_VIDEO_JOB_STATUSES.includes(status) ? 3000 : false;
|
|
674
|
+
},
|
|
675
|
+
});
|
|
457
676
|
|
|
458
677
|
// ── Instructors state ────────────────────────────────────────────────────
|
|
459
678
|
const [selectedInstructorIds, setSelectedInstructorIds] = useState<string[]>(
|
|
@@ -482,14 +701,62 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
482
701
|
|
|
483
702
|
useEffect(() => {
|
|
484
703
|
setLocalResources(lesson?.resources ?? []);
|
|
704
|
+
setResourcesDirty(false);
|
|
705
|
+
setConversionJobId(lesson?.videoConversionJobId ?? null);
|
|
485
706
|
setSelectedInstructorIds(lesson?.instructors?.map((i) => i.id) ?? []);
|
|
486
|
-
|
|
707
|
+
setTranscriptionSegments(parseTranscriptionSegments(lesson?.transcription));
|
|
708
|
+
}, [lesson?.id, lesson?.resources, lesson?.videoConversionJobId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
709
|
+
|
|
710
|
+
useEffect(() => {
|
|
711
|
+
if (watchedType === 'video') return;
|
|
712
|
+
if (activeTab === 'videos' || activeTab === 'transcricao') {
|
|
713
|
+
setActiveTab('conteudo');
|
|
714
|
+
}
|
|
715
|
+
}, [activeTab, watchedType]);
|
|
716
|
+
|
|
717
|
+
useEffect(() => {
|
|
718
|
+
lastTerminalJobStatusRef.current = null;
|
|
719
|
+
}, [conversionJobId]);
|
|
720
|
+
|
|
721
|
+
useEffect(() => {
|
|
722
|
+
if (!conversionJobId || !conversionJob) return;
|
|
723
|
+
if (!TERMINAL_VIDEO_JOB_STATUSES.includes(conversionJob.status)) return;
|
|
724
|
+
|
|
725
|
+
const terminalKey = `${conversionJob.id}:${conversionJob.status}`;
|
|
726
|
+
if (lastTerminalJobStatusRef.current === terminalKey) return;
|
|
727
|
+
|
|
728
|
+
lastTerminalJobStatusRef.current = terminalKey;
|
|
729
|
+
void queryClient.invalidateQueries({
|
|
730
|
+
queryKey: courseStructureQueryKey(courseId),
|
|
731
|
+
});
|
|
732
|
+
}, [conversionJob, conversionJobId, courseId, queryClient]);
|
|
487
733
|
|
|
488
734
|
if (!lesson) return null;
|
|
489
735
|
|
|
490
736
|
const cfg = TYPE_CONFIG[lesson.type];
|
|
491
737
|
const Icon = cfg.icon;
|
|
492
738
|
const lessonTypeLabel = t(cfg.labelKey as any);
|
|
739
|
+
const originalVideoResource =
|
|
740
|
+
localResources.find((res) => res.type === 'video_original') ?? null;
|
|
741
|
+
const profileVideoResources = new Map(
|
|
742
|
+
localResources
|
|
743
|
+
.filter((res) => res.type.startsWith('video_profile:'))
|
|
744
|
+
.map((res) => [Number(res.type.replace('video_profile:', '')), res])
|
|
745
|
+
);
|
|
746
|
+
const genericResources = localResources.filter(
|
|
747
|
+
(res) =>
|
|
748
|
+
res.type !== 'video_original' && !res.type.startsWith('video_profile:')
|
|
749
|
+
);
|
|
750
|
+
const isConversionJobActive = conversionJob
|
|
751
|
+
? ACTIVE_VIDEO_JOB_STATUSES.includes(conversionJob.status)
|
|
752
|
+
: false;
|
|
753
|
+
const latestConversionAttempt =
|
|
754
|
+
conversionJob?.queue_job_attempt.at(-1) ?? null;
|
|
755
|
+
const recentConversionEvents =
|
|
756
|
+
conversionJob?.queue_job_event.slice(-3).reverse() ?? [];
|
|
757
|
+
const isOriginalVideoUploadBlocked =
|
|
758
|
+
originalUploadProgress !== null || isConversionJobActive;
|
|
759
|
+
const isProfileVideoUploadBlocked = isConversionJobActive;
|
|
493
760
|
|
|
494
761
|
async function handleResourceFiles(files: File[]) {
|
|
495
762
|
setIsUploading(true);
|
|
@@ -497,7 +764,8 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
497
764
|
const results = await Promise.allSettled(
|
|
498
765
|
files.map((f) =>
|
|
499
766
|
uploadFile(request, f).then<Resource>((res) => ({
|
|
500
|
-
id:
|
|
767
|
+
id: `new-${res.id}`,
|
|
768
|
+
fileId: res.id,
|
|
501
769
|
name: f.name,
|
|
502
770
|
size: formatFileSize(f.size),
|
|
503
771
|
type: f.type || f.name.split('.').pop() || 'file',
|
|
@@ -518,49 +786,216 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
518
786
|
);
|
|
519
787
|
if (succeeded.length > 0)
|
|
520
788
|
setLocalResources((prev) => [...prev, ...succeeded]);
|
|
789
|
+
if (succeeded.length > 0) setResourcesDirty(true);
|
|
521
790
|
} finally {
|
|
522
791
|
setIsUploading(false);
|
|
523
792
|
}
|
|
524
793
|
}
|
|
525
794
|
|
|
526
795
|
async function removeResource(id: string) {
|
|
527
|
-
const
|
|
528
|
-
|
|
796
|
+
const res = localResources.find((r) => r.id === id);
|
|
797
|
+
const fileId = res?.fileId ?? Number(id);
|
|
798
|
+
if (Number.isInteger(fileId) && fileId > 0) {
|
|
529
799
|
try {
|
|
530
|
-
await deleteFile(request,
|
|
800
|
+
await deleteFile(request, fileId);
|
|
531
801
|
} catch {
|
|
532
802
|
toast.error(t('questionEditor.resourceRemoveError'));
|
|
533
803
|
return;
|
|
534
804
|
}
|
|
535
805
|
} else {
|
|
536
|
-
const res = localResources.find((r) => r.id === id);
|
|
537
806
|
if (res?.url?.startsWith('blob:')) URL.revokeObjectURL(res.url);
|
|
538
807
|
}
|
|
539
808
|
setLocalResources((prev) => prev.filter((r) => r.id !== id));
|
|
809
|
+
setResourcesDirty(true);
|
|
540
810
|
}
|
|
541
811
|
|
|
542
|
-
function
|
|
543
|
-
if (
|
|
812
|
+
async function resolveResourceUrl(res: Resource): Promise<string | null> {
|
|
813
|
+
if (res.url) return res.url;
|
|
814
|
+
const numId = res.fileId ?? Number(res.id);
|
|
815
|
+
if (!Number.isInteger(numId) || numId <= 0) return null;
|
|
816
|
+
try {
|
|
817
|
+
const response = await request<{ url?: string }>({
|
|
818
|
+
url: `/file/open/${numId}`,
|
|
819
|
+
method: 'PUT',
|
|
820
|
+
});
|
|
821
|
+
return response?.data?.url ?? null;
|
|
822
|
+
} catch {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
async function openResource(res: Resource) {
|
|
828
|
+
const url = await resolveResourceUrl(res);
|
|
829
|
+
if (!url) {
|
|
830
|
+
toast.error(t('questionEditor.resourceOpenError'));
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function handleResourceDownload(res: Resource) {
|
|
837
|
+
const resourceUrl = await resolveResourceUrl(res);
|
|
838
|
+
if (!resourceUrl) {
|
|
544
839
|
toast(t('questionEditor.resourceDownloadSoon', { name: res.name }));
|
|
545
840
|
return;
|
|
546
841
|
}
|
|
547
842
|
const a = document.createElement('a');
|
|
548
|
-
a.href =
|
|
843
|
+
a.href = resourceUrl;
|
|
549
844
|
a.download = res.name;
|
|
550
845
|
a.click();
|
|
551
846
|
}
|
|
552
847
|
|
|
848
|
+
async function openVideoPreview(res: Resource) {
|
|
849
|
+
setIsResolvingVideoPreview(true);
|
|
850
|
+
try {
|
|
851
|
+
const resourceUrl = await resolveResourceUrl(res);
|
|
852
|
+
if (!resourceUrl) {
|
|
853
|
+
toast.error(t('questionEditor.resourceOpenError'));
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
window.open(resourceUrl, '_blank', 'noopener,noreferrer');
|
|
857
|
+
} finally {
|
|
858
|
+
setIsResolvingVideoPreview(false);
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function handleVideoProfileFile(profileId: number, file: File) {
|
|
863
|
+
if (file.size > MAX_VIDEO_UPLOAD_SIZE_BYTES) {
|
|
864
|
+
const message = t('lessonForm.videoUploadMaxSizeError', {
|
|
865
|
+
size: '100MB',
|
|
866
|
+
});
|
|
867
|
+
setVideoUploadError(message);
|
|
868
|
+
toast.error(message);
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
setVideoUploadError(null);
|
|
873
|
+
setProfileUploadProgress((prev) => ({ ...prev, [profileId]: 0 }));
|
|
874
|
+
try {
|
|
875
|
+
const uploaded = await uploadFile(request, file, 'lms/lessons/videos', {
|
|
876
|
+
onUploadProgress: (event) => {
|
|
877
|
+
const total = event.total ?? 0;
|
|
878
|
+
const progress =
|
|
879
|
+
total > 0 ? Math.round((event.loaded / total) * 100) : 0;
|
|
880
|
+
setProfileUploadProgress((prev) => ({
|
|
881
|
+
...prev,
|
|
882
|
+
[profileId]: progress,
|
|
883
|
+
}));
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
const type = videoProfileResourceType(profileId);
|
|
887
|
+
const resource: Resource = {
|
|
888
|
+
id: `new-${uploaded.id}`,
|
|
889
|
+
fileId: uploaded.id,
|
|
890
|
+
name: file.name,
|
|
891
|
+
size: formatFileSize(file.size),
|
|
892
|
+
type,
|
|
893
|
+
public: false,
|
|
894
|
+
url: undefined,
|
|
895
|
+
};
|
|
896
|
+
setLocalResources((prev) => [
|
|
897
|
+
...prev.filter((item) => item.type !== type),
|
|
898
|
+
resource,
|
|
899
|
+
]);
|
|
900
|
+
setResourcesDirty(true);
|
|
901
|
+
} catch {
|
|
902
|
+
toast.error(t('questionEditor.videoUploadFailed', { count: 1 }));
|
|
903
|
+
} finally {
|
|
904
|
+
setProfileUploadProgress((prev) => {
|
|
905
|
+
const next = { ...prev };
|
|
906
|
+
delete next[profileId];
|
|
907
|
+
return next;
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function handleOriginalVideoFile(file: File) {
|
|
913
|
+
if (file.size > MAX_VIDEO_UPLOAD_SIZE_BYTES) {
|
|
914
|
+
const message = t('lessonForm.videoUploadMaxSizeError', {
|
|
915
|
+
size: '100MB',
|
|
916
|
+
});
|
|
917
|
+
setVideoUploadError(message);
|
|
918
|
+
toast.error(message);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
setVideoUploadError(null);
|
|
923
|
+
setOriginalUploadProgress(0);
|
|
924
|
+
try {
|
|
925
|
+
const uploaded = await uploadFile(
|
|
926
|
+
request,
|
|
927
|
+
file,
|
|
928
|
+
'lms/lessons/originals',
|
|
929
|
+
{
|
|
930
|
+
onUploadProgress: (event) => {
|
|
931
|
+
const total = event.total ?? 0;
|
|
932
|
+
const progress =
|
|
933
|
+
total > 0 ? Math.round((event.loaded / total) * 100) : 0;
|
|
934
|
+
setOriginalUploadProgress(progress);
|
|
935
|
+
},
|
|
936
|
+
}
|
|
937
|
+
);
|
|
938
|
+
const originalResource: Resource = {
|
|
939
|
+
id: `new-${uploaded.id}`,
|
|
940
|
+
fileId: uploaded.id,
|
|
941
|
+
name: file.name,
|
|
942
|
+
size: formatFileSize(file.size),
|
|
943
|
+
type: 'video_original',
|
|
944
|
+
public: false,
|
|
945
|
+
url: undefined,
|
|
946
|
+
};
|
|
947
|
+
setLocalResources((prev) => [
|
|
948
|
+
...prev.filter((item) => item.type !== 'video_original'),
|
|
949
|
+
originalResource,
|
|
950
|
+
]);
|
|
951
|
+
setResourcesDirty(true);
|
|
952
|
+
|
|
953
|
+
const queued = await enqueueLessonVideoConversion(
|
|
954
|
+
request,
|
|
955
|
+
courseId,
|
|
956
|
+
lesson!.sessionId,
|
|
957
|
+
lessonId,
|
|
958
|
+
uploaded.id
|
|
959
|
+
);
|
|
960
|
+
setConversionJobId(queued.queueJobId);
|
|
961
|
+
toast.success(
|
|
962
|
+
t('lessonForm.videoConversionQueued', {
|
|
963
|
+
id: queued.queueJobId,
|
|
964
|
+
})
|
|
965
|
+
);
|
|
966
|
+
void queryClient.invalidateQueries({
|
|
967
|
+
queryKey: courseStructureQueryKey(courseId),
|
|
968
|
+
});
|
|
969
|
+
} catch {
|
|
970
|
+
toast.error(t('lessonForm.videoConversionFailed'));
|
|
971
|
+
} finally {
|
|
972
|
+
setOriginalUploadProgress(null);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
553
976
|
function onSubmit(values: FormValues) {
|
|
977
|
+
const transcriptionValue =
|
|
978
|
+
values.type === 'video'
|
|
979
|
+
? serializeTranscriptionSegments(transcriptionSegments)
|
|
980
|
+
: values.transcription;
|
|
981
|
+
|
|
554
982
|
updateLesson.mutate({
|
|
555
983
|
lessonId,
|
|
556
984
|
sessionId: lesson!.sessionId,
|
|
557
985
|
formValues: {
|
|
558
986
|
...values,
|
|
987
|
+
videoUrl:
|
|
988
|
+
values.type === 'video' && values.videoProvider === 'file_storage'
|
|
989
|
+
? ''
|
|
990
|
+
: values.videoUrl,
|
|
991
|
+
transcription: transcriptionValue,
|
|
992
|
+
videoConversionJobId: conversionJobId ?? lesson?.videoConversionJobId,
|
|
559
993
|
resources: localResources,
|
|
560
994
|
instructorIds: selectedInstructorIds.map(Number),
|
|
561
995
|
},
|
|
562
996
|
});
|
|
563
|
-
form.reset(values);
|
|
997
|
+
form.reset({ ...values, transcription: transcriptionValue });
|
|
998
|
+
setResourcesDirty(false);
|
|
564
999
|
}
|
|
565
1000
|
|
|
566
1001
|
// ── Question sheet helpers ────────────────────────────────────────────────
|
|
@@ -684,14 +1119,14 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
684
1119
|
className="flex flex-col h-full min-h-0"
|
|
685
1120
|
>
|
|
686
1121
|
{/* ── Header ───────────────────────────────────────────────────────── */}
|
|
687
|
-
<div className="flex items-center gap-
|
|
1122
|
+
<div className="flex items-center gap-2 border-b bg-muted/30 px-2 py-2 shrink-0 sm:gap-3 sm:px-4 sm:py-3">
|
|
688
1123
|
<div
|
|
689
1124
|
className={cn(
|
|
690
|
-
'flex size-
|
|
1125
|
+
'flex size-8 items-center justify-center rounded-md shrink-0 sm:size-9 sm:rounded-lg',
|
|
691
1126
|
cfg.bg
|
|
692
1127
|
)}
|
|
693
1128
|
>
|
|
694
|
-
<Icon className={cn('size-4', cfg.color)} />
|
|
1129
|
+
<Icon className={cn('size-3.5 sm:size-4', cfg.color)} />
|
|
695
1130
|
</div>
|
|
696
1131
|
<div className="flex-1 min-w-0">
|
|
697
1132
|
<div className="flex items-center gap-1.5">
|
|
@@ -735,15 +1170,44 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
735
1170
|
</div>
|
|
736
1171
|
|
|
737
1172
|
{/* ── Tabs ─────────────────────────────────────────────────────────── */}
|
|
738
|
-
<Tabs
|
|
739
|
-
|
|
740
|
-
|
|
1173
|
+
<Tabs
|
|
1174
|
+
value={activeTab}
|
|
1175
|
+
onValueChange={(value) => setActiveTab(value as LessonEditorTab)}
|
|
1176
|
+
className="flex flex-col flex-1 min-h-0 min-w-0"
|
|
1177
|
+
>
|
|
1178
|
+
<TabsList className="mx-2 mt-1.5 h-auto w-[calc(100%-1rem)] justify-start shrink-0 bg-muted/50 overflow-x-auto overflow-y-hidden whitespace-nowrap sm:mx-3 sm:mt-2 sm:w-auto">
|
|
1179
|
+
<TabsTrigger
|
|
1180
|
+
value="dados"
|
|
1181
|
+
className="h-6 px-2 text-[11px] shrink-0 sm:h-7 sm:px-2.5 sm:text-xs"
|
|
1182
|
+
>
|
|
741
1183
|
{t('lessonForm.tabData')}
|
|
742
1184
|
</TabsTrigger>
|
|
743
|
-
<TabsTrigger
|
|
1185
|
+
<TabsTrigger
|
|
1186
|
+
value="conteudo"
|
|
1187
|
+
className="h-6 px-2 text-[11px] shrink-0 sm:h-7 sm:px-2.5 sm:text-xs"
|
|
1188
|
+
>
|
|
744
1189
|
{t('lessonForm.postContent')}
|
|
745
1190
|
</TabsTrigger>
|
|
746
|
-
|
|
1191
|
+
{watchedType === 'video' && (
|
|
1192
|
+
<TabsTrigger
|
|
1193
|
+
value="videos"
|
|
1194
|
+
className="h-6 px-2 text-[11px] shrink-0 sm:h-7 sm:px-2.5 sm:text-xs"
|
|
1195
|
+
>
|
|
1196
|
+
{t('lessonForm.tabVideos')}
|
|
1197
|
+
</TabsTrigger>
|
|
1198
|
+
)}
|
|
1199
|
+
{watchedType === 'video' && (
|
|
1200
|
+
<TabsTrigger
|
|
1201
|
+
value="transcricao"
|
|
1202
|
+
className="h-6 px-2 text-[11px] shrink-0 sm:h-7 sm:px-2.5 sm:text-xs"
|
|
1203
|
+
>
|
|
1204
|
+
{t('lessonForm.tabTranscription')}
|
|
1205
|
+
</TabsTrigger>
|
|
1206
|
+
)}
|
|
1207
|
+
<TabsTrigger
|
|
1208
|
+
value="recursos"
|
|
1209
|
+
className="h-6 px-2 text-[11px] shrink-0 sm:h-7 sm:px-2.5 sm:text-xs"
|
|
1210
|
+
>
|
|
747
1211
|
{t('lessonForm.tabResources')}
|
|
748
1212
|
</TabsTrigger>
|
|
749
1213
|
</TabsList>
|
|
@@ -751,7 +1215,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
751
1215
|
{/* ── Tab Dados ────────────────────────────────────────────────── */}
|
|
752
1216
|
<TabsContent value="dados" className="flex-1 min-h-0 mt-0">
|
|
753
1217
|
<ScrollArea className="h-full">
|
|
754
|
-
<div className="flex flex-col gap-3 p-3">
|
|
1218
|
+
<div className="flex flex-col gap-2 p-2 sm:gap-3 sm:p-3">
|
|
755
1219
|
{/* Identificação */}
|
|
756
1220
|
<Card className="bg-muted/20 py-2 gap-2">
|
|
757
1221
|
<CardHeader className="px-3 pt-2 pb-0">
|
|
@@ -760,7 +1224,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
760
1224
|
</CardTitle>
|
|
761
1225
|
</CardHeader>
|
|
762
1226
|
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
763
|
-
<div className="grid grid-cols-
|
|
1227
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-3">
|
|
764
1228
|
<FormField
|
|
765
1229
|
control={form.control}
|
|
766
1230
|
name="code"
|
|
@@ -869,7 +1333,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
869
1333
|
</CardTitle>
|
|
870
1334
|
</CardHeader>
|
|
871
1335
|
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
872
|
-
<div className="grid grid-cols-
|
|
1336
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
|
873
1337
|
<FormField
|
|
874
1338
|
control={form.control}
|
|
875
1339
|
name="status"
|
|
@@ -901,10 +1365,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
901
1365
|
STATUS_COLORS[val]
|
|
902
1366
|
)}
|
|
903
1367
|
>
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
1368
|
+
{lbl}
|
|
1369
|
+
</span>
|
|
1370
|
+
</SelectItem>
|
|
1371
|
+
))}
|
|
908
1372
|
</SelectContent>
|
|
909
1373
|
</Select>
|
|
910
1374
|
<FormMessage className="text-xs" />
|
|
@@ -932,17 +1396,20 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
932
1396
|
<SelectContent>
|
|
933
1397
|
<SelectItem value="publico">
|
|
934
1398
|
<span className="flex items-center gap-1.5">
|
|
935
|
-
<Eye className="size-3" />
|
|
1399
|
+
<Eye className="size-3" />{' '}
|
|
1400
|
+
{t('lessonForm.public')}
|
|
936
1401
|
</span>
|
|
937
1402
|
</SelectItem>
|
|
938
1403
|
<SelectItem value="privado">
|
|
939
1404
|
<span className="flex items-center gap-1.5">
|
|
940
|
-
<EyeOff className="size-3" />
|
|
1405
|
+
<EyeOff className="size-3" />{' '}
|
|
1406
|
+
{t('lessonForm.private')}
|
|
941
1407
|
</span>
|
|
942
1408
|
</SelectItem>
|
|
943
1409
|
<SelectItem value="restrito">
|
|
944
1410
|
<span className="flex items-center gap-1.5">
|
|
945
|
-
<Lock className="size-3" />
|
|
1411
|
+
<Lock className="size-3" />{' '}
|
|
1412
|
+
{t('questionEditor.restricted')}
|
|
946
1413
|
</span>
|
|
947
1414
|
</SelectItem>
|
|
948
1415
|
</SelectContent>
|
|
@@ -1024,9 +1491,12 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1024
1491
|
prev.filter((id) => id !== sid)
|
|
1025
1492
|
)
|
|
1026
1493
|
}
|
|
1027
|
-
aria-label={t(
|
|
1028
|
-
|
|
1029
|
-
|
|
1494
|
+
aria-label={t(
|
|
1495
|
+
'questionEditor.removeInstructor',
|
|
1496
|
+
{
|
|
1497
|
+
name: displayName,
|
|
1498
|
+
}
|
|
1499
|
+
)}
|
|
1030
1500
|
>
|
|
1031
1501
|
<X className="size-3" />
|
|
1032
1502
|
</Button>
|
|
@@ -1044,12 +1514,13 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1044
1514
|
{/* ── Tab Conteúdo ─────────────────────────────────────────────── */}
|
|
1045
1515
|
<TabsContent value="conteudo" className="flex-1 min-h-0 mt-0">
|
|
1046
1516
|
<ScrollArea className="h-full">
|
|
1047
|
-
<div className="flex flex-col gap-3 p-3">
|
|
1517
|
+
<div className="flex flex-col gap-2 p-2 sm:gap-3 sm:p-3">
|
|
1048
1518
|
{/* Descrição pública */}
|
|
1049
1519
|
<Card className="bg-muted/20 py-2 gap-2">
|
|
1050
1520
|
<CardHeader className="px-3 pt-2 pb-1">
|
|
1051
1521
|
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
1052
|
-
<Eye className="size-3" />
|
|
1522
|
+
<Eye className="size-3" />{' '}
|
|
1523
|
+
{t('lessonForm.publicDescription')}
|
|
1053
1524
|
</CardTitle>
|
|
1054
1525
|
</CardHeader>
|
|
1055
1526
|
<CardContent className="px-3 pb-2">
|
|
@@ -1075,7 +1546,8 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1075
1546
|
<Card className="bg-muted/20 py-2 gap-2">
|
|
1076
1547
|
<CardHeader className="px-3 pt-2 pb-1">
|
|
1077
1548
|
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
1078
|
-
<Lock className="size-3" />
|
|
1549
|
+
<Lock className="size-3" />{' '}
|
|
1550
|
+
{t('lessonForm.privateDescription')}
|
|
1079
1551
|
</CardTitle>
|
|
1080
1552
|
</CardHeader>
|
|
1081
1553
|
<CardContent className="px-3 pb-2">
|
|
@@ -1099,90 +1571,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1099
1571
|
</Card>
|
|
1100
1572
|
|
|
1101
1573
|
{/* Campos específicos por tipo */}
|
|
1102
|
-
{watchedType === 'video' && (
|
|
1103
|
-
<Card className="bg-muted/20 py-2 gap-2">
|
|
1104
|
-
<CardHeader className="px-3 pt-2 pb-1">
|
|
1105
|
-
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
1106
|
-
<Video className="size-3 text-blue-500" /> {t('types.video')}
|
|
1107
|
-
</CardTitle>
|
|
1108
|
-
</CardHeader>
|
|
1109
|
-
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
1110
|
-
<FormField
|
|
1111
|
-
control={form.control}
|
|
1112
|
-
name="videoProvider"
|
|
1113
|
-
render={({ field }) => (
|
|
1114
|
-
<FormItem>
|
|
1115
|
-
<FormLabel className="text-xs">
|
|
1116
|
-
{t('lessonForm.videoProvider')}
|
|
1117
|
-
</FormLabel>
|
|
1118
|
-
<Select
|
|
1119
|
-
value={field.value}
|
|
1120
|
-
onValueChange={field.onChange}
|
|
1121
|
-
>
|
|
1122
|
-
<FormControl>
|
|
1123
|
-
<SelectTrigger className="h-8 text-xs w-full">
|
|
1124
|
-
<SelectValue />
|
|
1125
|
-
</SelectTrigger>
|
|
1126
|
-
</FormControl>
|
|
1127
|
-
<SelectContent>
|
|
1128
|
-
{videoProviders.map((p) => (
|
|
1129
|
-
<SelectItem key={p.value} value={p.value}>
|
|
1130
|
-
{p.label}
|
|
1131
|
-
</SelectItem>
|
|
1132
|
-
))}
|
|
1133
|
-
</SelectContent>
|
|
1134
|
-
</Select>
|
|
1135
|
-
<FormMessage className="text-xs" />
|
|
1136
|
-
</FormItem>
|
|
1137
|
-
)}
|
|
1138
|
-
/>
|
|
1139
|
-
|
|
1140
|
-
<FormField
|
|
1141
|
-
control={form.control}
|
|
1142
|
-
name="videoUrl"
|
|
1143
|
-
render={({ field }) => (
|
|
1144
|
-
<FormItem>
|
|
1145
|
-
<FormLabel className="text-xs">
|
|
1146
|
-
{t('lessonForm.videoUrl')}
|
|
1147
|
-
</FormLabel>
|
|
1148
|
-
<FormControl>
|
|
1149
|
-
<Input
|
|
1150
|
-
{...field}
|
|
1151
|
-
value={field.value ?? ''}
|
|
1152
|
-
className="h-8 text-xs font-mono"
|
|
1153
|
-
placeholder={t('lessonForm.videoUrlPlaceholder')}
|
|
1154
|
-
/>
|
|
1155
|
-
</FormControl>
|
|
1156
|
-
<FormMessage className="text-xs" />
|
|
1157
|
-
</FormItem>
|
|
1158
|
-
)}
|
|
1159
|
-
/>
|
|
1160
|
-
|
|
1161
|
-
<FormField
|
|
1162
|
-
control={form.control}
|
|
1163
|
-
name="transcription"
|
|
1164
|
-
render={({ field }) => (
|
|
1165
|
-
<FormItem>
|
|
1166
|
-
<FormLabel className="text-xs">
|
|
1167
|
-
{t('lessonForm.tabTranscription')}
|
|
1168
|
-
</FormLabel>
|
|
1169
|
-
<FormControl>
|
|
1170
|
-
<Textarea
|
|
1171
|
-
{...field}
|
|
1172
|
-
value={field.value ?? ''}
|
|
1173
|
-
rows={5}
|
|
1174
|
-
className="text-xs resize-none font-mono"
|
|
1175
|
-
placeholder={t('lessonForm.transcriptionPlaceholder')}
|
|
1176
|
-
/>
|
|
1177
|
-
</FormControl>
|
|
1178
|
-
<FormMessage className="text-xs" />
|
|
1179
|
-
</FormItem>
|
|
1180
|
-
)}
|
|
1181
|
-
/>
|
|
1182
|
-
</CardContent>
|
|
1183
|
-
</Card>
|
|
1184
|
-
)}
|
|
1185
|
-
|
|
1186
1574
|
{watchedType === 'post' && (
|
|
1187
1575
|
<Card className="bg-muted/20 py-2 gap-2">
|
|
1188
1576
|
<CardHeader className="px-3 pt-2 pb-1">
|
|
@@ -1248,8 +1636,12 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1248
1636
|
setSelectedQuestion(found);
|
|
1249
1637
|
}}
|
|
1250
1638
|
placeholder={t('questionEditor.selectQuestion')}
|
|
1251
|
-
searchPlaceholder={t(
|
|
1252
|
-
|
|
1639
|
+
searchPlaceholder={t(
|
|
1640
|
+
'questionEditor.searchQuestion'
|
|
1641
|
+
)}
|
|
1642
|
+
emptyLabel={t(
|
|
1643
|
+
'questionEditor.noQuestionsFound'
|
|
1644
|
+
)}
|
|
1253
1645
|
entityLabel={t('questionEditor.questionEntity')}
|
|
1254
1646
|
options={MOCK_QUESTIONS}
|
|
1255
1647
|
getOptionValue={(o) => o.id}
|
|
@@ -1295,10 +1687,794 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1295
1687
|
</ScrollArea>
|
|
1296
1688
|
</TabsContent>
|
|
1297
1689
|
|
|
1690
|
+
{watchedType === 'video' && (
|
|
1691
|
+
<TabsContent value="videos" className="flex-1 min-h-0 mt-0">
|
|
1692
|
+
<ScrollArea className="h-full">
|
|
1693
|
+
<div className="flex flex-col gap-2 p-2 sm:gap-3 sm:p-3">
|
|
1694
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
1695
|
+
<CardHeader className="px-3 pt-2 pb-1">
|
|
1696
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
1697
|
+
<Video className="size-3 text-blue-500" />{' '}
|
|
1698
|
+
{t('lessonForm.tabVideos')}
|
|
1699
|
+
</CardTitle>
|
|
1700
|
+
</CardHeader>
|
|
1701
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
1702
|
+
<FormField
|
|
1703
|
+
control={form.control}
|
|
1704
|
+
name="videoProvider"
|
|
1705
|
+
render={({ field }) => (
|
|
1706
|
+
<FormItem>
|
|
1707
|
+
<FormLabel className="text-xs">
|
|
1708
|
+
{t('lessonForm.videoProvider')}
|
|
1709
|
+
</FormLabel>
|
|
1710
|
+
<Select
|
|
1711
|
+
value={field.value}
|
|
1712
|
+
onValueChange={field.onChange}
|
|
1713
|
+
>
|
|
1714
|
+
<FormControl>
|
|
1715
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
1716
|
+
<SelectValue />
|
|
1717
|
+
</SelectTrigger>
|
|
1718
|
+
</FormControl>
|
|
1719
|
+
<SelectContent>
|
|
1720
|
+
{videoProviders.map((p) => (
|
|
1721
|
+
<SelectItem key={p.value} value={p.value}>
|
|
1722
|
+
{p.label}
|
|
1723
|
+
</SelectItem>
|
|
1724
|
+
))}
|
|
1725
|
+
</SelectContent>
|
|
1726
|
+
</Select>
|
|
1727
|
+
<FormMessage className="text-xs" />
|
|
1728
|
+
</FormItem>
|
|
1729
|
+
)}
|
|
1730
|
+
/>
|
|
1731
|
+
|
|
1732
|
+
{watchedVideoProvider !== 'file_storage' ? (
|
|
1733
|
+
<FormField
|
|
1734
|
+
control={form.control}
|
|
1735
|
+
name="videoUrl"
|
|
1736
|
+
render={({ field }) => (
|
|
1737
|
+
<FormItem>
|
|
1738
|
+
<FormLabel className="text-xs">
|
|
1739
|
+
{t('lessonForm.videoUrl')}
|
|
1740
|
+
</FormLabel>
|
|
1741
|
+
<FormControl>
|
|
1742
|
+
<Input
|
|
1743
|
+
{...field}
|
|
1744
|
+
value={field.value ?? ''}
|
|
1745
|
+
className="h-8 text-xs font-mono"
|
|
1746
|
+
placeholder={t(
|
|
1747
|
+
'lessonForm.videoUrlPlaceholder'
|
|
1748
|
+
)}
|
|
1749
|
+
/>
|
|
1750
|
+
</FormControl>
|
|
1751
|
+
<FormMessage className="text-xs" />
|
|
1752
|
+
</FormItem>
|
|
1753
|
+
)}
|
|
1754
|
+
/>
|
|
1755
|
+
) : (
|
|
1756
|
+
<p className="text-xs text-muted-foreground">
|
|
1757
|
+
{t('lessonForm.fileStorageVideoHint')}
|
|
1758
|
+
</p>
|
|
1759
|
+
)}
|
|
1760
|
+
</CardContent>
|
|
1761
|
+
</Card>
|
|
1762
|
+
|
|
1763
|
+
{watchedVideoProvider === 'file_storage' && (
|
|
1764
|
+
<>
|
|
1765
|
+
{videoUploadError ? (
|
|
1766
|
+
<p className="text-xs text-destructive">
|
|
1767
|
+
{videoUploadError}
|
|
1768
|
+
</p>
|
|
1769
|
+
) : null}
|
|
1770
|
+
|
|
1771
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
1772
|
+
<CardHeader className="px-3 pt-2 pb-1">
|
|
1773
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
1774
|
+
{t('lessonForm.originalVideoTitle')}
|
|
1775
|
+
</CardTitle>
|
|
1776
|
+
</CardHeader>
|
|
1777
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
1778
|
+
<div className="rounded-lg border bg-background/90 p-3 shadow-sm">
|
|
1779
|
+
<div className="flex items-start gap-3">
|
|
1780
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-blue-600">
|
|
1781
|
+
<Video className="size-4" />
|
|
1782
|
+
</div>
|
|
1783
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
1784
|
+
<div className="flex items-start justify-between gap-2">
|
|
1785
|
+
<div className="min-w-0">
|
|
1786
|
+
<p className="truncate text-sm font-medium">
|
|
1787
|
+
{originalVideoResource
|
|
1788
|
+
? originalVideoResource.name
|
|
1789
|
+
: t('lessonForm.originalVideoTitle')}
|
|
1790
|
+
</p>
|
|
1791
|
+
<p className="text-xs text-muted-foreground">
|
|
1792
|
+
{conversionJobId
|
|
1793
|
+
? t('lessonForm.videoConversionJob', {
|
|
1794
|
+
id: conversionJobId,
|
|
1795
|
+
})
|
|
1796
|
+
: t('lessonForm.originalVideoHint')}
|
|
1797
|
+
</p>
|
|
1798
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
1799
|
+
{t('lessonForm.originalVideoPurpose')}
|
|
1800
|
+
</p>
|
|
1801
|
+
</div>
|
|
1802
|
+
{originalVideoResource && (
|
|
1803
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
1804
|
+
<Button
|
|
1805
|
+
type="button"
|
|
1806
|
+
variant="ghost"
|
|
1807
|
+
size="icon"
|
|
1808
|
+
className="size-7 shrink-0"
|
|
1809
|
+
disabled={isResolvingVideoPreview}
|
|
1810
|
+
onClick={() =>
|
|
1811
|
+
void openVideoPreview(
|
|
1812
|
+
originalVideoResource
|
|
1813
|
+
)
|
|
1814
|
+
}
|
|
1815
|
+
aria-label={t(
|
|
1816
|
+
'lessonForm.playVideoAria',
|
|
1817
|
+
{
|
|
1818
|
+
name: originalVideoResource.name,
|
|
1819
|
+
}
|
|
1820
|
+
)}
|
|
1821
|
+
>
|
|
1822
|
+
{isResolvingVideoPreview ? (
|
|
1823
|
+
<Loader2 className="size-3 animate-spin" />
|
|
1824
|
+
) : (
|
|
1825
|
+
<Play className="size-3" />
|
|
1826
|
+
)}
|
|
1827
|
+
</Button>
|
|
1828
|
+
<Button
|
|
1829
|
+
type="button"
|
|
1830
|
+
variant="ghost"
|
|
1831
|
+
size="icon"
|
|
1832
|
+
className="size-7 shrink-0"
|
|
1833
|
+
onClick={() =>
|
|
1834
|
+
void handleResourceDownload(
|
|
1835
|
+
originalVideoResource
|
|
1836
|
+
)
|
|
1837
|
+
}
|
|
1838
|
+
aria-label={t(
|
|
1839
|
+
'lessonForm.downloadVideoAria',
|
|
1840
|
+
{
|
|
1841
|
+
name: originalVideoResource.name,
|
|
1842
|
+
}
|
|
1843
|
+
)}
|
|
1844
|
+
>
|
|
1845
|
+
<Download className="size-3" />
|
|
1846
|
+
</Button>
|
|
1847
|
+
<Button
|
|
1848
|
+
type="button"
|
|
1849
|
+
variant="ghost"
|
|
1850
|
+
size="icon"
|
|
1851
|
+
className="size-7 shrink-0"
|
|
1852
|
+
onClick={() =>
|
|
1853
|
+
void openResource(
|
|
1854
|
+
originalVideoResource
|
|
1855
|
+
)
|
|
1856
|
+
}
|
|
1857
|
+
aria-label={t(
|
|
1858
|
+
'lessonForm.openVideoAria',
|
|
1859
|
+
{
|
|
1860
|
+
name: originalVideoResource.name,
|
|
1861
|
+
}
|
|
1862
|
+
)}
|
|
1863
|
+
>
|
|
1864
|
+
<ExternalLink className="size-3" />
|
|
1865
|
+
</Button>
|
|
1866
|
+
</div>
|
|
1867
|
+
)}
|
|
1868
|
+
</div>
|
|
1869
|
+
{originalVideoResource?.size ? (
|
|
1870
|
+
<div className="inline-flex rounded-full bg-muted px-2 py-0.5 text-[0.65rem] font-medium text-muted-foreground">
|
|
1871
|
+
{originalVideoResource.size}
|
|
1872
|
+
</div>
|
|
1873
|
+
) : null}
|
|
1874
|
+
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
1875
|
+
<Button
|
|
1876
|
+
type="button"
|
|
1877
|
+
variant="secondary"
|
|
1878
|
+
className="h-8 px-3 text-xs"
|
|
1879
|
+
disabled={isOriginalVideoUploadBlocked}
|
|
1880
|
+
onClick={() =>
|
|
1881
|
+
originalVideoInputRef.current?.click()
|
|
1882
|
+
}
|
|
1883
|
+
>
|
|
1884
|
+
<UploadCloud className="size-3.5 mr-1" />
|
|
1885
|
+
{t(
|
|
1886
|
+
'lessonForm.uploadOriginalForConversion'
|
|
1887
|
+
)}
|
|
1888
|
+
</Button>
|
|
1889
|
+
<span className="text-[0.65rem] text-muted-foreground">
|
|
1890
|
+
{isConversionJobActive
|
|
1891
|
+
? t(
|
|
1892
|
+
'lessonForm.videoUploadBlockedWhileProcessing'
|
|
1893
|
+
)
|
|
1894
|
+
: t('lessonForm.originalVideoHint')}
|
|
1895
|
+
</span>
|
|
1896
|
+
</div>
|
|
1897
|
+
{originalUploadProgress !== null ? (
|
|
1898
|
+
<div className="space-y-1 pt-1">
|
|
1899
|
+
<Progress
|
|
1900
|
+
value={originalUploadProgress}
|
|
1901
|
+
className="h-1.5"
|
|
1902
|
+
/>
|
|
1903
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
1904
|
+
{originalUploadProgress}%
|
|
1905
|
+
</p>
|
|
1906
|
+
</div>
|
|
1907
|
+
) : null}
|
|
1908
|
+
</div>
|
|
1909
|
+
</div>
|
|
1910
|
+
<input
|
|
1911
|
+
ref={originalVideoInputRef}
|
|
1912
|
+
type="file"
|
|
1913
|
+
accept="video/*"
|
|
1914
|
+
className="hidden"
|
|
1915
|
+
onChange={(event) => {
|
|
1916
|
+
const file = event.target.files?.[0];
|
|
1917
|
+
if (file && !isOriginalVideoUploadBlocked) {
|
|
1918
|
+
void handleOriginalVideoFile(file);
|
|
1919
|
+
}
|
|
1920
|
+
event.target.value = '';
|
|
1921
|
+
}}
|
|
1922
|
+
/>
|
|
1923
|
+
</div>
|
|
1924
|
+
</CardContent>
|
|
1925
|
+
</Card>
|
|
1926
|
+
|
|
1927
|
+
{conversionJobId ? (
|
|
1928
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
1929
|
+
<CardHeader className="px-3 pt-2 pb-1">
|
|
1930
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center justify-between gap-2">
|
|
1931
|
+
<span>
|
|
1932
|
+
{t('lessonForm.videoJobFeedbackTitle')}
|
|
1933
|
+
</span>
|
|
1934
|
+
{conversionJob ? (
|
|
1935
|
+
<span
|
|
1936
|
+
className={cn(
|
|
1937
|
+
'rounded-full px-2 py-0.5 text-[0.65rem] font-medium',
|
|
1938
|
+
VIDEO_JOB_STATUS_COLORS[
|
|
1939
|
+
conversionJob.status
|
|
1940
|
+
]
|
|
1941
|
+
)}
|
|
1942
|
+
>
|
|
1943
|
+
{t(
|
|
1944
|
+
`lessonForm.videoJobStatuses.${conversionJob.status}` as any
|
|
1945
|
+
)}
|
|
1946
|
+
</span>
|
|
1947
|
+
) : null}
|
|
1948
|
+
</CardTitle>
|
|
1949
|
+
</CardHeader>
|
|
1950
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
1951
|
+
{hasConversionJobError ? (
|
|
1952
|
+
<div className="flex flex-col gap-2 rounded-md border border-destructive/30 bg-destructive/5 p-3">
|
|
1953
|
+
<p className="text-xs text-destructive">
|
|
1954
|
+
{t('lessonForm.videoJobLoadError')}
|
|
1955
|
+
</p>
|
|
1956
|
+
<Button
|
|
1957
|
+
type="button"
|
|
1958
|
+
variant="outline"
|
|
1959
|
+
size="sm"
|
|
1960
|
+
className="h-7 w-fit px-2 text-xs"
|
|
1961
|
+
onClick={() => void refetchConversionJob()}
|
|
1962
|
+
>
|
|
1963
|
+
<RefreshCw className="size-3 mr-1" />
|
|
1964
|
+
{t('lessonForm.retryLoadVideoJob')}
|
|
1965
|
+
</Button>
|
|
1966
|
+
</div>
|
|
1967
|
+
) : !conversionJob ? (
|
|
1968
|
+
<div className="flex items-center gap-2 rounded-md border bg-background/70 px-3 py-2 text-xs text-muted-foreground">
|
|
1969
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
1970
|
+
{isFetchingConversionJob
|
|
1971
|
+
? t('lessonForm.videoJobLoading')
|
|
1972
|
+
: t('lessonForm.videoJobPendingLoad')}
|
|
1973
|
+
</div>
|
|
1974
|
+
) : (
|
|
1975
|
+
<>
|
|
1976
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 xl:grid-cols-4">
|
|
1977
|
+
<div className="rounded-md border bg-background/70 p-2">
|
|
1978
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
1979
|
+
{t('lessonForm.videoJobIdLabel')}
|
|
1980
|
+
</p>
|
|
1981
|
+
<p className="text-xs font-medium">
|
|
1982
|
+
#{conversionJob.id}
|
|
1983
|
+
</p>
|
|
1984
|
+
</div>
|
|
1985
|
+
<div className="rounded-md border bg-background/70 p-2">
|
|
1986
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
1987
|
+
{t('lessonForm.videoJobAttemptsLabel')}
|
|
1988
|
+
</p>
|
|
1989
|
+
<p className="text-xs font-medium">
|
|
1990
|
+
{t('lessonForm.videoJobAttemptsValue', {
|
|
1991
|
+
current: conversionJob.attempts,
|
|
1992
|
+
total: conversionJob.max_attempts,
|
|
1993
|
+
})}
|
|
1994
|
+
</p>
|
|
1995
|
+
</div>
|
|
1996
|
+
<div className="rounded-md border bg-background/70 p-2">
|
|
1997
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
1998
|
+
{t('lessonForm.videoJobCreatedAt')}
|
|
1999
|
+
</p>
|
|
2000
|
+
<p className="text-xs font-medium">
|
|
2001
|
+
{formatDateTimeLabel(
|
|
2002
|
+
conversionJob.created_at
|
|
2003
|
+
) ?? '—'}
|
|
2004
|
+
</p>
|
|
2005
|
+
</div>
|
|
2006
|
+
<div className="rounded-md border bg-background/70 p-2">
|
|
2007
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
2008
|
+
{TERMINAL_VIDEO_JOB_STATUSES.includes(
|
|
2009
|
+
conversionJob.status
|
|
2010
|
+
)
|
|
2011
|
+
? t('lessonForm.videoJobFinishedAt')
|
|
2012
|
+
: t('lessonForm.videoJobStartedAt')}
|
|
2013
|
+
</p>
|
|
2014
|
+
<p className="text-xs font-medium">
|
|
2015
|
+
{formatDateTimeLabel(
|
|
2016
|
+
TERMINAL_VIDEO_JOB_STATUSES.includes(
|
|
2017
|
+
conversionJob.status
|
|
2018
|
+
)
|
|
2019
|
+
? conversionJob.finished_at
|
|
2020
|
+
: conversionJob.started_at
|
|
2021
|
+
) ?? '—'}
|
|
2022
|
+
</p>
|
|
2023
|
+
</div>
|
|
2024
|
+
</div>
|
|
2025
|
+
|
|
2026
|
+
{latestConversionAttempt ? (
|
|
2027
|
+
<div className="rounded-md border bg-background/70 p-3">
|
|
2028
|
+
<div className="flex items-center justify-between gap-2">
|
|
2029
|
+
<p className="text-xs font-medium">
|
|
2030
|
+
{t('lessonForm.videoJobLatestAttempt')}
|
|
2031
|
+
</p>
|
|
2032
|
+
<span className="text-[0.65rem] text-muted-foreground">
|
|
2033
|
+
{t(
|
|
2034
|
+
`lessonForm.videoAttemptStatuses.${latestConversionAttempt.status}` as any
|
|
2035
|
+
)}
|
|
2036
|
+
</span>
|
|
2037
|
+
</div>
|
|
2038
|
+
<p className="mt-1 text-[0.65rem] text-muted-foreground">
|
|
2039
|
+
{t('lessonForm.videoJobAttemptValue', {
|
|
2040
|
+
count:
|
|
2041
|
+
latestConversionAttempt.attempt_number,
|
|
2042
|
+
})}
|
|
2043
|
+
{formatDurationLabel(
|
|
2044
|
+
latestConversionAttempt.duration_ms
|
|
2045
|
+
)
|
|
2046
|
+
? ` · ${formatDurationLabel(latestConversionAttempt.duration_ms)}`
|
|
2047
|
+
: ''}
|
|
2048
|
+
</p>
|
|
2049
|
+
{latestConversionAttempt.error_message ? (
|
|
2050
|
+
<p className="mt-2 text-xs text-destructive">
|
|
2051
|
+
{latestConversionAttempt.error_message}
|
|
2052
|
+
</p>
|
|
2053
|
+
) : null}
|
|
2054
|
+
</div>
|
|
2055
|
+
) : null}
|
|
2056
|
+
|
|
2057
|
+
{conversionJob.last_error ? (
|
|
2058
|
+
<div className="rounded-md border border-destructive/30 bg-destructive/5 p-3">
|
|
2059
|
+
<p className="text-[0.65rem] font-medium text-destructive">
|
|
2060
|
+
{t('lessonForm.videoJobLastError')}
|
|
2061
|
+
</p>
|
|
2062
|
+
<p className="mt-1 text-xs text-destructive">
|
|
2063
|
+
{conversionJob.last_error}
|
|
2064
|
+
</p>
|
|
2065
|
+
</div>
|
|
2066
|
+
) : null}
|
|
2067
|
+
|
|
2068
|
+
<div className="rounded-md border bg-background/70 p-3">
|
|
2069
|
+
<p className="text-xs font-medium">
|
|
2070
|
+
{t('lessonForm.videoJobRecentEvents')}
|
|
2071
|
+
</p>
|
|
2072
|
+
<div className="mt-2 flex flex-col gap-2">
|
|
2073
|
+
{recentConversionEvents.length === 0 ? (
|
|
2074
|
+
<p className="text-xs text-muted-foreground">
|
|
2075
|
+
{t('lessonForm.videoJobNoEvents')}
|
|
2076
|
+
</p>
|
|
2077
|
+
) : (
|
|
2078
|
+
recentConversionEvents.map((event) => (
|
|
2079
|
+
<div
|
|
2080
|
+
key={event.id}
|
|
2081
|
+
className="rounded-md border border-border/60 px-2.5 py-2"
|
|
2082
|
+
>
|
|
2083
|
+
<div className="flex items-center justify-between gap-2">
|
|
2084
|
+
<p className="text-xs font-medium">
|
|
2085
|
+
{t(
|
|
2086
|
+
`lessonForm.videoJobEvents.${event.event_type}` as any
|
|
2087
|
+
)}
|
|
2088
|
+
</p>
|
|
2089
|
+
<span className="text-[0.65rem] text-muted-foreground">
|
|
2090
|
+
{formatDateTimeLabel(
|
|
2091
|
+
event.created_at
|
|
2092
|
+
) ?? '—'}
|
|
2093
|
+
</span>
|
|
2094
|
+
</div>
|
|
2095
|
+
{event.message ? (
|
|
2096
|
+
<p className="mt-1 text-[0.65rem] text-muted-foreground">
|
|
2097
|
+
{event.message}
|
|
2098
|
+
</p>
|
|
2099
|
+
) : null}
|
|
2100
|
+
</div>
|
|
2101
|
+
))
|
|
2102
|
+
)}
|
|
2103
|
+
</div>
|
|
2104
|
+
</div>
|
|
2105
|
+
</>
|
|
2106
|
+
)}
|
|
2107
|
+
</CardContent>
|
|
2108
|
+
</Card>
|
|
2109
|
+
) : null}
|
|
2110
|
+
|
|
2111
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
2112
|
+
<CardHeader className="px-3 pt-2 pb-1">
|
|
2113
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
2114
|
+
{t('lessonForm.fileStorageVideosByResolution')}
|
|
2115
|
+
</CardTitle>
|
|
2116
|
+
</CardHeader>
|
|
2117
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
2118
|
+
{isFetchingCourseVideoProfiles ? (
|
|
2119
|
+
<p className="text-xs text-muted-foreground">
|
|
2120
|
+
{t('lessonForm.loadingVideoProfiles')}
|
|
2121
|
+
</p>
|
|
2122
|
+
) : hasCourseVideoProfilesError ? (
|
|
2123
|
+
<div className="flex flex-col gap-2">
|
|
2124
|
+
<p className="text-xs text-destructive">
|
|
2125
|
+
{t('lessonForm.videoProfilesLoadError')}
|
|
2126
|
+
</p>
|
|
2127
|
+
<Button
|
|
2128
|
+
type="button"
|
|
2129
|
+
variant="outline"
|
|
2130
|
+
size="sm"
|
|
2131
|
+
className="h-7 w-fit px-2 text-xs"
|
|
2132
|
+
onClick={() =>
|
|
2133
|
+
void refetchCourseVideoProfiles()
|
|
2134
|
+
}
|
|
2135
|
+
>
|
|
2136
|
+
<RefreshCw className="size-3 mr-1" />
|
|
2137
|
+
{t('lessonForm.retryLoadVideoProfiles')}
|
|
2138
|
+
</Button>
|
|
2139
|
+
</div>
|
|
2140
|
+
) : courseVideoProfiles.length === 0 ? (
|
|
2141
|
+
<p className="text-xs text-muted-foreground">
|
|
2142
|
+
{t('lessonForm.noVideoProfilesConfigured')}
|
|
2143
|
+
</p>
|
|
2144
|
+
) : (
|
|
2145
|
+
<>
|
|
2146
|
+
{isConversionJobActive ? (
|
|
2147
|
+
<p className="text-xs text-muted-foreground">
|
|
2148
|
+
{t(
|
|
2149
|
+
'lessonForm.videoProfilesLockedWhileProcessing'
|
|
2150
|
+
)}
|
|
2151
|
+
</p>
|
|
2152
|
+
) : null}
|
|
2153
|
+
<div className="flex flex-col gap-1">
|
|
2154
|
+
{courseVideoProfiles.map((profile) => {
|
|
2155
|
+
const res = profileVideoResources.get(
|
|
2156
|
+
profile.id
|
|
2157
|
+
);
|
|
2158
|
+
const currentUploadProgress =
|
|
2159
|
+
profileUploadProgress[profile.id];
|
|
2160
|
+
const inputId = `lesson-video-profile-${profile.id}`;
|
|
2161
|
+
|
|
2162
|
+
return (
|
|
2163
|
+
<div
|
|
2164
|
+
key={profile.id}
|
|
2165
|
+
className="flex items-center gap-2 rounded-md border bg-background/80 px-2.5 py-2"
|
|
2166
|
+
>
|
|
2167
|
+
<Video className="size-3.5 shrink-0 text-blue-500" />
|
|
2168
|
+
<div className="flex-1 min-w-0">
|
|
2169
|
+
<p className="text-xs truncate font-medium">
|
|
2170
|
+
{profile.name}
|
|
2171
|
+
</p>
|
|
2172
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
2173
|
+
{res
|
|
2174
|
+
? `${res.name}${res.size ? ` · ${res.size}` : ''}`
|
|
2175
|
+
: t(
|
|
2176
|
+
'lessonForm.videoProfileMissing'
|
|
2177
|
+
)}
|
|
2178
|
+
</p>
|
|
2179
|
+
{currentUploadProgress !== undefined ? (
|
|
2180
|
+
<div className="mt-1 space-y-1">
|
|
2181
|
+
<Progress
|
|
2182
|
+
value={currentUploadProgress}
|
|
2183
|
+
className="h-1.5"
|
|
2184
|
+
/>
|
|
2185
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
2186
|
+
{currentUploadProgress}%
|
|
2187
|
+
</p>
|
|
2188
|
+
</div>
|
|
2189
|
+
) : null}
|
|
2190
|
+
</div>
|
|
2191
|
+
<input
|
|
2192
|
+
id={inputId}
|
|
2193
|
+
type="file"
|
|
2194
|
+
accept="video/*"
|
|
2195
|
+
className="hidden"
|
|
2196
|
+
onChange={(event) => {
|
|
2197
|
+
const file = event.target.files?.[0];
|
|
2198
|
+
if (
|
|
2199
|
+
file &&
|
|
2200
|
+
!isProfileVideoUploadBlocked
|
|
2201
|
+
) {
|
|
2202
|
+
void handleVideoProfileFile(
|
|
2203
|
+
profile.id,
|
|
2204
|
+
file
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
event.target.value = '';
|
|
2208
|
+
}}
|
|
2209
|
+
/>
|
|
2210
|
+
<Button
|
|
2211
|
+
type="button"
|
|
2212
|
+
variant="outline"
|
|
2213
|
+
size="sm"
|
|
2214
|
+
className="h-7 px-2 text-xs"
|
|
2215
|
+
disabled={
|
|
2216
|
+
currentUploadProgress !== undefined ||
|
|
2217
|
+
isProfileVideoUploadBlocked
|
|
2218
|
+
}
|
|
2219
|
+
onClick={() =>
|
|
2220
|
+
document
|
|
2221
|
+
.getElementById(inputId)
|
|
2222
|
+
?.click()
|
|
2223
|
+
}
|
|
2224
|
+
>
|
|
2225
|
+
<UploadCloud className="size-3 mr-1" />
|
|
2226
|
+
{res
|
|
2227
|
+
? t('lessonForm.replaceVideo')
|
|
2228
|
+
: t('lessonForm.upload')}
|
|
2229
|
+
</Button>
|
|
2230
|
+
{res && (
|
|
2231
|
+
<>
|
|
2232
|
+
<Button
|
|
2233
|
+
type="button"
|
|
2234
|
+
variant="ghost"
|
|
2235
|
+
size="icon"
|
|
2236
|
+
className="size-6 shrink-0"
|
|
2237
|
+
disabled={isResolvingVideoPreview}
|
|
2238
|
+
onClick={() =>
|
|
2239
|
+
void openVideoPreview(res)
|
|
2240
|
+
}
|
|
2241
|
+
aria-label={t(
|
|
2242
|
+
'lessonForm.playVideoAria',
|
|
2243
|
+
{ name: res.name }
|
|
2244
|
+
)}
|
|
2245
|
+
>
|
|
2246
|
+
{isResolvingVideoPreview ? (
|
|
2247
|
+
<Loader2 className="size-3 animate-spin" />
|
|
2248
|
+
) : (
|
|
2249
|
+
<Play className="size-3" />
|
|
2250
|
+
)}
|
|
2251
|
+
</Button>
|
|
2252
|
+
<Button
|
|
2253
|
+
type="button"
|
|
2254
|
+
variant="ghost"
|
|
2255
|
+
size="icon"
|
|
2256
|
+
className="size-6 shrink-0"
|
|
2257
|
+
onClick={() =>
|
|
2258
|
+
void openResource(res)
|
|
2259
|
+
}
|
|
2260
|
+
aria-label={t(
|
|
2261
|
+
'lessonForm.openVideoAria',
|
|
2262
|
+
{ name: res.name }
|
|
2263
|
+
)}
|
|
2264
|
+
>
|
|
2265
|
+
<ExternalLink className="size-3" />
|
|
2266
|
+
</Button>
|
|
2267
|
+
<Button
|
|
2268
|
+
type="button"
|
|
2269
|
+
variant="ghost"
|
|
2270
|
+
size="icon"
|
|
2271
|
+
className="size-6 shrink-0"
|
|
2272
|
+
onClick={() =>
|
|
2273
|
+
void handleResourceDownload(res)
|
|
2274
|
+
}
|
|
2275
|
+
aria-label={t(
|
|
2276
|
+
'lessonForm.downloadVideoAria',
|
|
2277
|
+
{ name: res.name }
|
|
2278
|
+
)}
|
|
2279
|
+
>
|
|
2280
|
+
<Download className="size-3" />
|
|
2281
|
+
</Button>
|
|
2282
|
+
<Button
|
|
2283
|
+
type="button"
|
|
2284
|
+
variant="ghost"
|
|
2285
|
+
size="icon"
|
|
2286
|
+
className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
|
2287
|
+
onClick={() =>
|
|
2288
|
+
void removeResource(res.id)
|
|
2289
|
+
}
|
|
2290
|
+
aria-label={t(
|
|
2291
|
+
'lessonForm.removeVideoAria',
|
|
2292
|
+
{ name: res.name }
|
|
2293
|
+
)}
|
|
2294
|
+
>
|
|
2295
|
+
<X className="size-3" />
|
|
2296
|
+
</Button>
|
|
2297
|
+
</>
|
|
2298
|
+
)}
|
|
2299
|
+
</div>
|
|
2300
|
+
);
|
|
2301
|
+
})}
|
|
2302
|
+
</div>
|
|
2303
|
+
</>
|
|
2304
|
+
)}
|
|
2305
|
+
</CardContent>
|
|
2306
|
+
</Card>
|
|
2307
|
+
</>
|
|
2308
|
+
)}
|
|
2309
|
+
</div>
|
|
2310
|
+
</ScrollArea>
|
|
2311
|
+
</TabsContent>
|
|
2312
|
+
)}
|
|
2313
|
+
|
|
2314
|
+
{/* ── Tab Transcrição ─────────────────────────────────────────── */}
|
|
2315
|
+
{watchedType === 'video' && (
|
|
2316
|
+
<TabsContent value="transcricao" className="flex-1 min-h-0 mt-0">
|
|
2317
|
+
<ScrollArea className="h-full">
|
|
2318
|
+
<div className="flex flex-col gap-2 p-2 sm:gap-3 sm:p-3">
|
|
2319
|
+
<Card className="bg-muted/20 py-2 gap-2">
|
|
2320
|
+
<CardHeader className="px-3 pt-2 pb-1">
|
|
2321
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center justify-between gap-2">
|
|
2322
|
+
<span>{t('lessonForm.transcriptionSegments')}</span>
|
|
2323
|
+
<Button
|
|
2324
|
+
type="button"
|
|
2325
|
+
variant="outline"
|
|
2326
|
+
size="sm"
|
|
2327
|
+
className="h-6 text-xs px-2"
|
|
2328
|
+
onClick={() =>
|
|
2329
|
+
setTranscriptionSegments((prev) => [
|
|
2330
|
+
...prev,
|
|
2331
|
+
{
|
|
2332
|
+
id: segmentId(),
|
|
2333
|
+
start: '00:00',
|
|
2334
|
+
end: '00:15',
|
|
2335
|
+
text: '',
|
|
2336
|
+
},
|
|
2337
|
+
])
|
|
2338
|
+
}
|
|
2339
|
+
>
|
|
2340
|
+
<Plus className="size-3 mr-1" />
|
|
2341
|
+
{t('lessonForm.newTranscriptionSegment')}
|
|
2342
|
+
</Button>
|
|
2343
|
+
</CardTitle>
|
|
2344
|
+
</CardHeader>
|
|
2345
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-2">
|
|
2346
|
+
{transcriptionSegments.map((segment, index) => (
|
|
2347
|
+
<div
|
|
2348
|
+
key={segment.id}
|
|
2349
|
+
className="rounded-md border bg-background/80 p-2"
|
|
2350
|
+
>
|
|
2351
|
+
<div className="grid grid-cols-1 md:grid-cols-[92px_92px_1fr_auto] gap-2 items-start">
|
|
2352
|
+
<Input
|
|
2353
|
+
value={segment.start}
|
|
2354
|
+
onChange={(event) =>
|
|
2355
|
+
setTranscriptionSegments((prev) =>
|
|
2356
|
+
prev.map((item) =>
|
|
2357
|
+
item.id === segment.id
|
|
2358
|
+
? {
|
|
2359
|
+
...item,
|
|
2360
|
+
start: event.target.value,
|
|
2361
|
+
}
|
|
2362
|
+
: item
|
|
2363
|
+
)
|
|
2364
|
+
)
|
|
2365
|
+
}
|
|
2366
|
+
onBlur={() =>
|
|
2367
|
+
setTranscriptionSegments((prev) =>
|
|
2368
|
+
prev.map((item) =>
|
|
2369
|
+
item.id === segment.id
|
|
2370
|
+
? {
|
|
2371
|
+
...item,
|
|
2372
|
+
start: normalizeTimeInput(item.start),
|
|
2373
|
+
}
|
|
2374
|
+
: item
|
|
2375
|
+
)
|
|
2376
|
+
)
|
|
2377
|
+
}
|
|
2378
|
+
className="h-8 text-xs font-mono"
|
|
2379
|
+
placeholder="00:00"
|
|
2380
|
+
/>
|
|
2381
|
+
<Input
|
|
2382
|
+
value={segment.end}
|
|
2383
|
+
onChange={(event) =>
|
|
2384
|
+
setTranscriptionSegments((prev) =>
|
|
2385
|
+
prev.map((item) =>
|
|
2386
|
+
item.id === segment.id
|
|
2387
|
+
? {
|
|
2388
|
+
...item,
|
|
2389
|
+
end: event.target.value,
|
|
2390
|
+
}
|
|
2391
|
+
: item
|
|
2392
|
+
)
|
|
2393
|
+
)
|
|
2394
|
+
}
|
|
2395
|
+
onBlur={() =>
|
|
2396
|
+
setTranscriptionSegments((prev) =>
|
|
2397
|
+
prev.map((item) =>
|
|
2398
|
+
item.id === segment.id
|
|
2399
|
+
? {
|
|
2400
|
+
...item,
|
|
2401
|
+
end: normalizeTimeInput(item.end),
|
|
2402
|
+
}
|
|
2403
|
+
: item
|
|
2404
|
+
)
|
|
2405
|
+
)
|
|
2406
|
+
}
|
|
2407
|
+
className="h-8 text-xs font-mono"
|
|
2408
|
+
placeholder="00:15"
|
|
2409
|
+
/>
|
|
2410
|
+
<Textarea
|
|
2411
|
+
value={segment.text}
|
|
2412
|
+
onChange={(event) =>
|
|
2413
|
+
setTranscriptionSegments((prev) =>
|
|
2414
|
+
prev.map((item) =>
|
|
2415
|
+
item.id === segment.id
|
|
2416
|
+
? {
|
|
2417
|
+
...item,
|
|
2418
|
+
text: event.target.value,
|
|
2419
|
+
}
|
|
2420
|
+
: item
|
|
2421
|
+
)
|
|
2422
|
+
)
|
|
2423
|
+
}
|
|
2424
|
+
rows={2}
|
|
2425
|
+
className="text-xs resize-y"
|
|
2426
|
+
placeholder={t(
|
|
2427
|
+
'lessonForm.transcriptionSegmentPlaceholder',
|
|
2428
|
+
{
|
|
2429
|
+
count: index + 1,
|
|
2430
|
+
}
|
|
2431
|
+
)}
|
|
2432
|
+
/>
|
|
2433
|
+
<Button
|
|
2434
|
+
type="button"
|
|
2435
|
+
variant="ghost"
|
|
2436
|
+
size="icon"
|
|
2437
|
+
className="size-8 text-muted-foreground hover:text-destructive"
|
|
2438
|
+
onClick={() =>
|
|
2439
|
+
setTranscriptionSegments((prev) => {
|
|
2440
|
+
if (prev.length === 1) {
|
|
2441
|
+
const first = prev[0];
|
|
2442
|
+
return first
|
|
2443
|
+
? [{ ...first, text: '' }]
|
|
2444
|
+
: [];
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
return prev.filter(
|
|
2448
|
+
(item) => item.id !== segment.id
|
|
2449
|
+
);
|
|
2450
|
+
})
|
|
2451
|
+
}
|
|
2452
|
+
aria-label={t(
|
|
2453
|
+
'lessonForm.removeTranscriptionSegment',
|
|
2454
|
+
{
|
|
2455
|
+
count: index + 1,
|
|
2456
|
+
}
|
|
2457
|
+
)}
|
|
2458
|
+
>
|
|
2459
|
+
<X className="size-3.5" />
|
|
2460
|
+
</Button>
|
|
2461
|
+
</div>
|
|
2462
|
+
</div>
|
|
2463
|
+
))}
|
|
2464
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
2465
|
+
{t('lessonForm.transcriptionAcceptedFormats')}
|
|
2466
|
+
</p>
|
|
2467
|
+
</CardContent>
|
|
2468
|
+
</Card>
|
|
2469
|
+
</div>
|
|
2470
|
+
</ScrollArea>
|
|
2471
|
+
</TabsContent>
|
|
2472
|
+
)}
|
|
2473
|
+
|
|
1298
2474
|
{/* ── Tab Recursos ─────────────────────────────────────────────── */}
|
|
1299
2475
|
<TabsContent value="recursos" className="flex-1 min-h-0 mt-0">
|
|
1300
2476
|
<ScrollArea className="h-full">
|
|
1301
|
-
<div className="flex flex-col gap-3 p-3">
|
|
2477
|
+
<div className="flex flex-col gap-2 p-2 sm:gap-3 sm:p-3">
|
|
1302
2478
|
{/* Drop zone */}
|
|
1303
2479
|
<div
|
|
1304
2480
|
role="button"
|
|
@@ -1376,22 +2552,22 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1376
2552
|
</div>
|
|
1377
2553
|
|
|
1378
2554
|
{/* Counter */}
|
|
1379
|
-
{
|
|
2555
|
+
{genericResources.length > 0 && (
|
|
1380
2556
|
<p className="text-xs text-muted-foreground">
|
|
1381
2557
|
{t('lessonForm.resourcesCount', {
|
|
1382
|
-
count:
|
|
2558
|
+
count: genericResources.length,
|
|
1383
2559
|
})}
|
|
1384
2560
|
</p>
|
|
1385
2561
|
)}
|
|
1386
2562
|
|
|
1387
2563
|
{/* Resource list */}
|
|
1388
|
-
{
|
|
2564
|
+
{genericResources.length === 0 ? (
|
|
1389
2565
|
<p className="text-center text-xs text-muted-foreground py-1">
|
|
1390
2566
|
{t('questionEditor.noLinkedResources')}
|
|
1391
2567
|
</p>
|
|
1392
2568
|
) : (
|
|
1393
2569
|
<div className="flex flex-col gap-1">
|
|
1394
|
-
{
|
|
2570
|
+
{genericResources.map((res) => {
|
|
1395
2571
|
const ResIcon = getResourceIcon(res.type);
|
|
1396
2572
|
return (
|
|
1397
2573
|
<div
|
|
@@ -1418,7 +2594,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1418
2594
|
aria-label={t('lessonForm.public')}
|
|
1419
2595
|
/>
|
|
1420
2596
|
)}
|
|
1421
|
-
{/* Abrir em nova aba */}
|
|
1422
2597
|
{res.url && (
|
|
1423
2598
|
<a
|
|
1424
2599
|
href={res.url}
|
|
@@ -1433,7 +2608,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1433
2608
|
<ExternalLink className="size-3" />
|
|
1434
2609
|
</a>
|
|
1435
2610
|
)}
|
|
1436
|
-
{/* Download */}
|
|
1437
2611
|
<Button
|
|
1438
2612
|
type="button"
|
|
1439
2613
|
variant="ghost"
|
|
@@ -1446,7 +2620,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1446
2620
|
>
|
|
1447
2621
|
<Download className="size-3" />
|
|
1448
2622
|
</Button>
|
|
1449
|
-
{/* Remover */}
|
|
1450
2623
|
<Button
|
|
1451
2624
|
type="button"
|
|
1452
2625
|
variant="ghost"
|
|
@@ -1472,14 +2645,18 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1472
2645
|
{/* ── Footer ───────────────────────────────────────────────────────── */}
|
|
1473
2646
|
<div className="shrink-0 border-t bg-background">
|
|
1474
2647
|
<Separator />
|
|
1475
|
-
<div className="flex items-center gap-2 px-3 py-2">
|
|
2648
|
+
<div className="flex items-center gap-1.5 px-2 py-1.5 sm:gap-2 sm:px-3 sm:py-2">
|
|
1476
2649
|
<Button
|
|
1477
2650
|
type="button"
|
|
1478
2651
|
variant="ghost"
|
|
1479
2652
|
size="sm"
|
|
1480
|
-
className="h-7 text-xs"
|
|
1481
|
-
disabled={!isDirty || updateLesson.isPending}
|
|
1482
|
-
onClick={() =>
|
|
2653
|
+
className="h-6 text-[11px] sm:h-7 sm:text-xs"
|
|
2654
|
+
disabled={(!isDirty && !resourcesDirty) || updateLesson.isPending}
|
|
2655
|
+
onClick={() => {
|
|
2656
|
+
form.reset();
|
|
2657
|
+
setLocalResources(lesson?.resources ?? []);
|
|
2658
|
+
setResourcesDirty(false);
|
|
2659
|
+
}}
|
|
1483
2660
|
>
|
|
1484
2661
|
<Undo2 className="size-3 mr-1" />
|
|
1485
2662
|
{t('lessonForm.cancel')}
|
|
@@ -1488,8 +2665,8 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1488
2665
|
<Button
|
|
1489
2666
|
type="submit"
|
|
1490
2667
|
size="sm"
|
|
1491
|
-
className="h-7 text-xs"
|
|
1492
|
-
disabled={!isDirty || updateLesson.isPending}
|
|
2668
|
+
className="h-6 text-[11px] sm:h-7 sm:text-xs"
|
|
2669
|
+
disabled={(!isDirty && !resourcesDirty) || updateLesson.isPending}
|
|
1493
2670
|
>
|
|
1494
2671
|
{updateLesson.isPending ? (
|
|
1495
2672
|
<Loader2 className="size-3 mr-1 animate-spin" />
|
|
@@ -1503,7 +2680,11 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1503
2680
|
|
|
1504
2681
|
{/* ── Question Sheet ──────────────────────────────────────────────── */}
|
|
1505
2682
|
<Sheet open={questionSheetOpen} onOpenChange={setQuestionSheetOpen}>
|
|
1506
|
-
<
|
|
2683
|
+
<ResizableSheetContent
|
|
2684
|
+
sheetId="lms-course-structure-question-sheet"
|
|
2685
|
+
defaultWidth={560}
|
|
2686
|
+
minWidth={420}
|
|
2687
|
+
maxWidth={920}
|
|
1507
2688
|
side="right"
|
|
1508
2689
|
className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
|
|
1509
2690
|
>
|
|
@@ -1548,13 +2729,13 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1548
2729
|
<SelectValue />
|
|
1549
2730
|
</SelectTrigger>
|
|
1550
2731
|
<SelectContent>
|
|
1551
|
-
{(
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
)
|
|
2732
|
+
{(Object.keys(questionTypeLabels) as QuestionType[]).map(
|
|
2733
|
+
(t) => (
|
|
2734
|
+
<SelectItem key={t} value={t}>
|
|
2735
|
+
{questionTypeLabels[t]}
|
|
2736
|
+
</SelectItem>
|
|
2737
|
+
)
|
|
2738
|
+
)}
|
|
1558
2739
|
</SelectContent>
|
|
1559
2740
|
</Select>
|
|
1560
2741
|
</div>
|
|
@@ -1892,7 +3073,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1892
3073
|
: t('questionEditor.createQuestion')}
|
|
1893
3074
|
</Button>
|
|
1894
3075
|
</SheetFooter>
|
|
1895
|
-
</
|
|
3076
|
+
</ResizableSheetContent>
|
|
1896
3077
|
</Sheet>
|
|
1897
3078
|
</form>
|
|
1898
3079
|
</Form>
|