@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.
Files changed (496) hide show
  1. package/dist/achievement/achievement.controller.d.ts +62 -0
  2. package/dist/achievement/achievement.controller.d.ts.map +1 -0
  3. package/dist/achievement/achievement.controller.js +90 -0
  4. package/dist/achievement/achievement.controller.js.map +1 -0
  5. package/dist/achievement/achievement.mcp-tools.d.ts +19 -0
  6. package/dist/achievement/achievement.mcp-tools.d.ts.map +1 -0
  7. package/dist/achievement/achievement.mcp-tools.js +157 -0
  8. package/dist/achievement/achievement.mcp-tools.js.map +1 -0
  9. package/dist/achievement/achievement.module.d.ts +3 -0
  10. package/dist/achievement/achievement.module.d.ts.map +1 -0
  11. package/dist/achievement/achievement.module.js +26 -0
  12. package/dist/achievement/achievement.module.js.map +1 -0
  13. package/dist/achievement/achievement.service.d.ts +72 -0
  14. package/dist/achievement/achievement.service.d.ts.map +1 -0
  15. package/dist/achievement/achievement.service.js +200 -0
  16. package/dist/achievement/achievement.service.js.map +1 -0
  17. package/dist/achievement/dto/create-achievement.dto.d.ts +12 -0
  18. package/dist/achievement/dto/create-achievement.dto.d.ts.map +1 -0
  19. package/dist/achievement/dto/create-achievement.dto.js +60 -0
  20. package/dist/achievement/dto/create-achievement.dto.js.map +1 -0
  21. package/dist/achievement/dto/update-achievement.dto.d.ts +11 -0
  22. package/dist/achievement/dto/update-achievement.dto.d.ts.map +1 -0
  23. package/dist/achievement/dto/update-achievement.dto.js +57 -0
  24. package/dist/achievement/dto/update-achievement.dto.js.map +1 -0
  25. package/dist/bitcode-wallet/bitcode-wallet.controller.d.ts +114 -0
  26. package/dist/bitcode-wallet/bitcode-wallet.controller.d.ts.map +1 -0
  27. package/dist/bitcode-wallet/bitcode-wallet.controller.js +102 -0
  28. package/dist/bitcode-wallet/bitcode-wallet.controller.js.map +1 -0
  29. package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.d.ts +25 -0
  30. package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.d.ts.map +1 -0
  31. package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.js +160 -0
  32. package/dist/bitcode-wallet/bitcode-wallet.mcp-tools.js.map +1 -0
  33. package/dist/bitcode-wallet/bitcode-wallet.module.d.ts +3 -0
  34. package/dist/bitcode-wallet/bitcode-wallet.module.d.ts.map +1 -0
  35. package/dist/bitcode-wallet/bitcode-wallet.module.js +26 -0
  36. package/dist/bitcode-wallet/bitcode-wallet.module.js.map +1 -0
  37. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +127 -0
  38. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -0
  39. package/dist/bitcode-wallet/bitcode-wallet.service.js +264 -0
  40. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -0
  41. package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.d.ts +8 -0
  42. package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  43. package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.js +33 -0
  44. package/dist/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.js.map +1 -0
  45. package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.d.ts +4 -0
  46. package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.d.ts.map +1 -0
  47. package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.js +22 -0
  48. package/dist/bitcode-wallet/dto/create-bitcode-wallet.dto.js.map +1 -0
  49. package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.d.ts +7 -0
  50. package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  51. package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.js +35 -0
  52. package/dist/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.js.map +1 -0
  53. package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.d.ts +4 -0
  54. package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.d.ts.map +1 -0
  55. package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.js +23 -0
  56. package/dist/bitcode-wallet/dto/update-bitcode-wallet.dto.js.map +1 -0
  57. package/dist/certificate/certificate.controller.d.ts +24 -2
  58. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  59. package/dist/certificate/certificate.controller.js +20 -6
  60. package/dist/certificate/certificate.controller.js.map +1 -1
  61. package/dist/certificate/certificate.mcp-tools.d.ts +24 -0
  62. package/dist/certificate/certificate.mcp-tools.d.ts.map +1 -0
  63. package/dist/certificate/certificate.mcp-tools.js +188 -0
  64. package/dist/certificate/certificate.mcp-tools.js.map +1 -0
  65. package/dist/certificate/certificate.module.d.ts.map +1 -1
  66. package/dist/certificate/certificate.module.js +2 -1
  67. package/dist/certificate/certificate.module.js.map +1 -1
  68. package/dist/certificate/certificate.service.d.ts +30 -4
  69. package/dist/certificate/certificate.service.d.ts.map +1 -1
  70. package/dist/certificate/certificate.service.js +157 -8
  71. package/dist/certificate/certificate.service.js.map +1 -1
  72. package/dist/certificate/dto/update-certificate-public-access.dto.d.ts +4 -0
  73. package/dist/certificate/dto/update-certificate-public-access.dto.d.ts.map +1 -0
  74. package/dist/certificate/dto/update-certificate-public-access.dto.js +21 -0
  75. package/dist/certificate/dto/update-certificate-public-access.dto.js.map +1 -0
  76. package/dist/class-group/class-group.mcp-tools.d.ts +87 -0
  77. package/dist/class-group/class-group.mcp-tools.d.ts.map +1 -0
  78. package/dist/class-group/class-group.mcp-tools.js +553 -0
  79. package/dist/class-group/class-group.mcp-tools.js.map +1 -0
  80. package/dist/class-group/class-group.module.d.ts.map +1 -1
  81. package/dist/class-group/class-group.module.js +2 -1
  82. package/dist/class-group/class-group.module.js.map +1 -1
  83. package/dist/class-group/class-group.service.d.ts +3 -1
  84. package/dist/class-group/class-group.service.d.ts.map +1 -1
  85. package/dist/class-group/class-group.service.js +45 -2
  86. package/dist/class-group/class-group.service.js.map +1 -1
  87. package/dist/course/course-operations-integration.service.d.ts +40 -0
  88. package/dist/course/course-operations-integration.service.d.ts.map +1 -0
  89. package/dist/course/course-operations-integration.service.js +372 -0
  90. package/dist/course/course-operations-integration.service.js.map +1 -0
  91. package/dist/course/course-structure.controller.d.ts +67 -14
  92. package/dist/course/course-structure.controller.d.ts.map +1 -1
  93. package/dist/course/course-structure.controller.js +45 -2
  94. package/dist/course/course-structure.controller.js.map +1 -1
  95. package/dist/course/course-structure.service.d.ts +58 -9
  96. package/dist/course/course-structure.service.d.ts.map +1 -1
  97. package/dist/course/course-structure.service.js +260 -62
  98. package/dist/course/course-structure.service.js.map +1 -1
  99. package/dist/course/course-video-conversion.service.d.ts +37 -0
  100. package/dist/course/course-video-conversion.service.d.ts.map +1 -0
  101. package/dist/course/course-video-conversion.service.js +308 -0
  102. package/dist/course/course-video-conversion.service.js.map +1 -0
  103. package/dist/course/course.controller.d.ts +29 -0
  104. package/dist/course/course.controller.d.ts.map +1 -1
  105. package/dist/course/course.controller.js +23 -0
  106. package/dist/course/course.controller.js.map +1 -1
  107. package/dist/course/course.mcp-tools.d.ts +90 -0
  108. package/dist/course/course.mcp-tools.d.ts.map +1 -0
  109. package/dist/course/course.mcp-tools.js +520 -0
  110. package/dist/course/course.mcp-tools.js.map +1 -0
  111. package/dist/course/course.module.d.ts.map +1 -1
  112. package/dist/course/course.module.js +23 -3
  113. package/dist/course/course.module.js.map +1 -1
  114. package/dist/course/course.service.d.ts +30 -1
  115. package/dist/course/course.service.d.ts.map +1 -1
  116. package/dist/course/course.service.js +159 -70
  117. package/dist/course/course.service.js.map +1 -1
  118. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +5 -1
  119. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  120. package/dist/course/dto/create-course-structure-lesson.dto.js +16 -2
  121. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  122. package/dist/course/dto/create-course.dto.d.ts +2 -0
  123. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  124. package/dist/course/dto/create-course.dto.js +16 -0
  125. package/dist/course/dto/create-course.dto.js.map +1 -1
  126. package/dist/course/dto/update-course-resources.dto.d.ts +11 -0
  127. package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -0
  128. package/dist/course/dto/update-course-resources.dto.js +51 -0
  129. package/dist/course/dto/update-course-resources.dto.js.map +1 -0
  130. package/dist/course-lesson-discussion/course-lesson-discussion.controller.d.ts +23 -0
  131. package/dist/course-lesson-discussion/course-lesson-discussion.controller.d.ts.map +1 -0
  132. package/dist/course-lesson-discussion/course-lesson-discussion.controller.js +78 -0
  133. package/dist/course-lesson-discussion/course-lesson-discussion.controller.js.map +1 -0
  134. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.d.ts +22 -0
  135. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.d.ts.map +1 -0
  136. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.js +120 -0
  137. package/dist/course-lesson-discussion/course-lesson-discussion.mcp-tools.js.map +1 -0
  138. package/dist/course-lesson-discussion/course-lesson-discussion.module.d.ts +3 -0
  139. package/dist/course-lesson-discussion/course-lesson-discussion.module.d.ts.map +1 -0
  140. package/dist/course-lesson-discussion/course-lesson-discussion.module.js +26 -0
  141. package/dist/course-lesson-discussion/course-lesson-discussion.module.js.map +1 -0
  142. package/dist/course-lesson-discussion/course-lesson-discussion.service.d.ts +49 -0
  143. package/dist/course-lesson-discussion/course-lesson-discussion.service.d.ts.map +1 -0
  144. package/dist/course-lesson-discussion/course-lesson-discussion.service.js +272 -0
  145. package/dist/course-lesson-discussion/course-lesson-discussion.service.js.map +1 -0
  146. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.d.ts +6 -0
  147. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.d.ts.map +1 -0
  148. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.js +33 -0
  149. package/dist/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.js.map +1 -0
  150. package/dist/course-lesson-note/course-lesson-note.controller.d.ts +53 -0
  151. package/dist/course-lesson-note/course-lesson-note.controller.d.ts.map +1 -0
  152. package/dist/course-lesson-note/course-lesson-note.controller.js +93 -0
  153. package/dist/course-lesson-note/course-lesson-note.controller.js.map +1 -0
  154. package/dist/course-lesson-note/course-lesson-note.mcp-tools.d.ts +27 -0
  155. package/dist/course-lesson-note/course-lesson-note.mcp-tools.d.ts.map +1 -0
  156. package/dist/course-lesson-note/course-lesson-note.mcp-tools.js +145 -0
  157. package/dist/course-lesson-note/course-lesson-note.mcp-tools.js.map +1 -0
  158. package/dist/course-lesson-note/course-lesson-note.module.d.ts +3 -0
  159. package/dist/course-lesson-note/course-lesson-note.module.d.ts.map +1 -0
  160. package/dist/course-lesson-note/course-lesson-note.module.js +26 -0
  161. package/dist/course-lesson-note/course-lesson-note.module.js.map +1 -0
  162. package/dist/course-lesson-note/course-lesson-note.service.d.ts +59 -0
  163. package/dist/course-lesson-note/course-lesson-note.service.d.ts.map +1 -0
  164. package/dist/course-lesson-note/course-lesson-note.service.js +195 -0
  165. package/dist/course-lesson-note/course-lesson-note.service.js.map +1 -0
  166. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.d.ts +6 -0
  167. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.d.ts.map +1 -0
  168. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.js +33 -0
  169. package/dist/course-lesson-note/dto/create-course-lesson-note.dto.js.map +1 -0
  170. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.d.ts +6 -0
  171. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.d.ts.map +1 -0
  172. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.js +35 -0
  173. package/dist/course-lesson-note/dto/update-course-lesson-note.dto.js.map +1 -0
  174. package/dist/dashboard/dashboard.mcp-tools.d.ts +10 -0
  175. package/dist/dashboard/dashboard.mcp-tools.d.ts.map +1 -0
  176. package/dist/dashboard/dashboard.mcp-tools.js +46 -0
  177. package/dist/dashboard/dashboard.mcp-tools.js.map +1 -0
  178. package/dist/dashboard/dashboard.module.d.ts.map +1 -1
  179. package/dist/dashboard/dashboard.module.js +2 -1
  180. package/dist/dashboard/dashboard.module.js.map +1 -1
  181. package/dist/enterprise/enterprise.controller.d.ts +3 -3
  182. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  183. package/dist/enterprise/enterprise.controller.js +0 -1
  184. package/dist/enterprise/enterprise.controller.js.map +1 -1
  185. package/dist/enterprise/enterprise.mcp-tools.d.ts +82 -0
  186. package/dist/enterprise/enterprise.mcp-tools.d.ts.map +1 -0
  187. package/dist/enterprise/enterprise.mcp-tools.js +516 -0
  188. package/dist/enterprise/enterprise.mcp-tools.js.map +1 -0
  189. package/dist/enterprise/enterprise.module.d.ts.map +1 -1
  190. package/dist/enterprise/enterprise.module.js +2 -1
  191. package/dist/enterprise/enterprise.module.js.map +1 -1
  192. package/dist/enterprise/enterprise.service.d.ts +3 -3
  193. package/dist/enterprise/training/enterprise-training.module.d.ts.map +1 -1
  194. package/dist/enterprise/training/enterprise-training.module.js +11 -1
  195. package/dist/enterprise/training/enterprise-training.module.js.map +1 -1
  196. package/dist/enterprise/training/training-admin.mcp-tools.d.ts +79 -0
  197. package/dist/enterprise/training/training-admin.mcp-tools.d.ts.map +1 -0
  198. package/dist/enterprise/training/training-admin.mcp-tools.js +620 -0
  199. package/dist/enterprise/training/training-admin.mcp-tools.js.map +1 -0
  200. package/dist/enterprise/training/training-instructor.mcp-tools.d.ts +47 -0
  201. package/dist/enterprise/training/training-instructor.mcp-tools.d.ts.map +1 -0
  202. package/dist/enterprise/training/training-instructor.mcp-tools.js +275 -0
  203. package/dist/enterprise/training/training-instructor.mcp-tools.js.map +1 -0
  204. package/dist/enterprise/training/training-student.controller.d.ts +24 -0
  205. package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
  206. package/dist/enterprise/training/training-student.controller.js +22 -0
  207. package/dist/enterprise/training/training-student.controller.js.map +1 -1
  208. package/dist/enterprise/training/training-student.mcp-tools.d.ts +27 -0
  209. package/dist/enterprise/training/training-student.mcp-tools.d.ts.map +1 -0
  210. package/dist/enterprise/training/training-student.mcp-tools.js +186 -0
  211. package/dist/enterprise/training/training-student.mcp-tools.js.map +1 -0
  212. package/dist/enterprise/training/training-student.service.d.ts +32 -0
  213. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  214. package/dist/enterprise/training/training-student.service.js +138 -0
  215. package/dist/enterprise/training/training-student.service.js.map +1 -1
  216. package/dist/evaluation/evaluation.mcp-tools.d.ts +25 -0
  217. package/dist/evaluation/evaluation.mcp-tools.d.ts.map +1 -0
  218. package/dist/evaluation/evaluation.mcp-tools.js +220 -0
  219. package/dist/evaluation/evaluation.mcp-tools.js.map +1 -0
  220. package/dist/evaluation/evaluation.module.d.ts.map +1 -1
  221. package/dist/evaluation/evaluation.module.js +2 -1
  222. package/dist/evaluation/evaluation.module.js.map +1 -1
  223. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  224. package/dist/evaluation/evaluation.service.js +9 -2
  225. package/dist/evaluation/evaluation.service.js.map +1 -1
  226. package/dist/exam/dto/create-exam-question.dto.d.ts +2 -0
  227. package/dist/exam/dto/create-exam-question.dto.d.ts.map +1 -1
  228. package/dist/exam/dto/create-exam-question.dto.js +10 -0
  229. package/dist/exam/dto/create-exam-question.dto.js.map +1 -1
  230. package/dist/exam/dto/create-exam.dto.d.ts +2 -0
  231. package/dist/exam/dto/create-exam.dto.d.ts.map +1 -1
  232. package/dist/exam/dto/create-exam.dto.js +10 -0
  233. package/dist/exam/dto/create-exam.dto.js.map +1 -1
  234. package/dist/exam/dto/create-question-subject.dto.d.ts +5 -0
  235. package/dist/exam/dto/create-question-subject.dto.d.ts.map +1 -0
  236. package/dist/exam/dto/create-question-subject.dto.js +28 -0
  237. package/dist/exam/dto/create-question-subject.dto.js.map +1 -0
  238. package/dist/exam/exam-attempt.controller.d.ts +4 -0
  239. package/dist/exam/exam-attempt.controller.d.ts.map +1 -1
  240. package/dist/exam/exam-attempt.service.d.ts +7 -1
  241. package/dist/exam/exam-attempt.service.d.ts.map +1 -1
  242. package/dist/exam/exam-attempt.service.js +47 -17
  243. package/dist/exam/exam-attempt.service.js.map +1 -1
  244. package/dist/exam/exam.controller.d.ts +34 -0
  245. package/dist/exam/exam.controller.d.ts.map +1 -1
  246. package/dist/exam/exam.controller.js +27 -0
  247. package/dist/exam/exam.controller.js.map +1 -1
  248. package/dist/exam/exam.mcp-tools.d.ts +62 -0
  249. package/dist/exam/exam.mcp-tools.d.ts.map +1 -0
  250. package/dist/exam/exam.mcp-tools.js +430 -0
  251. package/dist/exam/exam.mcp-tools.js.map +1 -0
  252. package/dist/exam/exam.module.d.ts.map +1 -1
  253. package/dist/exam/exam.module.js +2 -1
  254. package/dist/exam/exam.module.js.map +1 -1
  255. package/dist/exam/exam.service.d.ts +38 -0
  256. package/dist/exam/exam.service.d.ts.map +1 -1
  257. package/dist/exam/exam.service.js +114 -17
  258. package/dist/exam/exam.service.js.map +1 -1
  259. package/dist/index.d.ts +10 -0
  260. package/dist/index.d.ts.map +1 -1
  261. package/dist/index.js +10 -0
  262. package/dist/index.js.map +1 -1
  263. package/dist/instructor/instructor.mcp-tools.d.ts +41 -0
  264. package/dist/instructor/instructor.mcp-tools.d.ts.map +1 -0
  265. package/dist/instructor/instructor.mcp-tools.js +326 -0
  266. package/dist/instructor/instructor.mcp-tools.js.map +1 -0
  267. package/dist/instructor/instructor.module.d.ts.map +1 -1
  268. package/dist/instructor/instructor.module.js +2 -1
  269. package/dist/instructor/instructor.module.js.map +1 -1
  270. package/dist/lms.module.d.ts.map +1 -1
  271. package/dist/lms.module.js +18 -0
  272. package/dist/lms.module.js.map +1 -1
  273. package/dist/realtime/lms-realtime.controller.d.ts +7 -0
  274. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -0
  275. package/dist/realtime/lms-realtime.controller.js +34 -0
  276. package/dist/realtime/lms-realtime.controller.js.map +1 -0
  277. package/dist/realtime/lms-realtime.module.d.ts +3 -0
  278. package/dist/realtime/lms-realtime.module.d.ts.map +1 -0
  279. package/dist/realtime/lms-realtime.module.js +25 -0
  280. package/dist/realtime/lms-realtime.module.js.map +1 -0
  281. package/dist/realtime/lms-realtime.service.d.ts +36 -0
  282. package/dist/realtime/lms-realtime.service.d.ts.map +1 -0
  283. package/dist/realtime/lms-realtime.service.js +59 -0
  284. package/dist/realtime/lms-realtime.service.js.map +1 -0
  285. package/dist/realtime/lms-realtime.subscriber.d.ts +10 -0
  286. package/dist/realtime/lms-realtime.subscriber.d.ts.map +1 -0
  287. package/dist/realtime/lms-realtime.subscriber.js +70 -0
  288. package/dist/realtime/lms-realtime.subscriber.js.map +1 -0
  289. package/dist/reports/reports.mcp-tools.d.ts +10 -0
  290. package/dist/reports/reports.mcp-tools.d.ts.map +1 -0
  291. package/dist/reports/reports.mcp-tools.js +50 -0
  292. package/dist/reports/reports.mcp-tools.js.map +1 -0
  293. package/dist/reports/reports.module.d.ts.map +1 -1
  294. package/dist/reports/reports.module.js +2 -1
  295. package/dist/reports/reports.module.js.map +1 -1
  296. package/dist/training/training.mcp-tools.d.ts +20 -0
  297. package/dist/training/training.mcp-tools.d.ts.map +1 -0
  298. package/dist/training/training.mcp-tools.js +181 -0
  299. package/dist/training/training.mcp-tools.js.map +1 -0
  300. package/dist/training/training.module.d.ts.map +1 -1
  301. package/dist/training/training.module.js +2 -1
  302. package/dist/training/training.module.js.map +1 -1
  303. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts +6 -0
  304. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts.map +1 -0
  305. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js +33 -0
  306. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js.map +1 -0
  307. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts +6 -0
  308. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts.map +1 -0
  309. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js +33 -0
  310. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js.map +1 -0
  311. package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts +38 -0
  312. package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts.map +1 -0
  313. package/dist/video-resolution-profile/video-resolution-profile.controller.js +89 -0
  314. package/dist/video-resolution-profile/video-resolution-profile.controller.js.map +1 -0
  315. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts +26 -0
  316. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts.map +1 -0
  317. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js +160 -0
  318. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js.map +1 -0
  319. package/dist/video-resolution-profile/video-resolution-profile.module.d.ts +3 -0
  320. package/dist/video-resolution-profile/video-resolution-profile.module.d.ts.map +1 -0
  321. package/dist/video-resolution-profile/video-resolution-profile.module.js +26 -0
  322. package/dist/video-resolution-profile/video-resolution-profile.module.js.map +1 -0
  323. package/dist/video-resolution-profile/video-resolution-profile.service.d.ts +45 -0
  324. package/dist/video-resolution-profile/video-resolution-profile.service.d.ts.map +1 -0
  325. package/dist/video-resolution-profile/video-resolution-profile.service.js +117 -0
  326. package/dist/video-resolution-profile/video-resolution-profile.service.js.map +1 -0
  327. package/hedhog/data/integration_event_catalog.yaml +69 -0
  328. package/hedhog/data/menu.yaml +51 -0
  329. package/hedhog/data/route.yaml +2484 -0
  330. package/hedhog/data/video_resolution_profile.yaml +7 -0
  331. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +432 -422
  332. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +200 -67
  333. package/hedhog/frontend/app/_components/course-picker.tsx.ejs +228 -0
  334. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +7 -4
  335. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +2 -2
  336. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +2 -2
  337. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +34 -4
  338. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +28 -3
  339. package/hedhog/frontend/app/_lib/hooks/use-lms-realtime-refresh.ts.ejs +58 -0
  340. package/hedhog/frontend/app/achievements/page.tsx.ejs +850 -0
  341. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +1016 -0
  342. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +68 -5
  343. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +29 -8
  344. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +14 -0
  345. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +194 -9
  346. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +15 -5
  347. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +16 -5
  348. package/hedhog/frontend/app/classes/page.tsx.ejs +126 -2105
  349. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +19 -9
  350. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +24 -1
  351. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +1 -1
  352. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +1 -1
  353. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +51 -11
  354. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +11 -6
  355. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +7 -4
  356. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +2 -0
  357. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +24 -96
  358. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +80 -66
  359. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1286 -230
  360. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1334 -153
  361. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +11 -11
  362. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -1
  363. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +62 -52
  364. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +106 -4
  365. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -1
  366. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +30 -7
  367. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +138 -6
  368. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +16 -2
  369. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +1 -0
  370. package/hedhog/frontend/app/courses/page.tsx.ejs +282 -113
  371. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +1 -1
  372. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +10 -3
  373. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -0
  374. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +7 -0
  375. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +8 -4
  376. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +2 -2
  377. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +10 -4
  378. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +10 -3
  379. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +10 -3
  380. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +10 -3
  381. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +186 -5
  382. package/hedhog/frontend/app/exams/page.tsx.ejs +89 -26
  383. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +10 -3
  384. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +190 -17
  385. package/hedhog/frontend/app/instructors/page.tsx.ejs +1 -0
  386. package/hedhog/frontend/app/layout.tsx.ejs +5 -1
  387. package/hedhog/frontend/app/paths/page.tsx.ejs +19 -29
  388. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +10 -10
  389. package/hedhog/frontend/app/training/page.tsx.ejs +19 -29
  390. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +607 -0
  391. package/hedhog/frontend/messages/en.json +563 -20
  392. package/hedhog/frontend/messages/pt.json +563 -20
  393. package/hedhog/query/triggers.sql +53 -0
  394. package/hedhog/table/achievement.yaml +46 -0
  395. package/hedhog/table/bitcode_wallet.yaml +18 -0
  396. package/hedhog/table/bitcode_wallet_transaction.yaml +22 -0
  397. package/hedhog/table/certificate.yaml +3 -0
  398. package/hedhog/table/course.yaml +8 -0
  399. package/hedhog/table/course_file.yaml +23 -0
  400. package/hedhog/table/course_lesson.yaml +5 -0
  401. package/hedhog/table/course_lesson_discussion_like.yaml +21 -0
  402. package/hedhog/table/course_lesson_discussion_topic.yaml +35 -0
  403. package/hedhog/table/course_lesson_file.yaml +8 -0
  404. package/hedhog/table/course_lesson_note.yaml +34 -0
  405. package/hedhog/table/course_video_resolution_profile.yaml +22 -0
  406. package/hedhog/table/exam.yaml +5 -0
  407. package/hedhog/table/learning_path_enrollment.yaml +6 -0
  408. package/hedhog/table/question.yaml +10 -0
  409. package/hedhog/table/question_subject.yaml +17 -0
  410. package/hedhog/table/student_activity_streak.yaml +25 -0
  411. package/hedhog/table/video_resolution_profile.yaml +18 -0
  412. package/package.json +8 -7
  413. package/src/achievement/achievement.controller.ts +60 -0
  414. package/src/achievement/achievement.mcp-tools.ts +108 -0
  415. package/src/achievement/achievement.module.ts +13 -0
  416. package/src/achievement/achievement.service.ts +252 -0
  417. package/src/achievement/dto/create-achievement.dto.ts +50 -0
  418. package/src/achievement/dto/update-achievement.dto.ts +47 -0
  419. package/src/bitcode-wallet/bitcode-wallet.controller.ts +69 -0
  420. package/src/bitcode-wallet/bitcode-wallet.mcp-tools.ts +107 -0
  421. package/src/bitcode-wallet/bitcode-wallet.module.ts +13 -0
  422. package/src/bitcode-wallet/bitcode-wallet.service.ts +361 -0
  423. package/src/bitcode-wallet/dto/create-bitcode-wallet-transaction.dto.ts +27 -0
  424. package/src/bitcode-wallet/dto/create-bitcode-wallet.dto.ts +7 -0
  425. package/src/bitcode-wallet/dto/update-bitcode-wallet-transaction.dto.ts +28 -0
  426. package/src/bitcode-wallet/dto/update-bitcode-wallet.dto.ts +8 -0
  427. package/src/certificate/certificate.controller.ts +17 -3
  428. package/src/certificate/certificate.mcp-tools.ts +131 -0
  429. package/src/certificate/certificate.module.ts +2 -1
  430. package/src/certificate/certificate.service.ts +193 -7
  431. package/src/certificate/dto/update-certificate-public-access.dto.ts +6 -0
  432. package/src/class-group/class-group.mcp-tools.ts +435 -0
  433. package/src/class-group/class-group.module.ts +2 -1
  434. package/src/class-group/class-group.service.ts +51 -1
  435. package/src/course/course-operations-integration.service.ts +520 -0
  436. package/src/course/course-structure.controller.ts +46 -10
  437. package/src/course/course-structure.service.ts +236 -27
  438. package/src/course/course-video-conversion.service.ts +415 -0
  439. package/src/course/course.controller.ts +18 -0
  440. package/src/course/course.mcp-tools.ts +409 -0
  441. package/src/course/course.module.ts +23 -3
  442. package/src/course/course.service.ts +178 -29
  443. package/src/course/dto/create-course-structure-lesson.dto.ts +13 -2
  444. package/src/course/dto/create-course.dto.ts +16 -0
  445. package/src/course/dto/update-course-resources.dto.ts +39 -0
  446. package/src/course-lesson-discussion/course-lesson-discussion.controller.ts +55 -0
  447. package/src/course-lesson-discussion/course-lesson-discussion.mcp-tools.ts +75 -0
  448. package/src/course-lesson-discussion/course-lesson-discussion.module.ts +13 -0
  449. package/src/course-lesson-discussion/course-lesson-discussion.service.ts +354 -0
  450. package/src/course-lesson-discussion/dto/create-course-lesson-discussion-topic.dto.ts +16 -0
  451. package/src/course-lesson-note/course-lesson-note.controller.ts +68 -0
  452. package/src/course-lesson-note/course-lesson-note.mcp-tools.ts +96 -0
  453. package/src/course-lesson-note/course-lesson-note.module.ts +13 -0
  454. package/src/course-lesson-note/course-lesson-note.service.ts +248 -0
  455. package/src/course-lesson-note/dto/create-course-lesson-note.dto.ts +16 -0
  456. package/src/course-lesson-note/dto/update-course-lesson-note.dto.ts +18 -0
  457. package/src/dashboard/dashboard.mcp-tools.ts +23 -0
  458. package/src/dashboard/dashboard.module.ts +2 -1
  459. package/src/enterprise/enterprise.controller.ts +0 -1
  460. package/src/enterprise/enterprise.mcp-tools.ts +403 -0
  461. package/src/enterprise/enterprise.module.ts +2 -1
  462. package/src/enterprise/training/enterprise-training.module.ts +11 -1
  463. package/src/enterprise/training/training-admin.mcp-tools.ts +479 -0
  464. package/src/enterprise/training/training-instructor.mcp-tools.ts +210 -0
  465. package/src/enterprise/training/training-student.controller.ts +17 -1
  466. package/src/enterprise/training/training-student.mcp-tools.ts +136 -0
  467. package/src/enterprise/training/training-student.service.ts +167 -1
  468. package/src/evaluation/evaluation.mcp-tools.ts +155 -0
  469. package/src/evaluation/evaluation.module.ts +2 -1
  470. package/src/evaluation/evaluation.service.ts +9 -2
  471. package/src/exam/dto/create-exam-question.dto.ts +8 -0
  472. package/src/exam/dto/create-exam.dto.ts +8 -0
  473. package/src/exam/dto/create-question-subject.dto.ts +12 -0
  474. package/src/exam/exam-attempt.service.ts +46 -14
  475. package/src/exam/exam.controller.ts +19 -0
  476. package/src/exam/exam.mcp-tools.ts +337 -0
  477. package/src/exam/exam.module.ts +2 -1
  478. package/src/exam/exam.service.ts +121 -0
  479. package/src/index.ts +10 -0
  480. package/src/instructor/instructor.mcp-tools.ts +243 -0
  481. package/src/instructor/instructor.module.ts +2 -1
  482. package/src/lms.module.ts +18 -1
  483. package/src/realtime/lms-realtime.controller.ts +12 -0
  484. package/src/realtime/lms-realtime.module.ts +12 -0
  485. package/src/realtime/lms-realtime.service.ts +98 -0
  486. package/src/realtime/lms-realtime.subscriber.ts +61 -0
  487. package/src/reports/reports.mcp-tools.ts +27 -0
  488. package/src/reports/reports.module.ts +2 -1
  489. package/src/training/training.mcp-tools.ts +128 -0
  490. package/src/training/training.module.ts +2 -1
  491. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +16 -0
  492. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +16 -0
  493. package/src/video-resolution-profile/video-resolution-profile.controller.ts +62 -0
  494. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +128 -0
  495. package/src/video-resolution-profile/video-resolution-profile.module.ts +13 -0
  496. 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: 'bunny', label: 'Bunny.net' },
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', 'bunny', 'custom'] as const)
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: lesson?.videoProvider ?? 'youtube',
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
- }, [lesson?.id]); // eslint-disable-line react-hooks/exhaustive-deps
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: String(res.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 numId = Number(id);
528
- if (Number.isInteger(numId) && numId > 0) {
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, numId);
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 handleResourceDownload(res: Resource) {
543
- if (!res.url) {
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 = res.url;
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-3 px-4 py-3 border-b bg-muted/30 shrink-0">
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-9 items-center justify-center rounded-lg shrink-0',
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 defaultValue="dados" className="flex flex-col flex-1 min-h-0">
739
- <TabsList className="mx-3 mt-2 h-8 w-auto justify-start shrink-0 bg-muted/50">
740
- <TabsTrigger value="dados" className="text-xs h-7 px-2.5">
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 value="conteudo" className="text-xs h-7 px-2.5">
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
- <TabsTrigger value="recursos" className="text-xs h-7 px-2.5">
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-3 gap-2">
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-2 gap-2">
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
- {lbl}
905
- </span>
906
- </SelectItem>
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" /> {t('lessonForm.public')}
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" /> {t('lessonForm.private')}
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" /> {t('questionEditor.restricted')}
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('questionEditor.removeInstructor', {
1028
- name: displayName,
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" /> {t('lessonForm.publicDescription')}
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" /> {t('lessonForm.privateDescription')}
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('questionEditor.searchQuestion')}
1252
- emptyLabel={t('questionEditor.noQuestionsFound')}
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
- {localResources.length > 0 && (
2555
+ {genericResources.length > 0 && (
1380
2556
  <p className="text-xs text-muted-foreground">
1381
2557
  {t('lessonForm.resourcesCount', {
1382
- count: localResources.length,
2558
+ count: genericResources.length,
1383
2559
  })}
1384
2560
  </p>
1385
2561
  )}
1386
2562
 
1387
2563
  {/* Resource list */}
1388
- {localResources.length === 0 ? (
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
- {localResources.map((res) => {
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={() => form.reset()}
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
- <SheetContent
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
- Object.keys(questionTypeLabels) as QuestionType[]
1553
- ).map((t) => (
1554
- <SelectItem key={t} value={t}>
1555
- {questionTypeLabels[t]}
1556
- </SelectItem>
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
- </SheetContent>
3076
+ </ResizableSheetContent>
1896
3077
  </Sheet>
1897
3078
  </form>
1898
3079
  </Form>