@hed-hog/lms 0.0.364 → 0.0.366

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 (290) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/class-group/class-group.controller.d.ts +1 -0
  6. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  7. package/dist/class-group/class-group.service.d.ts +1 -0
  8. package/dist/class-group/class-group.service.d.ts.map +1 -1
  9. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  10. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  12. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  13. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  14. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  15. package/dist/course/course-export-scorm12.service.js +628 -0
  16. package/dist/course/course-export-scorm12.service.js.map +1 -0
  17. package/dist/course/course-export.service.d.ts +84 -0
  18. package/dist/course/course-export.service.d.ts.map +1 -0
  19. package/dist/course/course-export.service.js +237 -0
  20. package/dist/course/course-export.service.js.map +1 -0
  21. package/dist/course/course-structure.controller.d.ts +20 -10
  22. package/dist/course/course-structure.controller.d.ts.map +1 -1
  23. package/dist/course/course-structure.controller.js +20 -4
  24. package/dist/course/course-structure.controller.js.map +1 -1
  25. package/dist/course/course-structure.service.d.ts +12 -4
  26. package/dist/course/course-structure.service.d.ts.map +1 -1
  27. package/dist/course/course-structure.service.js +98 -23
  28. package/dist/course/course-structure.service.js.map +1 -1
  29. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  30. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  31. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  32. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  33. package/dist/course/course-video-hls.service.d.ts +71 -0
  34. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  35. package/dist/course/course-video-hls.service.js +784 -0
  36. package/dist/course/course-video-hls.service.js.map +1 -0
  37. package/dist/course/course.controller.d.ts +47 -13
  38. package/dist/course/course.controller.d.ts.map +1 -1
  39. package/dist/course/course.controller.js +40 -26
  40. package/dist/course/course.controller.js.map +1 -1
  41. package/dist/course/course.mcp-tools.js +1 -1
  42. package/dist/course/course.mcp-tools.js.map +1 -1
  43. package/dist/course/course.module.d.ts.map +1 -1
  44. package/dist/course/course.module.js +16 -0
  45. package/dist/course/course.module.js.map +1 -1
  46. package/dist/course/course.service.d.ts +8 -9
  47. package/dist/course/course.service.d.ts.map +1 -1
  48. package/dist/course/course.service.js +93 -50
  49. package/dist/course/course.service.js.map +1 -1
  50. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  51. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  52. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  53. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  54. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  55. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  56. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  57. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  58. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  59. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  61. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  62. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  63. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  64. package/dist/course/dto/create-course-export.dto.js +71 -0
  65. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  66. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  67. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  68. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  69. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  70. package/dist/course/ffmpeg.util.d.ts +10 -0
  71. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  72. package/dist/course/ffmpeg.util.js +79 -0
  73. package/dist/course/ffmpeg.util.js.map +1 -0
  74. package/dist/course/lms-bulk-upload-automation.service.d.ts +18 -1
  75. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload-automation.service.js +106 -8
  77. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  78. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  79. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  80. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  81. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  82. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  83. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  84. package/dist/course/lms-bulk-upload.controller.js +43 -2
  85. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  86. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  87. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  88. package/dist/course/lms-bulk-upload.service.js +59 -6
  89. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  90. package/dist/course/lms-setting.controller.d.ts +2 -1
  91. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  92. package/dist/course/lms-setting.controller.js +4 -2
  93. package/dist/course/lms-setting.controller.js.map +1 -1
  94. package/dist/course/scorm12-schemas.d.ts +4 -0
  95. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  96. package/dist/course/scorm12-schemas.js +9 -0
  97. package/dist/course/scorm12-schemas.js.map +1 -0
  98. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  99. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  100. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  101. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  102. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  103. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  104. package/dist/enterprise/training/training-student.service.js +217 -4
  105. package/dist/enterprise/training/training-student.service.js.map +1 -1
  106. package/dist/evaluation/evaluation.service.d.ts +18 -0
  107. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  108. package/dist/evaluation/evaluation.service.js +125 -0
  109. package/dist/evaluation/evaluation.service.js.map +1 -1
  110. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  111. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  112. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  113. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  114. package/dist/exam/exam.module.d.ts.map +1 -1
  115. package/dist/exam/exam.module.js +2 -1
  116. package/dist/exam/exam.module.js.map +1 -1
  117. package/dist/exam/exam.service.d.ts +21 -0
  118. package/dist/exam/exam.service.d.ts.map +1 -1
  119. package/dist/exam/exam.service.js +80 -0
  120. package/dist/exam/exam.service.js.map +1 -1
  121. package/dist/exam/question.controller.d.ts +27 -0
  122. package/dist/exam/question.controller.d.ts.map +1 -0
  123. package/dist/exam/question.controller.js +53 -0
  124. package/dist/exam/question.controller.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  126. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  127. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  128. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  129. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  130. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  131. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  132. package/dist/lms-commerce-access.subscriber.js +74 -0
  133. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +16 -5
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  138. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  140. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  141. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  142. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  143. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  144. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  145. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  146. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  147. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  148. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  149. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  150. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  151. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  152. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  153. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  154. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  155. package/dist/platforma/platforma-performance.service.js +500 -0
  156. package/dist/platforma/platforma-performance.service.js.map +1 -0
  157. package/dist/platforma/platforma-search.service.d.ts +21 -0
  158. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  159. package/dist/platforma/platforma-search.service.js +64 -0
  160. package/dist/platforma/platforma-search.service.js.map +1 -0
  161. package/dist/platforma/platforma-video.service.d.ts +39 -0
  162. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  163. package/dist/platforma/platforma-video.service.js +301 -0
  164. package/dist/platforma/platforma-video.service.js.map +1 -0
  165. package/dist/platforma/platforma.controller.d.ts +209 -1
  166. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  167. package/dist/platforma/platforma.controller.js +208 -2
  168. package/dist/platforma/platforma.controller.js.map +1 -1
  169. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  170. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  171. package/dist/realtime/lms-realtime.controller.js +31 -0
  172. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  173. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  174. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  175. package/dist/realtime/lms-realtime.service.js.map +1 -1
  176. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  177. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  178. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  179. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  180. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  181. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  182. package/dist/student-xp/student-xp.controller.js +24 -0
  183. package/dist/student-xp/student-xp.controller.js.map +1 -1
  184. package/dist/student-xp/student-xp.service.d.ts +16 -0
  185. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  186. package/dist/student-xp/student-xp.service.js +51 -1
  187. package/dist/student-xp/student-xp.service.js.map +1 -1
  188. package/hedhog/data/evaluation_topic.yaml +17 -0
  189. package/hedhog/data/menu.yaml +0 -17
  190. package/hedhog/data/queue_definition.yaml +48 -0
  191. package/hedhog/data/route.yaml +94 -124
  192. package/hedhog/data/setting_group.yaml +19 -19
  193. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  194. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  195. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  196. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  197. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  198. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  199. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  200. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +158 -45
  201. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  202. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  203. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  204. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  205. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  206. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  207. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  208. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  209. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  210. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  211. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  212. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  213. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  214. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  215. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  216. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -1
  217. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  218. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  219. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  220. package/hedhog/frontend/app/courses/page.tsx.ejs +85 -9
  221. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  222. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  223. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  224. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  225. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  226. package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
  227. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  228. package/hedhog/frontend/messages/en.json +44 -28
  229. package/hedhog/frontend/messages/pt.json +47 -29
  230. package/hedhog/table/course_enrollment.yaml +3 -0
  231. package/hedhog/table/course_export.yaml +62 -0
  232. package/hedhog/table/lesson_view_event.yaml +66 -0
  233. package/package.json +14 -9
  234. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  235. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  236. package/src/course/course-export-scorm12.service.ts +668 -0
  237. package/src/course/course-export.service.ts +280 -0
  238. package/src/course/course-structure.controller.ts +16 -2
  239. package/src/course/course-structure.service.ts +100 -7
  240. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  241. package/src/course/course-video-hls.service.ts +966 -0
  242. package/src/course/course.controller.ts +33 -19
  243. package/src/course/course.mcp-tools.ts +1 -1
  244. package/src/course/course.module.ts +16 -0
  245. package/src/course/course.service.ts +119 -61
  246. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  247. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  248. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  249. package/src/course/dto/create-course-export.dto.ts +56 -0
  250. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  251. package/src/course/ffmpeg.util.ts +65 -0
  252. package/src/course/lms-bulk-upload-automation.service.ts +156 -6
  253. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  254. package/src/course/lms-bulk-upload.controller.ts +32 -2
  255. package/src/course/lms-bulk-upload.service.ts +70 -7
  256. package/src/course/lms-setting.controller.ts +4 -2
  257. package/src/course/scorm12-schemas.ts +9 -0
  258. package/src/enterprise/training/training-student.service.ts +221 -2
  259. package/src/evaluation/evaluation.service.ts +123 -0
  260. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  261. package/src/exam/exam.module.ts +2 -1
  262. package/src/exam/exam.service.ts +86 -0
  263. package/src/exam/question.controller.ts +28 -0
  264. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  265. package/src/lms-commerce-access.subscriber.ts +88 -0
  266. package/src/lms.module.ts +16 -5
  267. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  268. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  269. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  270. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  271. package/src/platforma/platforma-performance.service.ts +606 -0
  272. package/src/platforma/platforma-search.service.ts +48 -0
  273. package/src/platforma/platforma-video.service.ts +346 -0
  274. package/src/platforma/platforma.controller.ts +137 -1
  275. package/src/platforma/platforma.service.ts +268 -268
  276. package/src/realtime/lms-realtime.controller.ts +27 -1
  277. package/src/realtime/lms-realtime.service.ts +2 -1
  278. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  279. package/src/student-xp/student-xp.controller.ts +18 -2
  280. package/src/student-xp/student-xp.service.ts +84 -2
  281. package/hedhog/data/video_resolution_profile.yaml +0 -7
  282. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  283. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  284. package/hedhog/table/video_resolution_profile.yaml +0 -18
  285. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  286. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  287. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  288. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  289. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  290. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -8,7 +8,6 @@ import {
8
8
  ChevronUp,
9
9
  CircleDot,
10
10
  CircleOff,
11
- ClipboardList,
12
11
  Clock,
13
12
  Download,
14
13
  ExternalLink,
@@ -41,7 +40,6 @@ import { z } from 'zod';
41
40
 
42
41
  import { CopyButton } from '@/components/copy-button';
43
42
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
44
- import { Badge } from '@/components/ui/badge';
45
43
  import { Button } from '@/components/ui/button';
46
44
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
47
45
  import {
@@ -65,7 +63,6 @@ import { Label } from '@/components/ui/label';
65
63
  import { Progress } from '@/components/ui/progress';
66
64
  import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
67
65
  import { ScrollArea } from '@/components/ui/scroll-area';
68
- import { Skeleton } from '@/components/ui/skeleton';
69
66
  import {
70
67
  Select,
71
68
  SelectContent,
@@ -80,6 +77,7 @@ import {
80
77
  SheetHeader,
81
78
  SheetTitle,
82
79
  } from '@/components/ui/sheet';
80
+ import { Skeleton } from '@/components/ui/skeleton';
83
81
  import { Switch } from '@/components/ui/switch';
84
82
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
85
83
  import { Textarea } from '@/components/ui/textarea';
@@ -115,11 +113,14 @@ import {
115
113
  getQueueJob,
116
114
  updateLessonFrame,
117
115
  uploadFile,
116
+ type ApiQuestion,
118
117
  type QueueJobResponse,
119
118
  type QueueJobStatus,
120
119
  } from '../_data/services/course-structure.service';
121
120
  import {
121
+ useCreateQuestionMutation,
122
122
  useDeleteLessonMutation,
123
+ useQuestionsQuery,
123
124
  useUpdateLessonMutation,
124
125
  useUpdateResourceTypeMutation,
125
126
  } from '../_data/use-course-structure-mutations';
@@ -187,10 +188,6 @@ function getInstructorAvatarUrl(avatarId?: number | null): string | undefined {
187
188
  : `/person/avatar/${avatarId}`;
188
189
  }
189
190
 
190
- function videoProfileResourceType(profileId: number): string {
191
- return `video_profile:${profileId}`;
192
- }
193
-
194
191
  const MAX_VIDEO_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024;
195
192
 
196
193
  type LessonTypeLabelKey = `types.${LessonType}`;
@@ -205,7 +202,8 @@ type VideoJobEventMessageKey = `lessonForm.videoJobEvents.${VideoJobEventType}`;
205
202
  type VideoJobProgressPhase =
206
203
  | 'download_original'
207
204
  | 'probe_duration'
208
- | 'convert_profile'
205
+ | 'hls_encode'
206
+ | 'hls_upload'
209
207
  | 'extract_frames'
210
208
  | 'extract_frames_done'
211
209
  | 'extract_audio'
@@ -317,7 +315,8 @@ const VIDEO_JOB_STATUS_COLORS: Record<QueueJobStatus, string> = {
317
315
  const VIDEO_JOB_PROGRESS_PHASES = new Set<VideoJobProgressPhase>([
318
316
  'download_original',
319
317
  'probe_duration',
320
- 'convert_profile',
318
+ 'hls_encode',
319
+ 'hls_upload',
321
320
  'extract_frames',
322
321
  'extract_frames_done',
323
322
  'extract_audio',
@@ -410,19 +409,6 @@ function getVideoJobProgressMessage(
410
409
  const phase = getVideoJobProgressPhase(event);
411
410
  const metadata = event.metadata ?? {};
412
411
 
413
- if (phase === 'convert_profile') {
414
- const profileName =
415
- typeof metadata.profileName === 'string' && metadata.profileName.trim()
416
- ? metadata.profileName.trim()
417
- : typeof metadata.profileId === 'number'
418
- ? `#${metadata.profileId}`
419
- : '—';
420
-
421
- return t('lessonForm.videoJobProgress.convert_profile', {
422
- profileName,
423
- });
424
- }
425
-
426
412
  if (phase === 'extract_frames_done') {
427
413
  const count =
428
414
  typeof metadata.frames === 'number' && Number.isFinite(metadata.frames)
@@ -504,12 +490,6 @@ const TYPE_CONFIG: Record<
504
490
  bg: 'bg-amber-500/10',
505
491
  labelKey: 'types.questao',
506
492
  },
507
- exercicio: {
508
- icon: ClipboardList,
509
- color: 'text-purple-500',
510
- bg: 'bg-purple-500/10',
511
- labelKey: 'types.exercicio',
512
- },
513
493
  };
514
494
 
515
495
  const STATUS_COLORS: Record<LessonStatus, string> = {
@@ -553,39 +533,15 @@ function generateAltId(): string {
553
533
  return Math.random().toString(36).slice(2, 9);
554
534
  }
555
535
 
556
- const MOCK_QUESTIONS: MockQuestion[] = [
557
- {
558
- id: 'q1',
559
- title: 'O que é React?',
560
- type: 'multiple_choice',
561
- points: 1,
562
- alternatives: [
563
- {
564
- id: 'a1',
565
- texto: 'Uma biblioteca JavaScript para interfaces',
566
- correta: true,
567
- },
568
- { id: 'a2', texto: 'Um framework CSS', correta: false },
569
- { id: 'a3', texto: 'Um banco de dados', correta: false },
570
- ],
571
- },
572
- {
573
- id: 'q2',
574
- title: 'Defina componente funcional',
575
- type: 'essay',
576
- points: 2,
577
- },
578
- {
579
- id: 'q3',
580
- title: 'TypeScript é um superset de JavaScript?',
581
- type: 'true_false',
582
- points: 1,
583
- alternatives: [
584
- { id: 'true', texto: 'Verdadeiro', correta: true },
585
- { id: 'false', texto: 'Falso', correta: false },
586
- ],
587
- },
588
- ];
536
+ function apiQuestionToMock(q: ApiQuestion): MockQuestion {
537
+ return {
538
+ id: String(q.id),
539
+ title: q.statement.replace(/<[^>]+>/g, '').slice(0, 80),
540
+ type: (q.questionType as QuestionType) ?? 'multiple_choice',
541
+ statement: q.statement,
542
+ points: q.points,
543
+ };
544
+ }
589
545
 
590
546
  type FormValues = {
591
547
  code: string;
@@ -600,7 +556,7 @@ type FormValues = {
600
556
  videoUrl?: string;
601
557
  transcription?: string;
602
558
  postContent?: string;
603
- questionId?: string | null;
559
+ linkedExam?: string | null;
604
560
  };
605
561
 
606
562
  type EditableTranscriptionSegment = {
@@ -792,17 +748,23 @@ function SortableAlternativa({
792
748
 
793
749
  // ── Component ─────────────────────────────────────────────────────────────────
794
750
 
751
+ const LESSON_TABS: LessonEditorTab[] = [
752
+ 'dados',
753
+ 'conteudo',
754
+ 'videos',
755
+ 'imagens',
756
+ 'transcricao',
757
+ 'audios',
758
+ 'recursos',
759
+ 'xp',
760
+ ];
761
+
795
762
  interface EditorLessonProps {
796
763
  lessonId: string;
764
+ defaultTab?: string;
765
+ onTabChange?: (tab: string) => void;
797
766
  }
798
767
 
799
- type VideoProfileOption = {
800
- id: number;
801
- name: string;
802
- ffmpeg_params: string;
803
- status: string;
804
- };
805
-
806
768
  type FramePreviewSource = {
807
769
  key: string;
808
770
  label: string;
@@ -819,7 +781,7 @@ type FrameAssetMetadata = {
819
781
  sizeLabel: string;
820
782
  };
821
783
 
822
- export function EditorLesson({ lessonId }: EditorLessonProps) {
784
+ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLessonProps) {
823
785
  const t = useTranslations('lms.CoursesPage.StructurePage');
824
786
  const tabAudiosLabel = (() => {
825
787
  try {
@@ -881,11 +843,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
881
843
  fill_blank: t('questionEditor.types.fillBlank'),
882
844
  matching: t('questionEditor.types.matching'),
883
845
  };
884
- const isVideoConversionEnabled = lmsSettings.videoConversionEnabled;
885
846
  const schema = z.object({
886
847
  code: z.string().min(1, t('questionEditor.validation.codeRequired')),
887
848
  title: z.string().min(1, t('questionEditor.validation.titleRequired')),
888
- type: z.enum(['video', 'post', 'questao', 'exercicio'] as const),
849
+ type: z.enum(['video', 'post', 'questao'] as const),
889
850
  duration: z.coerce.number().min(0),
890
851
  status: z.enum([
891
852
  'preparada',
@@ -903,7 +864,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
903
864
  videoUrl: z.string().optional(),
904
865
  transcription: z.string().optional(),
905
866
  postContent: z.string().optional(),
906
- questionId: z.string().nullable().optional(),
867
+ linkedExam: z.string().nullable().optional(),
907
868
  });
908
869
 
909
870
  const instructorPool =
@@ -927,7 +888,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
927
888
  videoUrl: lesson?.videoUrl ?? '',
928
889
  transcription: lesson?.transcription ?? '',
929
890
  postContent: lesson?.postContent ?? '',
930
- questionId: null,
891
+ linkedExam: lesson?.linkedExam ?? null,
931
892
  });
932
893
 
933
894
  const form = useForm<FormValues>({
@@ -938,6 +899,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
938
899
  const { isDirty } = form.formState;
939
900
  const watchedType = useWatch({ control: form.control, name: 'type' });
940
901
  const watchedStatus = useWatch({ control: form.control, name: 'status' });
902
+ const watchedPublished = useWatch({
903
+ control: form.control,
904
+ name: 'published',
905
+ });
941
906
  const watchedVideoProvider = useWatch({
942
907
  control: form.control,
943
908
  name: 'videoProvider',
@@ -953,7 +918,11 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
953
918
  () => lesson?.resources ?? []
954
919
  );
955
920
  const [jobTimerNowMs, setJobTimerNowMs] = useState<number>(() => Date.now());
956
- const [activeTab, setActiveTab] = useState<LessonEditorTab>('dados');
921
+ const [activeTab, setActiveTab] = useState<LessonEditorTab>(() =>
922
+ defaultTab && LESSON_TABS.includes(defaultTab as LessonEditorTab)
923
+ ? (defaultTab as LessonEditorTab)
924
+ : 'dados'
925
+ );
957
926
  const [isJobFeedbackCollapsed, setIsJobFeedbackCollapsed] = useState<boolean>(
958
927
  () => readVideoJobFeedbackCollapsedPreference()
959
928
  );
@@ -973,9 +942,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
973
942
  >(null);
974
943
  const [isRequeueingOriginalVideo, setIsRequeueingOriginalVideo] =
975
944
  useState(false);
976
- const [profileUploadProgress, setProfileUploadProgress] = useState<
977
- Record<number, number>
978
- >({});
979
945
  const [isResolvingVideoPreview, setIsResolvingVideoPreview] = useState(false);
980
946
  const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
981
947
  const [videoPreviewResource, setVideoPreviewResource] =
@@ -1113,24 +1079,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1113
1079
  isLoading: isLoadingTranscription,
1114
1080
  } = useTranscriptionSegmentsQuery(lesson?.id ?? null);
1115
1081
 
1116
- const {
1117
- data: courseVideoProfiles = [],
1118
- isFetching: isFetchingCourseVideoProfiles,
1119
- isError: hasCourseVideoProfilesError,
1120
- refetch: refetchCourseVideoProfiles,
1121
- } = useQuery<VideoProfileOption[]>({
1122
- queryKey: ['lms-course-video-resolution-profiles', courseId],
1123
- queryFn: async () => {
1124
- const response = await request<VideoProfileOption[]>({
1125
- url: `/lms/courses/${courseId}/video-resolution-profiles`,
1126
- method: 'GET',
1127
- });
1128
- return response.data ?? [];
1129
- },
1130
- enabled: Boolean(courseId),
1131
- initialData: [],
1132
- });
1133
-
1134
1082
  const {
1135
1083
  data: conversionJob,
1136
1084
  isFetching: isFetchingConversionJob,
@@ -1180,13 +1128,19 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1180
1128
  );
1181
1129
 
1182
1130
  // ── Question sheet state ────────────────────────────────────────────────────
1131
+ const { data: questionsData, isLoading: questionsLoading } = useQuestionsQuery();
1132
+ const createQuestionMutation = useCreateQuestionMutation();
1183
1133
  const [questionSheetOpen, setQuestionSheetOpen] = useState(false);
1184
1134
  const [editingQuestion, setEditingQuestion] = useState<MockQuestion | null>(
1185
1135
  null
1186
1136
  );
1187
- const [selectedQuestion, setSelectedQuestion] = useState<MockQuestion | null>(
1188
- null
1189
- );
1137
+ const [selectedQuestion, setSelectedQuestion] = useState<MockQuestion | null>(null);
1138
+
1139
+ useEffect(() => {
1140
+ if (!lesson?.linkedExam || !questionsData) return;
1141
+ const found = questionsData.data.find((q) => String(q.id) === lesson.linkedExam);
1142
+ if (found) setSelectedQuestion(apiQuestionToMock(found));
1143
+ }, [lesson?.linkedExam, questionsData]);
1190
1144
  const [qSheetStatement, setQSheetStatement] = useState('');
1191
1145
  const [qSheetType, setQSheetType] = useState<QuestionType>('multiple_choice');
1192
1146
  const [qSheetPoints, setQSheetPoints] = useState(1);
@@ -1479,11 +1433,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1479
1433
  String(originalVideoResource.fileId ?? originalVideoResource.id)
1480
1434
  )
1481
1435
  : false;
1482
- const profileVideoResources = new Map(
1483
- localResources
1484
- .filter((res) => res.type.startsWith('video_profile:'))
1485
- .map((res) => [Number(res.type.replace('video_profile:', '')), res])
1486
- );
1487
1436
  const audioResources = localResources.filter(
1488
1437
  (res) => res.type === 'lesson_audio'
1489
1438
  );
@@ -1585,7 +1534,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1585
1534
  persistedVideoProvider !== 'file_storage');
1586
1535
  const canRequeueSavedOriginalVideo =
1587
1536
  Boolean(originalVideoResource?.fileId) && !isOriginalVideoUploadBlocked;
1588
- const isProfileVideoUploadBlocked = isConversionJobActive;
1589
1537
  const currentQueueJobId =
1590
1538
  focusedPipelineJob?.id ?? transcriptionJobId ?? conversionJobId;
1591
1539
  const isTranscriptionJobActive = transcriptionJob
@@ -1626,18 +1574,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1626
1574
  },
1627
1575
  ]
1628
1576
  : []),
1629
- ...courseVideoProfiles.flatMap((profile) => {
1630
- const resource = profileVideoResources.get(profile.id);
1631
- if (!resource) return [];
1632
-
1633
- return [
1634
- {
1635
- key: `profile:${profile.id}`,
1636
- label: profile.name,
1637
- resource,
1638
- },
1639
- ];
1640
- }),
1641
1577
  ];
1642
1578
  const activeFramePreviewSource =
1643
1579
  framePreviewSources.find(
@@ -2396,63 +2332,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2396
2332
  return frameAssetMetadataById[frame.id]?.sizeLabel ?? '—';
2397
2333
  }
2398
2334
 
2399
- async function handleVideoProfileFile(profileId: number, file: File) {
2400
- if (file.size > MAX_VIDEO_UPLOAD_SIZE_BYTES) {
2401
- const message = t('lessonForm.videoUploadMaxSizeError', {
2402
- size: '100MB',
2403
- });
2404
- setVideoUploadError(message);
2405
- toast.error(message);
2406
- return;
2407
- }
2408
-
2409
- setVideoUploadError(null);
2410
- setProfileUploadProgress((prev) => ({ ...prev, [profileId]: 0 }));
2411
- try {
2412
- const uploaded = await uploadFile(request, file, 'lms/lessons/videos', {
2413
- onUploadProgress: (event) => {
2414
- const total = event.total ?? 0;
2415
- const progress =
2416
- total > 0 ? Math.round((event.loaded / total) * 100) : 0;
2417
- setProfileUploadProgress((prev) => ({
2418
- ...prev,
2419
- [profileId]: progress,
2420
- }));
2421
- },
2422
- });
2423
- const type = videoProfileResourceType(profileId);
2424
- const resource: Resource = {
2425
- id: `new-${uploaded.id}`,
2426
- fileId: uploaded.id,
2427
- name: file.name,
2428
- size: formatFileSize(file.size),
2429
- type,
2430
- public: false,
2431
- uploadedAt: new Date().toISOString(),
2432
- url: undefined,
2433
- };
2434
- setLocalResources((prev) => [
2435
- ...prev.filter((item) => item.type !== type),
2436
- resource,
2437
- ]);
2438
- setResourcesDirty(true);
2439
- } catch {
2440
- toast.error(t('questionEditor.videoUploadFailed', { count: 1 }));
2441
- } finally {
2442
- setProfileUploadProgress((prev) => {
2443
- const next = { ...prev };
2444
- delete next[profileId];
2445
- return next;
2446
- });
2447
- }
2448
- }
2449
-
2450
2335
  async function handleOriginalVideoFile(file: File) {
2451
- if (!isVideoConversionEnabled) {
2452
- toast.error(t('lessonForm.videoConversionFailed'));
2453
- return;
2454
- }
2455
-
2456
2336
  if (
2457
2337
  watchedVideoProvider === 'file_storage' &&
2458
2338
  persistedVideoProvider !== 'file_storage'
@@ -2579,6 +2459,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2579
2459
  videoConversionJobId: conversionJobId ?? lesson?.videoConversionJobId,
2580
2460
  resources: localResources,
2581
2461
  instructorIds: selectedInstructorIds.map(Number),
2462
+ linkedExam: values.linkedExam ?? undefined,
2582
2463
  },
2583
2464
  },
2584
2465
  {
@@ -2696,28 +2577,43 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2696
2577
  setQSheetErrors(errors);
2697
2578
  return;
2698
2579
  }
2699
- const saved: MockQuestion = {
2700
- id: editingQuestion?.id ?? `q-${Date.now()}`,
2701
- title: qSheetStatement.replace(/<[^>]+>/g, '').slice(0, 80),
2702
- type: qSheetType,
2580
+
2581
+ const dto = {
2703
2582
  statement: qSheetStatement,
2583
+ questionType: qSheetType,
2704
2584
  points: qSheetPoints,
2705
2585
  alternatives:
2706
2586
  qSheetType === 'multiple_choice' || qSheetType === 'true_false'
2707
- ? qSheetAlts
2587
+ ? qSheetAlts.map((a) => ({ text: a.texto, isCorrect: a.correta }))
2708
2588
  : undefined,
2709
2589
  fillBlankAnswers:
2710
- qSheetType === 'fill_blank' ? qSheetFillBlanks : undefined,
2711
- matchingPairs: qSheetType === 'matching' ? qSheetPairs : undefined,
2590
+ qSheetType === 'fill_blank'
2591
+ ? qSheetFillBlanks.map((f) => ({
2592
+ answer: f.answer,
2593
+ alternatives: f.alternativesText
2594
+ ? f.alternativesText.split(',').map((s) => s.trim()).filter(Boolean)
2595
+ : undefined,
2596
+ }))
2597
+ : undefined,
2598
+ matchingPairs:
2599
+ qSheetType === 'matching'
2600
+ ? qSheetPairs.map((p) => ({ id: p.id, leftText: p.leftText, rightText: p.rightText }))
2601
+ : undefined,
2712
2602
  };
2713
- setSelectedQuestion(saved);
2714
- form.setValue('questionId', saved.id, { shouldDirty: true });
2715
- setQuestionSheetOpen(false);
2716
- toast.success(
2717
- editingQuestion
2718
- ? t('questionEditor.updated')
2719
- : t('questionEditor.created')
2720
- );
2603
+
2604
+ createQuestionMutation.mutate(dto, {
2605
+ onSuccess: (result) => {
2606
+ const mock = apiQuestionToMock(result);
2607
+ setSelectedQuestion(mock);
2608
+ form.setValue('linkedExam', String(result.id), { shouldDirty: true });
2609
+ setQuestionSheetOpen(false);
2610
+ toast.success(
2611
+ editingQuestion
2612
+ ? t('questionEditor.updated')
2613
+ : t('questionEditor.created')
2614
+ );
2615
+ },
2616
+ });
2721
2617
  }
2722
2618
 
2723
2619
  function handleDelete() {
@@ -2807,7 +2703,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2807
2703
  {/* ── Tabs ─────────────────────────────────────────────────────────── */}
2808
2704
  <Tabs
2809
2705
  value={activeTab}
2810
- onValueChange={(value) => setActiveTab(value as LessonEditorTab)}
2706
+ onValueChange={(value) => {
2707
+ setActiveTab(value as LessonEditorTab);
2708
+ onTabChange?.(value);
2709
+ }}
2811
2710
  className="flex flex-col flex-1 min-h-0 min-w-0"
2812
2711
  >
2813
2712
  <TabsList className="mt-0 h-auto w-full justify-start shrink-0 rounded-none border-b bg-muted/50 px-2 py-1 overflow-x-auto overflow-y-hidden whitespace-nowrap sm:px-3">
@@ -2990,48 +2889,57 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2990
2889
  </CardTitle>
2991
2890
  </CardHeader>
2992
2891
  <CardContent className="px-3 pb-2 flex flex-col gap-3">
2993
- <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
2994
- <FormField
2995
- control={form.control}
2996
- name="status"
2997
- render={({ field }) => (
2998
- <FormItem>
2999
- <FormLabel className="text-xs">
3000
- {t('questionEditor.productionStatus')}
3001
- </FormLabel>
3002
- <Select
3003
- value={field.value}
3004
- onValueChange={field.onChange}
3005
- >
3006
- <FormControl>
3007
- <SelectTrigger className="h-8 text-xs w-full">
3008
- <SelectValue />
3009
- </SelectTrigger>
3010
- </FormControl>
3011
- <SelectContent>
3012
- {(
3013
- Object.entries(statusLabels) as [
3014
- LessonStatus,
3015
- string,
3016
- ][]
3017
- ).map(([val, lbl]) => (
3018
- <SelectItem key={val} value={val}>
3019
- <span
3020
- className={cn(
3021
- 'text-xs px-1.5 py-0.5 rounded',
3022
- STATUS_COLORS[val]
3023
- )}
3024
- >
3025
- {lbl}
3026
- </span>
3027
- </SelectItem>
3028
- ))}
3029
- </SelectContent>
3030
- </Select>
3031
- <FormMessage className="text-xs" />
3032
- </FormItem>
3033
- )}
3034
- />
2892
+ <div
2893
+ className={cn(
2894
+ 'grid grid-cols-1 gap-2',
2895
+ watchedType === 'video' && !watchedPublished
2896
+ ? 'sm:grid-cols-2'
2897
+ : 'sm:grid-cols-1'
2898
+ )}
2899
+ >
2900
+ {watchedType === 'video' && !watchedPublished && (
2901
+ <FormField
2902
+ control={form.control}
2903
+ name="status"
2904
+ render={({ field }) => (
2905
+ <FormItem>
2906
+ <FormLabel className="text-xs">
2907
+ {t('questionEditor.productionStatus')}
2908
+ </FormLabel>
2909
+ <Select
2910
+ value={field.value}
2911
+ onValueChange={field.onChange}
2912
+ >
2913
+ <FormControl>
2914
+ <SelectTrigger className="h-8 text-xs w-full">
2915
+ <SelectValue />
2916
+ </SelectTrigger>
2917
+ </FormControl>
2918
+ <SelectContent>
2919
+ {(
2920
+ Object.entries(statusLabels) as [
2921
+ LessonStatus,
2922
+ string,
2923
+ ][]
2924
+ ).map(([val, lbl]) => (
2925
+ <SelectItem key={val} value={val}>
2926
+ <span
2927
+ className={cn(
2928
+ 'text-xs px-1.5 py-0.5 rounded',
2929
+ STATUS_COLORS[val]
2930
+ )}
2931
+ >
2932
+ {lbl}
2933
+ </span>
2934
+ </SelectItem>
2935
+ ))}
2936
+ </SelectContent>
2937
+ </Select>
2938
+ <FormMessage className="text-xs" />
2939
+ </FormItem>
2940
+ )}
2941
+ />
2942
+ )}
3035
2943
 
3036
2944
  <FormField
3037
2945
  control={form.control}
@@ -3311,20 +3219,25 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
3311
3219
  <CardContent className="px-3 pb-2 flex flex-col gap-2">
3312
3220
  <FormField
3313
3221
  control={form.control}
3314
- name="questionId"
3222
+ name="linkedExam"
3315
3223
  render={({ field }) => (
3316
3224
  <FormItem>
3317
3225
  <FormControl>
3318
- <EntityPicker<MockQuestion>
3226
+ <EntityPicker<ApiQuestion>
3319
3227
  value={field.value ?? null}
3320
3228
  onChange={(val) => {
3321
3229
  field.onChange(val);
3322
3230
  const found =
3323
- MOCK_QUESTIONS.find((q) => q.id === val) ??
3324
- null;
3325
- setSelectedQuestion(found);
3231
+ (questionsData?.data ?? []).find(
3232
+ (q) => String(q.id) === val
3233
+ ) ?? null;
3234
+ setSelectedQuestion(found ? apiQuestionToMock(found) : null);
3326
3235
  }}
3327
- placeholder={t('questionEditor.selectQuestion')}
3236
+ placeholder={
3237
+ questionsLoading
3238
+ ? t('questionEditor.loadingQuestions')
3239
+ : t('questionEditor.selectQuestion')
3240
+ }
3328
3241
  searchPlaceholder={t(
3329
3242
  'questionEditor.searchQuestion'
3330
3243
  )}
@@ -3332,11 +3245,14 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
3332
3245
  'questionEditor.noQuestionsFound'
3333
3246
  )}
3334
3247
  entityLabel={t('questionEditor.questionEntity')}
3335
- options={MOCK_QUESTIONS}
3336
- getOptionValue={(o) => o.id}
3337
- getOptionLabel={(o) => o.title}
3248
+ options={questionsData?.data ?? []}
3249
+ getOptionValue={(o) => String(o.id)}
3250
+ getOptionLabel={(o) =>
3251
+ o.statement.replace(/<[^>]+>/g, '').slice(0, 80)
3252
+ }
3338
3253
  getOptionDescription={(o) =>
3339
- questionTypeLabels[o.type]
3254
+ questionTypeLabels[o.questionType as QuestionType] ??
3255
+ o.questionType
3340
3256
  }
3341
3257
  />
3342
3258
  </FormControl>
@@ -3461,181 +3377,140 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
3461
3377
  </p>
3462
3378
  ) : null}
3463
3379
 
3464
- {isVideoConversionEnabled ? (
3465
- <Card className="bg-muted/20 py-2 gap-2 order-3">
3466
- <CardHeader className="px-3 pt-2 pb-1">
3467
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
3468
- {t('lessonForm.originalVideoTitle')}
3469
- </CardTitle>
3470
- </CardHeader>
3471
- <CardContent className="px-3 pb-2 flex flex-col gap-3">
3472
- <div className="rounded-lg border bg-background/90 p-3 shadow-sm">
3473
- <div className="flex items-start gap-3">
3474
- <div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-blue-600">
3475
- <Video className="size-4" />
3476
- </div>
3477
- <div className="min-w-0 flex-1 space-y-1">
3478
- <div className="flex items-start justify-between gap-2">
3479
- <div className="min-w-0">
3480
- <p className="truncate text-sm font-medium">
3481
- {originalVideoResource
3482
- ? originalVideoResource.name
3483
- : t('lessonForm.originalVideoTitle')}
3484
- </p>
3485
- <p className="text-xs text-muted-foreground">
3486
- {conversionJobId
3487
- ? t('lessonForm.videoConversionJob', {
3488
- id: conversionJobId,
3489
- })
3490
- : t('lessonForm.originalVideoHint')}
3491
- </p>
3492
- <p className="text-[0.65rem] text-muted-foreground">
3493
- {t('lessonForm.originalVideoPurpose')}
3494
- </p>
3495
- </div>
3496
- {originalVideoResource && (
3497
- <div className="flex shrink-0 items-center gap-1">
3498
- <IconActionTooltip
3499
- label={t('lessonForm.playVideoAria', {
3500
- name: originalVideoResource.name,
3501
- })}
3502
- asWrapper={isResolvingVideoPreview}
3503
- >
3504
- <Button
3505
- type="button"
3506
- variant="ghost"
3507
- size="icon"
3508
- className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-emerald-600"
3509
- disabled={isResolvingVideoPreview}
3510
- onClick={() =>
3511
- void openVideoPreview(
3512
- originalVideoResource
3513
- )
3514
- }
3515
- aria-label={t(
3516
- 'lessonForm.playVideoAria',
3517
- {
3518
- name: originalVideoResource.name,
3519
- }
3520
- )}
3521
- >
3522
- {isResolvingVideoPreview ? (
3523
- <Loader2 className="size-3 animate-spin" />
3524
- ) : (
3525
- <Play className="size-3" />
3526
- )}
3527
- </Button>
3528
- </IconActionTooltip>
3529
- <IconActionTooltip
3530
- label={t(
3531
- 'lessonForm.downloadVideoAria',
3380
+ <Card className="bg-muted/20 py-2 gap-2 order-3">
3381
+ <CardHeader className="px-3 pt-2 pb-1">
3382
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
3383
+ {t('lessonForm.originalVideoTitle')}
3384
+ </CardTitle>
3385
+ </CardHeader>
3386
+ <CardContent className="px-3 pb-2 flex flex-col gap-3">
3387
+ <div className="rounded-lg border bg-background/90 p-3 shadow-sm">
3388
+ <div className="flex items-start gap-3">
3389
+ <div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-blue-600">
3390
+ <Video className="size-4" />
3391
+ </div>
3392
+ <div className="min-w-0 flex-1 space-y-1">
3393
+ <div className="flex items-start justify-between gap-2">
3394
+ <div className="min-w-0">
3395
+ <p className="truncate text-sm font-medium">
3396
+ {originalVideoResource
3397
+ ? originalVideoResource.name
3398
+ : t('lessonForm.originalVideoTitle')}
3399
+ </p>
3400
+ <p className="text-xs text-muted-foreground">
3401
+ {conversionJobId
3402
+ ? t('lessonForm.videoConversionJob', {
3403
+ id: conversionJobId,
3404
+ })
3405
+ : t('lessonForm.originalVideoHint')}
3406
+ </p>
3407
+ <p className="text-[0.65rem] text-muted-foreground">
3408
+ {t('lessonForm.originalVideoPurpose')}
3409
+ </p>
3410
+ </div>
3411
+ {originalVideoResource && (
3412
+ <div className="flex shrink-0 items-center gap-1">
3413
+ <IconActionTooltip
3414
+ label={t('lessonForm.playVideoAria', {
3415
+ name: originalVideoResource.name,
3416
+ })}
3417
+ asWrapper={isResolvingVideoPreview}
3418
+ >
3419
+ <Button
3420
+ type="button"
3421
+ variant="ghost"
3422
+ size="icon"
3423
+ className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-emerald-600"
3424
+ disabled={isResolvingVideoPreview}
3425
+ onClick={() =>
3426
+ void openVideoPreview(
3427
+ originalVideoResource
3428
+ )
3429
+ }
3430
+ aria-label={t(
3431
+ 'lessonForm.playVideoAria',
3532
3432
  {
3533
3433
  name: originalVideoResource.name,
3534
3434
  }
3535
3435
  )}
3536
- asWrapper={isDownloadingOriginalVideo}
3537
3436
  >
3538
- <Button
3539
- type="button"
3540
- variant="ghost"
3541
- size="icon"
3542
- className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-amber-600"
3543
- disabled={
3544
- isDownloadingOriginalVideo
3545
- }
3546
- onClick={() =>
3547
- void handleResourceDownload(
3548
- originalVideoResource
3549
- )
3550
- }
3551
- aria-label={t(
3552
- 'lessonForm.downloadVideoAria',
3553
- {
3554
- name: originalVideoResource.name,
3555
- }
3556
- )}
3557
- >
3558
- {isDownloadingOriginalVideo ? (
3559
- <Loader2 className="size-3 animate-spin" />
3560
- ) : (
3561
- <Download className="size-3" />
3562
- )}
3563
- </Button>
3564
- </IconActionTooltip>
3565
- <IconActionTooltip
3566
- label={t('lessonForm.openVideoAria', {
3437
+ {isResolvingVideoPreview ? (
3438
+ <Loader2 className="size-3 animate-spin" />
3439
+ ) : (
3440
+ <Play className="size-3" />
3441
+ )}
3442
+ </Button>
3443
+ </IconActionTooltip>
3444
+ <IconActionTooltip
3445
+ label={t(
3446
+ 'lessonForm.downloadVideoAria',
3447
+ {
3567
3448
  name: originalVideoResource.name,
3568
- })}
3569
- >
3570
- <Button
3571
- type="button"
3572
- variant="ghost"
3573
- size="icon"
3574
- className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
3575
- onClick={() =>
3576
- void openResource(
3577
- originalVideoResource
3578
- )
3579
- }
3580
- aria-label={t(
3581
- 'lessonForm.openVideoAria',
3582
- {
3583
- name: originalVideoResource.name,
3584
- }
3585
- )}
3586
- >
3587
- <ExternalLink className="size-3" />
3588
- </Button>
3589
- </IconActionTooltip>
3590
- </div>
3591
- )}
3592
- </div>
3593
- {originalVideoResource?.size ? (
3594
- <div className="inline-flex rounded-full bg-muted px-2 py-0.5 text-[0.65rem] font-medium text-muted-foreground">
3595
- {originalVideoResource.size}
3596
- </div>
3597
- ) : null}
3598
- <div className="flex flex-wrap items-center gap-2 pt-1">
3599
- {originalVideoResource ? (
3600
- <>
3449
+ }
3450
+ )}
3451
+ asWrapper={isDownloadingOriginalVideo}
3452
+ >
3601
3453
  <Button
3602
3454
  type="button"
3603
- variant="secondary"
3604
- className="h-8 px-3 text-xs"
3605
- disabled={
3606
- isOriginalVideoUploadBlocked
3607
- }
3455
+ variant="ghost"
3456
+ size="icon"
3457
+ className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-amber-600"
3458
+ disabled={isDownloadingOriginalVideo}
3608
3459
  onClick={() =>
3609
- originalVideoInputRef.current?.click()
3460
+ void handleResourceDownload(
3461
+ originalVideoResource
3462
+ )
3610
3463
  }
3464
+ aria-label={t(
3465
+ 'lessonForm.downloadVideoAria',
3466
+ {
3467
+ name: originalVideoResource.name,
3468
+ }
3469
+ )}
3611
3470
  >
3612
- <UploadCloud className="size-3.5 mr-1" />
3613
- {t(
3614
- 'lessonForm.replaceOriginalForConversion'
3471
+ {isDownloadingOriginalVideo ? (
3472
+ <Loader2 className="size-3 animate-spin" />
3473
+ ) : (
3474
+ <Download className="size-3" />
3615
3475
  )}
3616
3476
  </Button>
3477
+ </IconActionTooltip>
3478
+ <IconActionTooltip
3479
+ label={t('lessonForm.openVideoAria', {
3480
+ name: originalVideoResource.name,
3481
+ })}
3482
+ >
3617
3483
  <Button
3618
3484
  type="button"
3619
- variant="outline"
3620
- className="h-8 px-3 text-xs"
3621
- disabled={
3622
- !canRequeueSavedOriginalVideo
3623
- }
3485
+ variant="ghost"
3486
+ size="icon"
3487
+ className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
3624
3488
  onClick={() =>
3625
- void handleRequeueOriginalVideo()
3489
+ void openResource(
3490
+ originalVideoResource
3491
+ )
3626
3492
  }
3627
- >
3628
- {isRequeueingOriginalVideo ? (
3629
- <Loader2 className="size-3.5 mr-1 animate-spin" />
3630
- ) : (
3631
- <RefreshCw className="size-3.5 mr-1" />
3632
- )}
3633
- {t(
3634
- 'lessonForm.retryConversionWithSavedOriginal'
3493
+ aria-label={t(
3494
+ 'lessonForm.openVideoAria',
3495
+ {
3496
+ name: originalVideoResource.name,
3497
+ }
3635
3498
  )}
3499
+ >
3500
+ <ExternalLink className="size-3" />
3636
3501
  </Button>
3637
- </>
3638
- ) : (
3502
+ </IconActionTooltip>
3503
+ </div>
3504
+ )}
3505
+ </div>
3506
+ {originalVideoResource?.size ? (
3507
+ <div className="inline-flex rounded-full bg-muted px-2 py-0.5 text-[0.65rem] font-medium text-muted-foreground">
3508
+ {originalVideoResource.size}
3509
+ </div>
3510
+ ) : null}
3511
+ <div className="flex flex-wrap items-center gap-2 pt-1">
3512
+ {originalVideoResource ? (
3513
+ <>
3639
3514
  <Button
3640
3515
  type="button"
3641
3516
  variant="secondary"
@@ -3647,50 +3522,83 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
3647
3522
  >
3648
3523
  <UploadCloud className="size-3.5 mr-1" />
3649
3524
  {t(
3650
- 'lessonForm.uploadOriginalForConversion'
3525
+ 'lessonForm.replaceOriginalForConversion'
3651
3526
  )}
3652
3527
  </Button>
3653
- )}
3654
- <span className="text-[0.65rem] text-muted-foreground">
3655
- {isConversionJobActive
3656
- ? t(
3657
- 'lessonForm.videoUploadBlockedWhileProcessing'
3658
- )
3659
- : isConversionJobStatusResolving
3660
- ? t('lessonForm.videoJobStateLoading')
3661
- : t('lessonForm.originalVideoHint')}
3662
- </span>
3663
- </div>
3664
- {originalUploadProgress !== null ? (
3665
- <div className="space-y-1 pt-1">
3666
- <Progress
3667
- value={originalUploadProgress}
3668
- className="h-1.5"
3669
- />
3670
- <p className="text-[0.65rem] text-muted-foreground">
3671
- {originalUploadProgress}%
3672
- </p>
3673
- </div>
3674
- ) : null}
3528
+ <Button
3529
+ type="button"
3530
+ variant="outline"
3531
+ className="h-8 px-3 text-xs"
3532
+ disabled={!canRequeueSavedOriginalVideo}
3533
+ onClick={() =>
3534
+ void handleRequeueOriginalVideo()
3535
+ }
3536
+ >
3537
+ {isRequeueingOriginalVideo ? (
3538
+ <Loader2 className="size-3.5 mr-1 animate-spin" />
3539
+ ) : (
3540
+ <RefreshCw className="size-3.5 mr-1" />
3541
+ )}
3542
+ {t(
3543
+ 'lessonForm.retryConversionWithSavedOriginal'
3544
+ )}
3545
+ </Button>
3546
+ </>
3547
+ ) : (
3548
+ <Button
3549
+ type="button"
3550
+ variant="secondary"
3551
+ className="h-8 px-3 text-xs"
3552
+ disabled={isOriginalVideoUploadBlocked}
3553
+ onClick={() =>
3554
+ originalVideoInputRef.current?.click()
3555
+ }
3556
+ >
3557
+ <UploadCloud className="size-3.5 mr-1" />
3558
+ {t(
3559
+ 'lessonForm.uploadOriginalForConversion'
3560
+ )}
3561
+ </Button>
3562
+ )}
3563
+ <span className="text-[0.65rem] text-muted-foreground">
3564
+ {isConversionJobActive
3565
+ ? t(
3566
+ 'lessonForm.videoUploadBlockedWhileProcessing'
3567
+ )
3568
+ : isConversionJobStatusResolving
3569
+ ? t('lessonForm.videoJobStateLoading')
3570
+ : t('lessonForm.originalVideoHint')}
3571
+ </span>
3675
3572
  </div>
3573
+ {originalUploadProgress !== null ? (
3574
+ <div className="space-y-1 pt-1">
3575
+ <Progress
3576
+ value={originalUploadProgress}
3577
+ className="h-1.5"
3578
+ />
3579
+ <p className="text-[0.65rem] text-muted-foreground">
3580
+ {originalUploadProgress}%
3581
+ </p>
3582
+ </div>
3583
+ ) : null}
3676
3584
  </div>
3677
- <input
3678
- ref={originalVideoInputRef}
3679
- type="file"
3680
- accept="video/*"
3681
- className="hidden"
3682
- onChange={(event) => {
3683
- const file = event.target.files?.[0];
3684
- if (file && !isOriginalVideoUploadBlocked) {
3685
- void handleOriginalVideoFile(file);
3686
- }
3687
- event.target.value = '';
3688
- }}
3689
- />
3690
3585
  </div>
3691
- </CardContent>
3692
- </Card>
3693
- ) : null}
3586
+ <input
3587
+ ref={originalVideoInputRef}
3588
+ type="file"
3589
+ accept="video/*"
3590
+ className="hidden"
3591
+ onChange={(event) => {
3592
+ const file = event.target.files?.[0];
3593
+ if (file && !isOriginalVideoUploadBlocked) {
3594
+ void handleOriginalVideoFile(file);
3595
+ }
3596
+ event.target.value = '';
3597
+ }}
3598
+ />
3599
+ </div>
3600
+ </CardContent>
3601
+ </Card>
3694
3602
 
3695
3603
  {conversionJobId && !shouldHidePipelineCard ? (
3696
3604
  <Card className="bg-muted/20 py-2 gap-2 order-4">
@@ -4047,259 +3955,43 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
4047
3955
  <Card className="bg-muted/20 py-2 gap-2 order-2">
4048
3956
  <CardHeader className="px-3 pt-2 pb-1">
4049
3957
  <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
4050
- {t('lessonForm.fileStorageVideosByResolution')}
3958
+ {t('lessonForm.hlsStatusTitle')}
4051
3959
  </CardTitle>
4052
3960
  </CardHeader>
4053
- <CardContent className="px-3 pb-2 flex flex-col gap-3">
4054
- {isFetchingCourseVideoProfiles ? (
4055
- <p className="text-xs text-muted-foreground">
4056
- {t('lessonForm.loadingVideoProfiles')}
4057
- </p>
4058
- ) : hasCourseVideoProfilesError ? (
4059
- <div className="flex flex-col gap-2">
4060
- <p className="text-xs text-destructive">
4061
- {t('lessonForm.videoProfilesLoadError')}
4062
- </p>
4063
- <Button
4064
- type="button"
4065
- variant="outline"
4066
- size="sm"
4067
- className="h-7 w-fit px-2 text-xs"
4068
- onClick={() =>
4069
- void refetchCourseVideoProfiles()
4070
- }
4071
- >
4072
- <RefreshCw className="size-3 mr-1" />
4073
- {t('lessonForm.retryLoadVideoProfiles')}
4074
- </Button>
4075
- </div>
4076
- ) : courseVideoProfiles.length === 0 ? (
4077
- <p className="text-xs text-muted-foreground">
4078
- {t('lessonForm.noVideoProfilesConfigured')}
4079
- </p>
4080
- ) : (
4081
- <>
4082
- {isConversionJobActive ? (
3961
+ <CardContent className="px-3 pb-2">
3962
+ {(() => {
3963
+ const hlsResource = localResources.find(
3964
+ (r) => r.type === 'video_hls'
3965
+ );
3966
+ if (hlsResource) {
3967
+ return (
3968
+ <div className="flex items-center gap-2">
3969
+ <div className="size-2 rounded-full bg-emerald-500 shrink-0" />
3970
+ <p className="text-xs text-emerald-700 dark:text-emerald-400">
3971
+ {t('lessonForm.hlsStatusReady')}
3972
+ </p>
3973
+ </div>
3974
+ );
3975
+ }
3976
+ if (isConversionJobActive) {
3977
+ return (
3978
+ <div className="flex items-center gap-2">
3979
+ <Loader2 className="size-3.5 animate-spin text-muted-foreground shrink-0" />
3980
+ <p className="text-xs text-muted-foreground">
3981
+ {t('lessonForm.hlsStatusProcessing')}
3982
+ </p>
3983
+ </div>
3984
+ );
3985
+ }
3986
+ return (
3987
+ <div className="flex items-center gap-2">
3988
+ <div className="size-2 rounded-full bg-muted-foreground/40 shrink-0" />
4083
3989
  <p className="text-xs text-muted-foreground">
4084
- {t(
4085
- 'lessonForm.videoProfilesLockedWhileProcessing'
4086
- )}
3990
+ {t('lessonForm.hlsStatusPending')}
4087
3991
  </p>
4088
- ) : null}
4089
- <div className="flex flex-col gap-1">
4090
- {courseVideoProfiles.map((profile) => {
4091
- const res = profileVideoResources.get(
4092
- profile.id
4093
- );
4094
- const hasVideo = Boolean(res);
4095
- const isDownloadingResource = res
4096
- ? downloadingResourceKeys.has(
4097
- String(res.fileId ?? res.id)
4098
- )
4099
- : false;
4100
- const currentUploadProgress =
4101
- profileUploadProgress[profile.id];
4102
- const inputId = `lesson-video-profile-${profile.id}`;
4103
-
4104
- return (
4105
- <div
4106
- key={profile.id}
4107
- className="flex items-center gap-2 rounded-md border bg-background/80 px-2.5 py-2"
4108
- >
4109
- <Video
4110
- className={cn(
4111
- 'size-3.5 shrink-0',
4112
- hasVideo
4113
- ? 'text-emerald-600'
4114
- : 'text-blue-600'
4115
- )}
4116
- />
4117
- <div className="flex-1 min-w-0">
4118
- <p className="text-xs truncate font-medium">
4119
- {profile.name}
4120
- </p>
4121
- {res ? (
4122
- (() => {
4123
- const metadata =
4124
- resolveResourceMetadata(res);
4125
-
4126
- return (
4127
- <p className="text-[0.65rem] text-muted-foreground">
4128
- {`${metadata.sizeLabel} · ${metadata.uploadedAtLabel}`}
4129
- </p>
4130
- );
4131
- })()
4132
- ) : (
4133
- <Badge className="mt-1 inline-flex items-center gap-1 border border-blue-200 bg-blue-100 text-blue-700 hover:bg-blue-100 dark:border-blue-800 dark:bg-blue-950/40 dark:text-blue-200 dark:hover:bg-blue-950/50">
4134
- {isConversionJobActive ? (
4135
- <Loader2 className="size-3 animate-spin" />
4136
- ) : (
4137
- <Clock className="size-3" />
4138
- )}
4139
- {t('lessonForm.awaitingConversion')}
4140
- </Badge>
4141
- )}
4142
- {currentUploadProgress !== undefined ? (
4143
- <div className="mt-1 space-y-1">
4144
- <Progress
4145
- value={currentUploadProgress}
4146
- className="h-1.5"
4147
- />
4148
- <p className="text-[0.65rem] text-muted-foreground">
4149
- {currentUploadProgress}%
4150
- </p>
4151
- </div>
4152
- ) : null}
4153
- </div>
4154
- <input
4155
- id={inputId}
4156
- type="file"
4157
- accept="video/*"
4158
- className="hidden"
4159
- onChange={(event) => {
4160
- const file = event.target.files?.[0];
4161
- if (
4162
- file &&
4163
- !isProfileVideoUploadBlocked
4164
- ) {
4165
- void handleVideoProfileFile(
4166
- profile.id,
4167
- file
4168
- );
4169
- }
4170
- event.target.value = '';
4171
- }}
4172
- />
4173
- <Button
4174
- type="button"
4175
- variant="outline"
4176
- size="sm"
4177
- className="h-7 px-2 text-xs"
4178
- disabled={
4179
- currentUploadProgress !== undefined ||
4180
- isProfileVideoUploadBlocked
4181
- }
4182
- onClick={() =>
4183
- document
4184
- .getElementById(inputId)
4185
- ?.click()
4186
- }
4187
- >
4188
- <UploadCloud className="size-3 mr-1" />
4189
- {res
4190
- ? t('lessonForm.replaceVideo')
4191
- : t('lessonForm.upload')}
4192
- </Button>
4193
- {res && (
4194
- <>
4195
- <IconActionTooltip
4196
- label={t(
4197
- 'lessonForm.playVideoAria',
4198
- { name: res.name }
4199
- )}
4200
- asWrapper={isResolvingVideoPreview}
4201
- >
4202
- <Button
4203
- type="button"
4204
- variant="ghost"
4205
- size="icon"
4206
- className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-emerald-600"
4207
- disabled={isResolvingVideoPreview}
4208
- onClick={() =>
4209
- void openVideoPreview(res)
4210
- }
4211
- aria-label={t(
4212
- 'lessonForm.playVideoAria',
4213
- { name: res.name }
4214
- )}
4215
- >
4216
- {isResolvingVideoPreview ? (
4217
- <Loader2 className="size-3 animate-spin" />
4218
- ) : (
4219
- <Play className="size-3" />
4220
- )}
4221
- </Button>
4222
- </IconActionTooltip>
4223
- <IconActionTooltip
4224
- label={t(
4225
- 'lessonForm.openVideoAria',
4226
- { name: res.name }
4227
- )}
4228
- >
4229
- <Button
4230
- type="button"
4231
- variant="ghost"
4232
- size="icon"
4233
- className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
4234
- onClick={() =>
4235
- void openResource(res)
4236
- }
4237
- aria-label={t(
4238
- 'lessonForm.openVideoAria',
4239
- { name: res.name }
4240
- )}
4241
- >
4242
- <ExternalLink className="size-3" />
4243
- </Button>
4244
- </IconActionTooltip>
4245
- <IconActionTooltip
4246
- label={t(
4247
- 'lessonForm.downloadVideoAria',
4248
- { name: res.name }
4249
- )}
4250
- asWrapper={isDownloadingResource}
4251
- >
4252
- <Button
4253
- type="button"
4254
- variant="ghost"
4255
- size="icon"
4256
- className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-amber-600"
4257
- disabled={isDownloadingResource}
4258
- onClick={() =>
4259
- void handleResourceDownload(res)
4260
- }
4261
- aria-label={t(
4262
- 'lessonForm.downloadVideoAria',
4263
- { name: res.name }
4264
- )}
4265
- >
4266
- {isDownloadingResource ? (
4267
- <Loader2 className="size-3 animate-spin" />
4268
- ) : (
4269
- <Download className="size-3" />
4270
- )}
4271
- </Button>
4272
- </IconActionTooltip>
4273
- <IconActionTooltip
4274
- label={t(
4275
- 'lessonForm.removeVideoAria',
4276
- { name: res.name }
4277
- )}
4278
- >
4279
- <Button
4280
- type="button"
4281
- variant="ghost"
4282
- size="icon"
4283
- className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-destructive"
4284
- onClick={() =>
4285
- void removeResource(res.id)
4286
- }
4287
- aria-label={t(
4288
- 'lessonForm.removeVideoAria',
4289
- { name: res.name }
4290
- )}
4291
- >
4292
- <X className="size-3" />
4293
- </Button>
4294
- </IconActionTooltip>
4295
- </>
4296
- )}
4297
- </div>
4298
- );
4299
- })}
4300
3992
  </div>
4301
- </>
4302
- )}
3993
+ );
3994
+ })()}
4303
3995
  </CardContent>
4304
3996
  </Card>
4305
3997
  </>
@@ -5117,11 +4809,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
5117
4809
  <DialogTitle className="text-base">
5118
4810
  {videoPreviewResource?.name ?? t('lessonForm.tabVideos')}
5119
4811
  </DialogTitle>
5120
- <DialogDescription>
5121
- {videoPreviewResource
5122
- ? t('lessonForm.fileStorageVideosByResolution')
5123
- : t('lessonForm.tabVideos')}
5124
- </DialogDescription>
4812
+ <DialogDescription>{t('lessonForm.tabVideos')}</DialogDescription>
5125
4813
  </DialogHeader>
5126
4814
  <div className="px-5 pb-5">
5127
4815
  {videoPreviewError ? (
@@ -5140,7 +4828,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
5140
4828
  ) : (
5141
4829
  <div className="flex min-h-72 items-center justify-center rounded-lg border border-dashed bg-muted/30 px-6 text-center text-sm text-muted-foreground">
5142
4830
  {isResolvingVideoPreview
5143
- ? t('lessonForm.loadingVideoProfiles')
4831
+ ? t('lessonForm.videoJobLoading')
5144
4832
  : t('questionEditor.resourceOpenError')}
5145
4833
  </div>
5146
4834
  )}