@hed-hog/lms 0.0.361 → 0.0.365

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