@hed-hog/lms 0.0.358 → 0.0.364

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 (330) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +65 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +72 -0
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
  6. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
  7. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
  8. package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
  9. package/dist/class-group/class-group.controller.d.ts +16 -16
  10. package/dist/class-group/class-group.service.d.ts +12 -12
  11. package/dist/course/course-audio-transcription.service.d.ts +3 -2
  12. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  13. package/dist/course/course-audio-transcription.service.js +49 -8
  14. package/dist/course/course-audio-transcription.service.js.map +1 -1
  15. package/dist/course/course-lesson.controller.d.ts +4 -0
  16. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  17. package/dist/course/course-lesson.controller.js +10 -0
  18. package/dist/course/course-lesson.controller.js.map +1 -1
  19. package/dist/course/course-structure.controller.d.ts +11 -4
  20. package/dist/course/course-structure.controller.d.ts.map +1 -1
  21. package/dist/course/course-structure.controller.js +14 -0
  22. package/dist/course/course-structure.controller.js.map +1 -1
  23. package/dist/course/course-structure.service.d.ts +15 -1
  24. package/dist/course/course-structure.service.d.ts.map +1 -1
  25. package/dist/course/course-structure.service.js +139 -4
  26. package/dist/course/course-structure.service.js.map +1 -1
  27. package/dist/course/course-video-conversion.service.d.ts +8 -0
  28. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  29. package/dist/course/course-video-conversion.service.js +87 -51
  30. package/dist/course/course-video-conversion.service.js.map +1 -1
  31. package/dist/course/course.controller.d.ts +73 -1
  32. package/dist/course/course.controller.d.ts.map +1 -1
  33. package/dist/course/course.controller.js +27 -3
  34. package/dist/course/course.controller.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +15 -3
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +108 -4
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +631 -30
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
  44. package/dist/course/dto/cleanup-course-storage.dto.js +33 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
  48. package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +4 -0
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
  52. package/dist/course/dto/create-course-bulk-job.dto.js +21 -0
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
  54. package/dist/course/lms-bulk-upload-automation.service.d.ts +39 -0
  55. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
  56. package/dist/course/lms-bulk-upload-automation.service.js +443 -0
  57. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
  58. package/dist/course/lms-bulk-upload-infra.service.d.ts +31 -0
  59. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
  60. package/dist/course/lms-bulk-upload-infra.service.js +277 -0
  61. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
  62. package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
  63. package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
  64. package/dist/course/lms-bulk-upload.constants.js +7 -0
  65. package/dist/course/lms-bulk-upload.constants.js.map +1 -0
  66. package/dist/course/lms-bulk-upload.controller.d.ts +153 -0
  67. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -0
  68. package/dist/course/lms-bulk-upload.controller.js +129 -0
  69. package/dist/course/lms-bulk-upload.controller.js.map +1 -0
  70. package/dist/course/lms-bulk-upload.service.d.ts +181 -0
  71. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -0
  72. package/dist/course/lms-bulk-upload.service.js +754 -0
  73. package/dist/course/lms-bulk-upload.service.js.map +1 -0
  74. package/dist/course/lms-setting.controller.d.ts +7 -1
  75. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  76. package/dist/course/lms-setting.controller.js +26 -3
  77. package/dist/course/lms-setting.controller.js.map +1 -1
  78. package/dist/enterprise/enterprise.controller.d.ts +20 -20
  79. package/dist/enterprise/enterprise.service.d.ts +20 -20
  80. package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
  81. package/dist/enterprise/training/training-admin.service.d.ts +11 -11
  82. package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
  83. package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
  84. package/dist/enterprise/training/training-student.controller.d.ts +1 -1
  85. package/dist/enterprise/training/training-student.service.d.ts +1 -1
  86. package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
  87. package/dist/evaluation/evaluation.controller.d.ts +8 -8
  88. package/dist/evaluation/evaluation.service.d.ts +8 -8
  89. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
  90. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
  91. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
  92. package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
  93. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
  94. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
  95. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
  96. package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
  97. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
  98. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
  99. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
  100. package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
  101. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
  102. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
  103. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
  104. package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
  105. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
  106. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
  107. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
  108. package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +26 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +304 -0
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
  113. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
  114. package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
  115. package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
  116. package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
  117. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
  118. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
  119. package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
  120. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
  121. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
  122. package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
  123. package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
  124. package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
  125. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
  126. package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
  127. package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
  128. package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
  129. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
  130. package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
  131. package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
  132. package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
  133. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
  134. package/dist/lms.module.d.ts.map +1 -1
  135. package/dist/lms.module.js +17 -2
  136. package/dist/lms.module.js.map +1 -1
  137. package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
  138. package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
  139. package/dist/platforma/dto/update-profile.dto.js +87 -0
  140. package/dist/platforma/dto/update-profile.dto.js.map +1 -0
  141. package/dist/platforma/platforma.controller.d.ts +94 -7
  142. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  143. package/dist/platforma/platforma.controller.js +85 -2
  144. package/dist/platforma/platforma.controller.js.map +1 -1
  145. package/dist/platforma/platforma.service.d.ts +27 -0
  146. package/dist/platforma/platforma.service.d.ts.map +1 -0
  147. package/dist/platforma/platforma.service.js +274 -0
  148. package/dist/platforma/platforma.service.js.map +1 -0
  149. package/dist/student-xp/student-xp.controller.d.ts +41 -0
  150. package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
  151. package/dist/student-xp/student-xp.controller.js +114 -0
  152. package/dist/student-xp/student-xp.controller.js.map +1 -0
  153. package/dist/student-xp/student-xp.module.d.ts +3 -0
  154. package/dist/student-xp/student-xp.module.d.ts.map +1 -0
  155. package/dist/student-xp/student-xp.module.js +25 -0
  156. package/dist/student-xp/student-xp.module.js.map +1 -0
  157. package/dist/student-xp/student-xp.service.d.ts +65 -0
  158. package/dist/student-xp/student-xp.service.d.ts.map +1 -0
  159. package/dist/student-xp/student-xp.service.js +197 -0
  160. package/dist/student-xp/student-xp.service.js.map +1 -0
  161. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
  162. package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
  163. package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
  164. package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
  165. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
  166. package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
  167. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
  168. package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
  169. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
  170. package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
  171. package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
  172. package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
  173. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
  174. package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
  175. package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
  176. package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
  177. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
  178. package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
  179. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
  180. package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
  181. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
  182. package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
  183. package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
  184. package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
  185. package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
  186. package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
  187. package/dist/xp-catalog/xp-area.controller.js +105 -0
  188. package/dist/xp-catalog/xp-area.controller.js.map +1 -0
  189. package/dist/xp-catalog/xp-area.service.d.ts +35 -0
  190. package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
  191. package/dist/xp-catalog/xp-area.service.js +168 -0
  192. package/dist/xp-catalog/xp-area.service.js.map +1 -0
  193. package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
  194. package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
  195. package/dist/xp-catalog/xp-catalog.module.js +29 -0
  196. package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
  197. package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
  198. package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
  199. package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
  200. package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
  201. package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
  202. package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
  203. package/dist/xp-catalog/xp-learning-type.service.js +146 -0
  204. package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
  205. package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
  206. package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
  207. package/dist/xp-catalog/xp-skill.controller.js +113 -0
  208. package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
  209. package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
  210. package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
  211. package/dist/xp-catalog/xp-skill.service.js +174 -0
  212. package/dist/xp-catalog/xp-skill.service.js.map +1 -0
  213. package/hedhog/data/menu.yaml +101 -0
  214. package/hedhog/data/role.yaml +8 -0
  215. package/hedhog/data/route.yaml +547 -0
  216. package/hedhog/data/setting_group.yaml +33 -0
  217. package/hedhog/data/xp_area.yaml +164 -0
  218. package/hedhog/data/xp_learning_type.yaml +131 -0
  219. package/hedhog/data/xp_skill.yaml +1834 -0
  220. package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
  221. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
  222. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1453 -0
  223. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
  224. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
  225. package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
  226. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
  227. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
  228. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +4 -4
  229. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
  230. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1170 -0
  231. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
  232. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
  233. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
  234. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
  235. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +55 -2
  236. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +442 -104
  237. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +296 -49
  238. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
  239. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
  240. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
  241. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +21 -1
  242. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
  243. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
  244. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
  245. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +54 -0
  246. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +52 -0
  247. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
  248. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
  249. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
  250. package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
  251. package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
  252. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
  253. package/hedhog/frontend/app/courses/page.tsx.ejs +400 -230
  254. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
  255. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
  256. package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
  257. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
  258. package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
  259. package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
  260. package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
  261. package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
  262. package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
  263. package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
  264. package/hedhog/frontend/messages/en.json +386 -3
  265. package/hedhog/frontend/messages/pt.json +386 -3
  266. package/hedhog/table/lesson_xp_map.yaml +50 -0
  267. package/hedhog/table/lesson_xp_segment.yaml +40 -0
  268. package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
  269. package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
  270. package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
  271. package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
  272. package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
  273. package/hedhog/table/student_area_xp.yaml +30 -0
  274. package/hedhog/table/student_learning_type_xp.yaml +30 -0
  275. package/hedhog/table/student_skill_xp.yaml +30 -0
  276. package/hedhog/table/student_xp_event.yaml +34 -0
  277. package/hedhog/table/xp_area.yaml +39 -0
  278. package/hedhog/table/xp_learning_type.yaml +34 -0
  279. package/hedhog/table/xp_skill.yaml +39 -0
  280. package/package.json +9 -8
  281. package/src/bitcode-wallet/bitcode-wallet.service.ts +113 -0
  282. package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
  283. package/src/course/course-audio-transcription.service.ts +58 -21
  284. package/src/course/course-lesson.controller.ts +6 -1
  285. package/src/course/course-structure.controller.ts +10 -0
  286. package/src/course/course-structure.service.ts +174 -1
  287. package/src/course/course-video-conversion.service.ts +113 -75
  288. package/src/course/course.controller.ts +22 -3
  289. package/src/course/course.module.ts +15 -3
  290. package/src/course/course.service.ts +847 -30
  291. package/src/course/dto/cleanup-course-storage.dto.ts +22 -0
  292. package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
  293. package/src/course/dto/create-course-bulk-job.dto.ts +6 -0
  294. package/src/course/lms-bulk-upload-automation.service.ts +560 -0
  295. package/src/course/lms-bulk-upload-infra.service.ts +327 -0
  296. package/src/course/lms-bulk-upload.constants.ts +5 -0
  297. package/src/course/lms-bulk-upload.controller.ts +103 -0
  298. package/src/course/lms-bulk-upload.service.ts +1029 -0
  299. package/src/course/lms-setting.controller.ts +28 -1
  300. package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
  301. package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
  302. package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
  303. package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
  304. package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
  305. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +396 -0
  306. package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
  307. package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
  308. package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
  309. package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
  310. package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
  311. package/src/lms.module.ts +17 -2
  312. package/src/platforma/dto/update-profile.dto.ts +59 -0
  313. package/src/platforma/platforma.controller.ts +57 -2
  314. package/src/platforma/platforma.service.ts +268 -0
  315. package/src/student-xp/student-xp.controller.ts +76 -0
  316. package/src/student-xp/student-xp.module.ts +12 -0
  317. package/src/student-xp/student-xp.service.ts +236 -0
  318. package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
  319. package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
  320. package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
  321. package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
  322. package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
  323. package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
  324. package/src/xp-catalog/xp-area.controller.ts +64 -0
  325. package/src/xp-catalog/xp-area.service.ts +196 -0
  326. package/src/xp-catalog/xp-catalog.module.ts +16 -0
  327. package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
  328. package/src/xp-catalog/xp-learning-type.service.ts +170 -0
  329. package/src/xp-catalog/xp-skill.controller.ts +71 -0
  330. package/src/xp-catalog/xp-skill.service.ts +205 -0
@@ -0,0 +1,1458 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Button } from '@/components/ui/button';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
6
+ import { Skeleton } from '@/components/ui/skeleton';
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipProvider,
11
+ TooltipTrigger,
12
+ } from '@/components/ui/tooltip';
13
+ import { cn } from '@/lib/utils';
14
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
15
+ import {
16
+ AlertCircle,
17
+ BookOpen,
18
+ Brain,
19
+ Clock3,
20
+ Cpu,
21
+ Loader2,
22
+ RefreshCw,
23
+ Zap,
24
+ } from 'lucide-react';
25
+ import { useMemo, useState, type CSSProperties, type ReactNode } from 'react';
26
+ import {
27
+ Cell,
28
+ Pie,
29
+ PieChart,
30
+ ResponsiveContainer,
31
+ Tooltip as RechartsTooltip,
32
+ } from 'recharts';
33
+
34
+ import {
35
+ useLessonXpMapQuery,
36
+ useRecalculateLessonXpMutation,
37
+ type LessonXpMapStatus,
38
+ type LessonXpSegment,
39
+ type LessonXpSegmentLearningType,
40
+ } from '../_data/use-lesson-xp-map';
41
+ import {
42
+ resolveXpAreaColor,
43
+ resolveXpLearningTypeColor,
44
+ resolveXpSkillColor,
45
+ } from '../_utils/xp-color-config';
46
+
47
+ interface LessonXpTabProps {
48
+ lessonId: string;
49
+ activeTab: string;
50
+ hasTranscription: boolean;
51
+ }
52
+
53
+ type XpAreaOption = {
54
+ id: number;
55
+ name: string;
56
+ color: string | null;
57
+ };
58
+
59
+ type XpSkillOption = {
60
+ id: number;
61
+ name: string;
62
+ xpAreaId: number | null;
63
+ };
64
+
65
+ type PaginatedResult<T> = {
66
+ data: T[];
67
+ total: number;
68
+ page: number;
69
+ pageSize: number;
70
+ lastPage?: number;
71
+ };
72
+
73
+ type DistributionItem = {
74
+ id: number;
75
+ label: string;
76
+ xp: number;
77
+ percent: number;
78
+ color: string;
79
+ };
80
+
81
+ type SegmentView = {
82
+ id: number;
83
+ label: string;
84
+ durationSeconds: number;
85
+ startSeconds: number;
86
+ endSeconds: number;
87
+ xpValue: number;
88
+ difficulty: LessonXpSegment['difficulty'];
89
+ shouldGrantXp: boolean;
90
+ aiSummary: string | null;
91
+ aiConfidence: number | null;
92
+ areaLabel: string;
93
+ areaColor: string;
94
+ skillLabel: string;
95
+ skillColor: string;
96
+ learningTypeLabel: string;
97
+ learningTypeColor: string;
98
+ areaBreakdown: Array<{
99
+ key: number;
100
+ label: string;
101
+ percent: number;
102
+ color: string;
103
+ }>;
104
+ skillBreakdown: Array<{
105
+ key: number;
106
+ label: string;
107
+ percent: number;
108
+ color: string;
109
+ }>;
110
+ learningTypeBreakdown: Array<{
111
+ key: number;
112
+ label: string;
113
+ percent: number;
114
+ color: string;
115
+ }>;
116
+ areas: LessonXpSegment['areas'];
117
+ skills: LessonXpSegment['skills'];
118
+ learningTypes: LessonXpSegment['learningTypes'];
119
+ };
120
+
121
+ const chartTooltipStyle = {
122
+ backgroundColor: '#ffffff',
123
+ border: '1px solid hsl(var(--border))',
124
+ borderRadius: '10px',
125
+ fontSize: '12px',
126
+ boxShadow: '0 18px 36px -20px rgba(15,23,42,0.42)',
127
+ };
128
+
129
+ const chartTooltipWrapperStyle = {
130
+ zIndex: 30,
131
+ };
132
+
133
+ export function LessonXpTab({
134
+ lessonId,
135
+ activeTab,
136
+ hasTranscription,
137
+ }: LessonXpTabProps) {
138
+ const { currentLocaleCode, request } = useApp();
139
+ const { data: xpMap, isLoading } = useLessonXpMapQuery(lessonId, activeTab);
140
+ const {
141
+ mutate: recalculate,
142
+ isPending: isRecalculating,
143
+ error: recalcError,
144
+ } = useRecalculateLessonXpMutation(lessonId);
145
+ const [localError, setLocalError] = useState<string | null>(null);
146
+
147
+ const { data: areaOptions = [] } = useQuery<XpAreaOption[]>({
148
+ queryKey: ['lms-xp-areas-all'],
149
+ enabled: activeTab === 'xp',
150
+ queryFn: async () => {
151
+ const response = await request<XpAreaOption[]>({
152
+ url: '/lms/xp/areas/all',
153
+ method: 'GET',
154
+ });
155
+ return response.data;
156
+ },
157
+ placeholderData: [],
158
+ });
159
+
160
+ const {
161
+ data: skillPage = {
162
+ data: [],
163
+ total: 0,
164
+ page: 1,
165
+ pageSize: 1000,
166
+ lastPage: 1,
167
+ },
168
+ } = useQuery<PaginatedResult<XpSkillOption>>({
169
+ queryKey: ['lms-xp-skills-all'],
170
+ enabled: activeTab === 'xp',
171
+ queryFn: async () => {
172
+ const response = await request<PaginatedResult<XpSkillOption>>({
173
+ url: '/lms/xp/skills?page=1&pageSize=1000',
174
+ method: 'GET',
175
+ });
176
+ return response.data;
177
+ },
178
+ placeholderData: {
179
+ data: [],
180
+ total: 0,
181
+ page: 1,
182
+ pageSize: 1000,
183
+ lastPage: 1,
184
+ },
185
+ });
186
+
187
+ const handleRecalculate = () => {
188
+ setLocalError(null);
189
+ recalculate({
190
+ onError: (msg) => setLocalError(msg),
191
+ });
192
+ };
193
+
194
+ const displayError = localError ?? recalcError;
195
+ const isProcessing = xpMap?.status === 'processing';
196
+
197
+ const areaLookup = useMemo(
198
+ () => new Map(areaOptions.map((area) => [area.id, area])),
199
+ [areaOptions]
200
+ );
201
+ const skillLookup = useMemo(
202
+ () => new Map(skillPage.data.map((skill) => [skill.id, skill])),
203
+ [skillPage.data]
204
+ );
205
+
206
+ const areaDistribution = useMemo(() => {
207
+ if (!xpMap) return [];
208
+ return buildDistributionItems(
209
+ aggregateAreas(xpMap.segments),
210
+ xpMap.totalXp,
211
+ (id) => areaLookup.get(id)?.name ?? `Área ${id}`,
212
+ (id, index) =>
213
+ resolveXpAreaColor({
214
+ areaId: id,
215
+ index,
216
+ persistedColor: areaLookup.get(id)?.color,
217
+ })
218
+ );
219
+ }, [areaLookup, xpMap]);
220
+
221
+ const skillDistribution = useMemo(() => {
222
+ if (!xpMap) return [];
223
+ return buildDistributionItems(
224
+ aggregateSkills(xpMap.segments),
225
+ xpMap.totalXp,
226
+ (id) => skillLookup.get(id)?.name ?? `Skill ${id}`,
227
+ (id, index) => resolveXpSkillColor({ skillId: id, index })
228
+ );
229
+ }, [skillLookup, xpMap]);
230
+
231
+ const learningTypeDistribution = useMemo(() => {
232
+ if (!xpMap) return [];
233
+ const totals = aggregateLearningTypes(xpMap.segments);
234
+ return [...totals.entries()].map(([id, item], index) => ({
235
+ id,
236
+ label: item.learningType.name,
237
+ xp: item.xp,
238
+ percent:
239
+ xpMap.totalXp > 0 ? Math.round((item.xp / xpMap.totalXp) * 100) : 0,
240
+ color: resolveXpLearningTypeColor({ learningTypeId: id, index }),
241
+ }));
242
+ }, [xpMap]);
243
+
244
+ const segmentViews = useMemo<SegmentView[]>(() => {
245
+ if (!xpMap) return [];
246
+
247
+ return xpMap.segments.map((segment, index) => {
248
+ const primaryArea = pickDominantItem(segment.areas);
249
+ const primarySkill = pickDominantItem(segment.skills);
250
+ const primaryLearningType = pickDominantItem(segment.learningTypes);
251
+
252
+ const areaId = primaryArea?.xpAreaId ?? null;
253
+ const skillId = primarySkill?.xpSkillId ?? null;
254
+ const learningTypeId = primaryLearningType?.xpLearningTypeId ?? null;
255
+ const areaBreakdown = segment.areas.map((area, areaIndex) => ({
256
+ key: area.xpAreaId,
257
+ label: areaLookup.get(area.xpAreaId)?.name ?? `Área ${area.xpAreaId}`,
258
+ percent: area.weightPercent,
259
+ color: resolveXpAreaColor({
260
+ areaId: area.xpAreaId,
261
+ index: areaIndex,
262
+ persistedColor: areaLookup.get(area.xpAreaId)?.color,
263
+ }),
264
+ }));
265
+ const skillBreakdown = segment.skills.map((skill, skillIndex) => ({
266
+ key: skill.xpSkillId,
267
+ label:
268
+ skillLookup.get(skill.xpSkillId)?.name ?? `Skill ${skill.xpSkillId}`,
269
+ percent: skill.weightPercent,
270
+ color: resolveXpSkillColor({
271
+ skillId: skill.xpSkillId,
272
+ index: skillIndex,
273
+ }),
274
+ }));
275
+ const learningTypeBreakdown = segment.learningTypes.map(
276
+ (learningType, learningTypeIndex) => ({
277
+ key: learningType.xpLearningTypeId,
278
+ label: learningType.name ?? `Tipo ${learningType.xpLearningTypeId}`,
279
+ percent: learningType.weightPercent,
280
+ color: resolveXpLearningTypeColor({
281
+ learningTypeId: learningType.xpLearningTypeId,
282
+ learningTypeSlug: learningType.slug,
283
+ index: learningTypeIndex,
284
+ }),
285
+ })
286
+ );
287
+
288
+ return {
289
+ id: segment.id,
290
+ label: `Segmento ${index + 1}`,
291
+ durationSeconds: Math.max(segment.endSeconds - segment.startSeconds, 1),
292
+ startSeconds: segment.startSeconds,
293
+ endSeconds: segment.endSeconds,
294
+ xpValue: segment.xpValue,
295
+ difficulty: segment.difficulty,
296
+ shouldGrantXp: segment.shouldGrantXp,
297
+ aiSummary: segment.aiSummary,
298
+ aiConfidence: segment.aiConfidence,
299
+ areaLabel:
300
+ areaId !== null
301
+ ? (areaLookup.get(areaId)?.name ?? `Área ${areaId}`)
302
+ : 'Sem área',
303
+ areaColor:
304
+ areaId !== null
305
+ ? resolveXpAreaColor({
306
+ areaId,
307
+ index,
308
+ persistedColor: areaLookup.get(areaId)?.color,
309
+ })
310
+ : '#64748b',
311
+ skillLabel:
312
+ skillId !== null
313
+ ? (skillLookup.get(skillId)?.name ?? `Skill ${skillId}`)
314
+ : 'Sem skill',
315
+ skillColor:
316
+ skillId !== null
317
+ ? resolveXpSkillColor({ skillId, index })
318
+ : '#64748b',
319
+ learningTypeLabel:
320
+ learningTypeId !== null
321
+ ? (primaryLearningType?.name ?? `Tipo ${learningTypeId}`)
322
+ : 'Sem tipo',
323
+ learningTypeColor:
324
+ learningTypeId !== null
325
+ ? resolveXpLearningTypeColor({ learningTypeId, index })
326
+ : '#64748b',
327
+ areaBreakdown,
328
+ skillBreakdown,
329
+ learningTypeBreakdown,
330
+ areas: segment.areas,
331
+ skills: segment.skills,
332
+ learningTypes: segment.learningTypes,
333
+ };
334
+ });
335
+ }, [areaLookup, skillLookup, xpMap]);
336
+
337
+ if (isLoading) {
338
+ return <LessonXpSkeleton />;
339
+ }
340
+
341
+ if (!hasTranscription && !xpMap) {
342
+ return (
343
+ <div className="flex flex-col items-center gap-3 px-4 py-12 text-center">
344
+ <BookOpen className="size-8 text-muted-foreground/40" />
345
+ <p className="text-sm text-muted-foreground">
346
+ Esta aula ainda não possui transcrição. Gere ou importe a transcrição
347
+ para calcular o XP com IA.
348
+ </p>
349
+ </div>
350
+ );
351
+ }
352
+
353
+ return (
354
+ <div className="flex flex-col gap-2.5">
355
+ <div className="rounded-2xl border border-border/70 bg-linear-to-br from-background via-background to-muted/30 px-3 py-2.5 shadow-[0_14px_34px_-28px_rgba(15,23,42,0.45)] backdrop-blur-sm sm:px-4">
356
+ <div className="flex flex-wrap items-start justify-between gap-3">
357
+ <div className="flex min-w-0 flex-col gap-2">
358
+ <div className="flex min-w-0 items-center gap-1.5">
359
+ {xpMap ? (
360
+ <>
361
+ <Zap className="size-3.5 shrink-0 text-yellow-500" />
362
+ <span className="text-sm font-semibold">
363
+ {xpMap.totalXp} XP
364
+ </span>
365
+ <XpStatusBadge status={xpMap.status} />
366
+ </>
367
+ ) : (
368
+ <span className="text-xs text-muted-foreground">
369
+ Sem mapa de XP
370
+ </span>
371
+ )}
372
+ </div>
373
+ {xpMap && (
374
+ <div className="flex flex-wrap items-center gap-1.5">
375
+ <MetricPill
376
+ label="Segmentos"
377
+ value={String(xpMap.segments.length)}
378
+ />
379
+ <MetricPill
380
+ label="Áreas"
381
+ value={String(countUniqueAreas(xpMap.segments))}
382
+ tone="sky"
383
+ />
384
+ <MetricPill
385
+ label="Skills"
386
+ value={String(countUniqueSkills(xpMap.segments))}
387
+ tone="emerald"
388
+ />
389
+ {avgConfidence(xpMap.segments) !== null && (
390
+ <MetricPill
391
+ label="Confiança"
392
+ value={`${Math.round(avgConfidence(xpMap.segments)! * 100)}%`}
393
+ tone="violet"
394
+ />
395
+ )}
396
+ </div>
397
+ )}
398
+ </div>
399
+ <div className="flex shrink-0 flex-col items-end gap-0.5">
400
+ <Button
401
+ size="sm"
402
+ variant="outline"
403
+ onClick={handleRecalculate}
404
+ disabled={isRecalculating || isProcessing}
405
+ className="h-8 rounded-xl border-border/70 bg-background/80 gap-1.5 px-3 text-xs shadow-sm"
406
+ >
407
+ {isRecalculating || isProcessing ? (
408
+ <Loader2 className="size-3 animate-spin" />
409
+ ) : (
410
+ <RefreshCw className="size-3" />
411
+ )}
412
+ Recalcular XP
413
+ </Button>
414
+ {displayError && (
415
+ <p className="max-w-40 text-right text-[0.65rem] leading-tight text-destructive">
416
+ {displayError}
417
+ </p>
418
+ )}
419
+ </div>
420
+ </div>
421
+ </div>
422
+
423
+ {isProcessing && (
424
+ <div className="flex items-center gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 px-2.5 py-1.5 text-xs text-yellow-700 dark:text-yellow-400">
425
+ <Loader2 className="size-3.5 shrink-0 animate-spin" />
426
+ Calculando XP com IA...
427
+ </div>
428
+ )}
429
+
430
+ {xpMap && (
431
+ <div className="grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
432
+ <DistributionCard
433
+ title="Áreas Macro"
434
+ description="Distribuição do XP por área macro na aula"
435
+ icon={<Cpu className="size-4 text-sky-600" />}
436
+ total={xpMap.totalXp}
437
+ items={areaDistribution}
438
+ toneClassName="bg-linear-to-r from-sky-500/10 via-sky-500/5 to-transparent"
439
+ />
440
+ <DistributionCard
441
+ title="Skills"
442
+ description="Distribuição do XP por skill principal"
443
+ icon={<Zap className="size-4 text-emerald-600" />}
444
+ total={xpMap.totalXp}
445
+ items={skillDistribution}
446
+ toneClassName="bg-linear-to-r from-emerald-500/10 via-emerald-500/5 to-transparent"
447
+ />
448
+ <TimelineCard
449
+ segmentViews={segmentViews}
450
+ totalDuration={Math.max(
451
+ ...segmentViews.map((segment) => segment.endSeconds),
452
+ 0
453
+ )}
454
+ />
455
+ <LearningTypesCard items={learningTypeDistribution} />
456
+ </div>
457
+ )}
458
+
459
+ {xpMap && (
460
+ <Card className="border-border/70">
461
+ <CardHeader className="pb-1.5">
462
+ <CardTitle className="text-sm font-semibold">
463
+ Resumo do mapa
464
+ </CardTitle>
465
+ </CardHeader>
466
+ <CardContent className="grid grid-cols-2 gap-x-3 gap-y-1.5 pt-0 text-xs sm:grid-cols-4">
467
+ <SummaryItem
468
+ label="Áreas"
469
+ value={String(countUniqueAreas(xpMap.segments))}
470
+ />
471
+ <SummaryItem
472
+ label="Skills"
473
+ value={String(countUniqueSkills(xpMap.segments))}
474
+ />
475
+ <SummaryItem
476
+ label="Tipos"
477
+ value={String(countUniqueLearningTypes(xpMap.segments))}
478
+ />
479
+ <SummaryItem
480
+ label="Segmentos"
481
+ value={String(xpMap.segments.length)}
482
+ />
483
+ {xpMap.generatedAt && (
484
+ <SummaryItem
485
+ label="Gerado"
486
+ value={formatDate(xpMap.generatedAt)}
487
+ />
488
+ )}
489
+ {xpMap.aiModelVersion && (
490
+ <SummaryItem label="Modelo" value={xpMap.aiModelVersion} />
491
+ )}
492
+ {avgConfidence(xpMap.segments) !== null && (
493
+ <SummaryItem
494
+ label="Confiança"
495
+ value={`${Math.round(avgConfidence(xpMap.segments)! * 100)}%`}
496
+ />
497
+ )}
498
+ </CardContent>
499
+ </Card>
500
+ )}
501
+
502
+ {!xpMap && !isProcessing && (
503
+ <div className="flex flex-col items-center gap-2 py-6 text-center">
504
+ <Brain className="size-7 text-muted-foreground/40" />
505
+ <p className="text-xs text-muted-foreground">
506
+ Nenhum mapa de XP calculado.
507
+ </p>
508
+ </div>
509
+ )}
510
+
511
+ {xpMap?.processingError && (
512
+ <div className="flex items-start gap-1.5 rounded-md bg-destructive/10 px-2.5 py-1.5 text-xs text-destructive">
513
+ <AlertCircle className="mt-0.5 size-3.5 shrink-0" />
514
+ <span>{xpMap.processingError}</span>
515
+ </div>
516
+ )}
517
+
518
+ {xpMap && xpMap.segments.length > 0 && !isProcessing && (
519
+ <>
520
+ <SegmentsSection
521
+ segments={segmentViews}
522
+ areaLookup={areaLookup}
523
+ skillLookup={skillLookup}
524
+ locale={currentLocaleCode || 'pt-BR'}
525
+ />
526
+ </>
527
+ )}
528
+ </div>
529
+ );
530
+ }
531
+
532
+ function LessonXpSkeleton() {
533
+ return (
534
+ <div className="flex flex-col gap-3 p-1">
535
+ <div className="flex items-center justify-between gap-2">
536
+ <Skeleton className="h-5 w-24 rounded-full" />
537
+ <Skeleton className="h-7 w-28 rounded-md" />
538
+ </div>
539
+ <div className="grid gap-3 xl:grid-cols-[1fr_1fr]">
540
+ <Skeleton className="h-72 rounded-xl" />
541
+ <Skeleton className="h-72 rounded-xl" />
542
+ <Skeleton className="h-28 rounded-xl xl:col-span-2" />
543
+ </div>
544
+ {Array.from({ length: 3 }).map((_, index) => (
545
+ <div
546
+ key={index}
547
+ className="flex flex-col gap-2 rounded-lg border bg-muted/20 p-3"
548
+ >
549
+ <div className="flex items-center justify-between gap-2">
550
+ <Skeleton className="h-3 w-32" />
551
+ <Skeleton className="h-4 w-14 rounded-full" />
552
+ </div>
553
+ <Skeleton className="h-3 w-3/4" />
554
+ <div className="flex items-center gap-2">
555
+ <Skeleton className="h-3 w-16" />
556
+ <Skeleton className="h-3 w-20" />
557
+ <Skeleton className="h-3 w-12" />
558
+ </div>
559
+ </div>
560
+ ))}
561
+ </div>
562
+ );
563
+ }
564
+
565
+ function DistributionCard({
566
+ title,
567
+ description,
568
+ icon,
569
+ total,
570
+ items,
571
+ toneClassName,
572
+ }: {
573
+ title: string;
574
+ description: string;
575
+ icon: ReactNode;
576
+ total: number;
577
+ items: DistributionItem[];
578
+ toneClassName: string;
579
+ }) {
580
+ if (!items.length) return null;
581
+
582
+ return (
583
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
584
+ <CardHeader
585
+ className={cn(
586
+ 'space-y-0.5 border-b border-border/60 pt-3 pb-1.5!',
587
+ toneClassName
588
+ )}
589
+ >
590
+ <div className="flex items-center justify-between gap-3">
591
+ <div className="min-w-0">
592
+ <CardTitle className="text-sm font-semibold">{title}</CardTitle>
593
+ <p className="text-xs text-muted-foreground">{description}</p>
594
+ </div>
595
+ <div className="flex items-center gap-2 rounded-full border border-white/40 bg-background/85 px-2 py-1 shadow-sm backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-background hover:shadow-md dark:border-white/10 dark:bg-background/70">
596
+ {icon}
597
+ <span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
598
+ {items.length} itens
599
+ </span>
600
+ </div>
601
+ </div>
602
+ </CardHeader>
603
+ <CardContent className="flex flex-col gap-2.5 px-2.5 py-2.5 sm:px-3">
604
+ <div className="relative h-44">
605
+ <ResponsiveContainer width="100%" height="100%">
606
+ <PieChart>
607
+ <Pie
608
+ data={items}
609
+ cx="50%"
610
+ cy="50%"
611
+ innerRadius={46}
612
+ outerRadius={68}
613
+ paddingAngle={2.5}
614
+ dataKey="xp"
615
+ strokeWidth={3}
616
+ stroke="hsl(var(--background))"
617
+ >
618
+ {items.map((entry) => (
619
+ <Cell key={entry.id} fill={entry.color} />
620
+ ))}
621
+ </Pie>
622
+ <RechartsTooltip
623
+ wrapperStyle={chartTooltipWrapperStyle}
624
+ content={<DistributionTooltip />}
625
+ />
626
+ </PieChart>
627
+ </ResponsiveContainer>
628
+ <div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
629
+ <span className="text-xl font-bold text-foreground">{total}</span>
630
+ <span className="text-[10px] text-muted-foreground">XP total</span>
631
+ </div>
632
+ </div>
633
+
634
+ <div className="grid max-h-40 grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-1.5 overflow-auto pr-1">
635
+ {items.map((item) => (
636
+ <div
637
+ key={item.id}
638
+ className="flex min-w-0 items-start gap-2 rounded-xl border border-border/50 bg-muted/15 px-2 py-1.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-border hover:bg-background/90 hover:shadow-md"
639
+ >
640
+ <span
641
+ className="mt-1 size-2 shrink-0 rounded-sm"
642
+ style={{ backgroundColor: item.color }}
643
+ />
644
+ <div className="flex min-w-0 flex-col">
645
+ <span className="wrap-break-word text-xs font-medium leading-tight text-foreground">
646
+ {item.label}
647
+ </span>
648
+ <span className="text-[10px] text-muted-foreground">
649
+ {item.xp} XP ({item.percent}%)
650
+ </span>
651
+ </div>
652
+ </div>
653
+ ))}
654
+ </div>
655
+ </CardContent>
656
+ </Card>
657
+ );
658
+ }
659
+
660
+ function TimelineCard({
661
+ segmentViews,
662
+ totalDuration,
663
+ }: {
664
+ segmentViews: SegmentView[];
665
+ totalDuration: number;
666
+ }) {
667
+ if (!segmentViews.length) return null;
668
+
669
+ const safeDuration = Math.max(totalDuration, 1);
670
+
671
+ return (
672
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)] xl:col-span-2">
673
+ <CardHeader className="border-b border-border/60 bg-linear-to-r from-slate-500/10 via-slate-500/5 to-transparent pt-3 pb-1.5!">
674
+ <div className="flex items-center justify-between gap-3">
675
+ <div>
676
+ <CardTitle className="text-sm font-semibold">
677
+ Timeline da aula
678
+ </CardTitle>
679
+ <p className="text-xs text-muted-foreground">
680
+ Segmentos proporcionais ao tempo total, com pesos visuais por
681
+ área, skill e tipo
682
+ </p>
683
+ </div>
684
+ <div className="flex items-center gap-2 rounded-full border border-white/40 bg-background/85 px-2 py-1 shadow-sm backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-background hover:shadow-md dark:border-white/10 dark:bg-background/70">
685
+ <Clock3 className="size-4 text-muted-foreground" />
686
+ <span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
687
+ {segmentViews.length} blocos
688
+ </span>
689
+ </div>
690
+ </div>
691
+ </CardHeader>
692
+ <CardContent className="px-2.5 py-2.5 sm:px-3">
693
+ <div className="rounded-xl border border-border/60 bg-linear-to-br from-muted/35 via-muted/20 to-background p-2.5">
694
+ <TooltipProvider delayDuration={120}>
695
+ <div className="flex h-12 items-stretch gap-1 overflow-x-auto rounded-lg">
696
+ {segmentViews.map((segment) => {
697
+ const width = Math.max(
698
+ 4,
699
+ (segment.durationSeconds / safeDuration) * 100
700
+ );
701
+ return (
702
+ <Tooltip key={segment.id}>
703
+ <TooltipTrigger asChild>
704
+ <button
705
+ type="button"
706
+ className={cn(
707
+ 'group relative flex min-w-0 flex-col overflow-hidden rounded-md border border-border/60 bg-background/80 text-left outline-none transition-all duration-200 hover:-translate-y-0.5 hover:border-border hover:bg-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
708
+ !segment.shouldGrantXp && 'opacity-50'
709
+ )}
710
+ style={{ flexBasis: `${width}%`, flexGrow: 0 }}
711
+ aria-label={`${segment.label} ${formatSeconds(segment.startSeconds)} a ${formatSeconds(segment.endSeconds)} ${segment.xpValue} XP`}
712
+ >
713
+ <SegmentCompositionRow
714
+ items={segment.areaBreakdown}
715
+ getKey={(item) => item.key}
716
+ getValue={(item) => item.percent}
717
+ getColor={(item) => item.color}
718
+ />
719
+ <SegmentCompositionRow
720
+ items={segment.skillBreakdown}
721
+ getKey={(item) => item.key}
722
+ getValue={(item) => item.percent}
723
+ getColor={(item) => item.color}
724
+ />
725
+ <SegmentCompositionRow
726
+ items={segment.learningTypeBreakdown}
727
+ getKey={(item) => item.key}
728
+ getValue={(item) => item.percent}
729
+ getColor={(item) => item.color}
730
+ />
731
+ <div className="pointer-events-none absolute inset-x-1 bottom-0.5 flex items-center justify-between gap-1 rounded bg-black/45 px-1 py-0.5 text-[9px] font-medium text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
732
+ <span>{formatSeconds(segment.startSeconds)}</span>
733
+ <span>{segment.xpValue} XP</span>
734
+ </div>
735
+ </button>
736
+ </TooltipTrigger>
737
+ <TooltipContent
738
+ side="top"
739
+ hideArrow
740
+ className="max-w-72 rounded-lg border border-border bg-popover px-3 py-2 text-popover-foreground shadow-lg"
741
+ >
742
+ <SegmentTooltipContent segment={segment} />
743
+ </TooltipContent>
744
+ </Tooltip>
745
+ );
746
+ })}
747
+ </div>
748
+ </TooltipProvider>
749
+ <div className="mt-2 flex items-center justify-between text-[10px] text-muted-foreground">
750
+ <span>{formatSeconds(0)}</span>
751
+ <span>{formatSeconds(safeDuration)}</span>
752
+ </div>
753
+ </div>
754
+ </CardContent>
755
+ </Card>
756
+ );
757
+ }
758
+
759
+ function LearningTypesCard({ items }: { items: DistributionItem[] }) {
760
+ if (!items.length) return null;
761
+
762
+ return (
763
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)] xl:col-span-2">
764
+ <CardHeader className="border-b border-border/60 bg-linear-to-r from-violet-500/10 via-violet-500/5 to-transparent pt-3 pb-1.5!">
765
+ <div className="flex items-center justify-between gap-3">
766
+ <CardTitle className="text-sm font-semibold">
767
+ Tipos de aprendizado
768
+ </CardTitle>
769
+ <div className="rounded-full border border-white/40 bg-background/85 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground shadow-sm backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-background hover:shadow-md dark:border-white/10 dark:bg-background/70">
770
+ {items.length} tipos
771
+ </div>
772
+ </div>
773
+ </CardHeader>
774
+ <CardContent className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] content-start gap-2 px-2.5 py-2.5 sm:px-3">
775
+ {items.map((item) => (
776
+ <div
777
+ key={item.id}
778
+ className="flex h-full flex-col justify-start gap-1.5 rounded-xl border border-border/60 bg-muted/20 px-2.5 py-2 text-xs shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-violet-500/20 hover:bg-background/95 hover:shadow-md"
779
+ >
780
+ <div className="flex flex-wrap items-center justify-between gap-2">
781
+ <span className="min-w-0 flex-1 font-medium">{item.label}</span>
782
+ <span className="font-semibold tabular-nums">{item.xp} XP</span>
783
+ </div>
784
+ <div className="h-1.5 overflow-hidden rounded-full bg-muted">
785
+ <div
786
+ className="h-full rounded-full"
787
+ style={{
788
+ width: `${item.percent}%`,
789
+ backgroundColor: item.color,
790
+ }}
791
+ />
792
+ </div>
793
+ <div className="text-[10px] text-muted-foreground">
794
+ {item.percent}%
795
+ </div>
796
+ </div>
797
+ ))}
798
+ </CardContent>
799
+ </Card>
800
+ );
801
+ }
802
+
803
+ function SegmentsSection({
804
+ segments,
805
+ areaLookup,
806
+ skillLookup,
807
+ locale,
808
+ }: {
809
+ segments: SegmentView[];
810
+ areaLookup: Map<number, XpAreaOption>;
811
+ skillLookup: Map<number, XpSkillOption>;
812
+ locale: string;
813
+ }) {
814
+ return (
815
+ <Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
816
+ <CardHeader className="border-b border-border/60 bg-linear-to-r from-cyan-500/10 via-cyan-500/5 to-transparent pt-3 pb-1.5!">
817
+ <div className="flex items-center justify-between gap-3">
818
+ <CardTitle className="text-sm font-semibold">
819
+ Segmentos ({segments.length})
820
+ </CardTitle>
821
+ <div className="rounded-full border border-white/40 bg-background/85 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground shadow-sm backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-background hover:shadow-md dark:border-white/10 dark:bg-background/70">
822
+ leitura IA
823
+ </div>
824
+ </div>
825
+ </CardHeader>
826
+ <CardContent className="flex flex-col gap-2.5 px-2.5 py-2.5 sm:px-3">
827
+ <TooltipProvider delayDuration={120}>
828
+ {segments.map((segment) => (
829
+ <div
830
+ key={segment.id}
831
+ className={cn(
832
+ 'flex flex-col gap-2 rounded-2xl border border-border/60 bg-linear-to-br from-background via-background to-muted/25 px-3 py-2.5 text-xs shadow-[0_16px_30px_-24px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:border-cyan-500/15 hover:to-muted/35 hover:shadow-[0_20px_36px_-24px_rgba(15,23,42,0.52)]',
833
+ !segment.shouldGrantXp && 'opacity-50'
834
+ )}
835
+ >
836
+ <div className="flex flex-wrap items-start justify-between gap-2">
837
+ <div className="flex min-w-0 flex-col gap-1">
838
+ <div className="flex flex-wrap items-center gap-1.5">
839
+ <span className="rounded-full border border-cyan-500/15 bg-cyan-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-cyan-700 transition-all duration-200 hover:bg-cyan-500/15 hover:shadow-sm dark:text-cyan-300">
840
+ {segment.label}
841
+ </span>
842
+ <span className="rounded-full border border-border/60 bg-background/90 px-2 py-0.5 font-mono text-[10px] text-muted-foreground shadow-sm transition-all duration-200 hover:border-border hover:bg-background hover:text-foreground hover:shadow-md">
843
+ {formatSeconds(segment.startSeconds)} -{' '}
844
+ {formatSeconds(segment.endSeconds)}
845
+ </span>
846
+ </div>
847
+ <div className="flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground">
848
+ <span className="font-medium text-foreground">
849
+ {segment.areaLabel}
850
+ </span>
851
+ <span className="text-muted-foreground/60">/</span>
852
+ <span>{segment.skillLabel}</span>
853
+ <span className="text-muted-foreground/60">/</span>
854
+ <span>{segment.learningTypeLabel}</span>
855
+ </div>
856
+ </div>
857
+
858
+ <div className="flex flex-wrap items-center justify-end gap-1.5">
859
+ <span className="rounded-full border border-amber-500/15 bg-amber-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-amber-700 dark:text-amber-300">
860
+ {segment.xpValue} XP
861
+ </span>
862
+ <DifficultyBadge difficulty={segment.difficulty} />
863
+ {segment.aiConfidence !== null && (
864
+ <span className="rounded-full border border-border/60 bg-background/90 px-2 py-0.5 text-[10px] font-semibold text-muted-foreground shadow-sm">
865
+ {Math.round(segment.aiConfidence * 100)}% conf.
866
+ </span>
867
+ )}
868
+ </div>
869
+ </div>
870
+
871
+ <div className="overflow-hidden rounded-full border border-border/50 bg-muted/40">
872
+ <div className="flex h-1.5 w-full overflow-hidden">
873
+ {buildSegmentComposition(
874
+ segment.areaBreakdown,
875
+ (item) => item.percent
876
+ ).map(({ item, percent }, index) => (
877
+ <Tooltip key={`area-${item.key}-${index}`}>
878
+ <TooltipTrigger asChild>
879
+ <div
880
+ className="h-full cursor-help transition-opacity hover:opacity-85"
881
+ style={{
882
+ width: `${percent}%`,
883
+ backgroundColor: item.color,
884
+ }}
885
+ />
886
+ </TooltipTrigger>
887
+ <TooltipContent
888
+ side="top"
889
+ hideArrow
890
+ className="rounded-lg border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-lg"
891
+ >
892
+ <div className="flex items-center gap-1.5">
893
+ <span
894
+ className="size-2 rounded-full"
895
+ style={{ backgroundColor: item.color }}
896
+ />
897
+ <span className="font-medium text-foreground">
898
+ {item.label}
899
+ </span>
900
+ <span className="text-muted-foreground">
901
+ {Math.round(percent)}%
902
+ </span>
903
+ </div>
904
+ </TooltipContent>
905
+ </Tooltip>
906
+ ))}
907
+ </div>
908
+ </div>
909
+
910
+ {segment.aiSummary && (
911
+ <div className="rounded-xl border border-border/50 bg-muted/20 px-2.5 py-2">
912
+ <p className="line-clamp-2 leading-relaxed text-muted-foreground">
913
+ {segment.aiSummary}
914
+ </p>
915
+ </div>
916
+ )}
917
+ <div className="grid gap-1.5 md:grid-cols-2 xl:grid-cols-3">
918
+ <SegmentBreakdownGroup
919
+ title="Áreas"
920
+ tone="sky"
921
+ items={segment.areaBreakdown.map((area) => ({
922
+ key: area.key,
923
+ label:
924
+ areaLookup.get(area.key)?.name ??
925
+ area.label ??
926
+ `Área ${area.key}`,
927
+ percent: area.percent,
928
+ color: area.color,
929
+ }))}
930
+ />
931
+ <SegmentBreakdownGroup
932
+ title="Skills"
933
+ tone="emerald"
934
+ items={segment.skillBreakdown.map((skill) => ({
935
+ key: skill.key,
936
+ label:
937
+ skillLookup.get(skill.key)?.name ??
938
+ skill.label ??
939
+ `Skill ${skill.key}`,
940
+ percent: skill.percent,
941
+ color: skill.color,
942
+ }))}
943
+ />
944
+ <SegmentBreakdownGroup
945
+ title="Tipos"
946
+ tone="violet"
947
+ items={segment.learningTypeBreakdown.map((learningType) => {
948
+ const metadata = segment.learningTypes.find(
949
+ (item) => item.xpLearningTypeId === learningType.key
950
+ );
951
+
952
+ return {
953
+ key: learningType.key,
954
+ label:
955
+ learningType.label +
956
+ (metadata?.multiplier !== undefined
957
+ ? ` ${formatMultiplier(metadata.multiplier, locale)}`
958
+ : ''),
959
+ percent: learningType.percent,
960
+ color: learningType.color,
961
+ };
962
+ })}
963
+ />
964
+ </div>
965
+ </div>
966
+ ))}
967
+ </TooltipProvider>
968
+ </CardContent>
969
+ </Card>
970
+ );
971
+ }
972
+
973
+ function DistributionTooltip({
974
+ active,
975
+ payload,
976
+ }: {
977
+ active?: boolean;
978
+ payload?: Array<{
979
+ name?: string;
980
+ value?: number;
981
+ payload?: { color?: string; label?: string; percent?: number };
982
+ }>;
983
+ }) {
984
+ if (!active || !payload?.length) return null;
985
+
986
+ const entry = payload[0];
987
+ const label = entry?.payload?.label ?? entry?.name ?? 'Item';
988
+ const percent = entry?.payload?.percent;
989
+
990
+ return (
991
+ <div
992
+ className="rounded-xl border border-border bg-white px-3 py-2 text-slate-900 shadow-xl"
993
+ style={chartTooltipStyle as CSSProperties}
994
+ >
995
+ <p className="flex items-center gap-1.5 text-xs text-slate-500">
996
+ <span
997
+ className="inline-block size-2 rounded-full"
998
+ style={{ backgroundColor: entry?.payload?.color }}
999
+ />
1000
+ {label}
1001
+ </p>
1002
+ <p className="mt-0.5 text-sm font-semibold text-slate-900">
1003
+ {entry?.value} XP
1004
+ </p>
1005
+ {typeof percent === 'number' && (
1006
+ <p className="mt-0.5 text-[11px] text-slate-500">{percent}% do total</p>
1007
+ )}
1008
+ </div>
1009
+ );
1010
+ }
1011
+
1012
+ function SummaryItem({ label, value }: { label: string; value: string }) {
1013
+ return (
1014
+ <div className="rounded-xl border border-border/50 bg-muted/15 px-2 py-1.5">
1015
+ <div className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
1016
+ {label}
1017
+ </div>
1018
+ <div className="mt-0.5 text-sm font-semibold text-foreground">
1019
+ {value}
1020
+ </div>
1021
+ </div>
1022
+ );
1023
+ }
1024
+
1025
+ function MetricPill({
1026
+ label,
1027
+ value,
1028
+ tone = 'default',
1029
+ }: {
1030
+ label: string;
1031
+ value: string;
1032
+ tone?: 'default' | 'sky' | 'emerald' | 'violet';
1033
+ }) {
1034
+ const toneClassName =
1035
+ tone === 'sky'
1036
+ ? 'border-sky-500/15 bg-sky-500/10 text-sky-700 dark:text-sky-300'
1037
+ : tone === 'emerald'
1038
+ ? 'border-emerald-500/15 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
1039
+ : tone === 'violet'
1040
+ ? 'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300'
1041
+ : 'border-border/60 bg-muted/40 text-foreground';
1042
+
1043
+ return (
1044
+ <div
1045
+ className={cn(
1046
+ 'inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs shadow-sm',
1047
+ toneClassName
1048
+ )}
1049
+ >
1050
+ <span className="text-[10px] font-semibold uppercase tracking-[0.14em] opacity-80">
1051
+ {label}
1052
+ </span>
1053
+ <span className="font-semibold">{value}</span>
1054
+ </div>
1055
+ );
1056
+ }
1057
+
1058
+ function SegmentBreakdownGroup({
1059
+ title,
1060
+ tone,
1061
+ items,
1062
+ }: {
1063
+ title: string;
1064
+ tone: 'sky' | 'emerald' | 'violet';
1065
+ items: Array<{
1066
+ key: number;
1067
+ label: string;
1068
+ percent: number;
1069
+ color: string;
1070
+ }>;
1071
+ }) {
1072
+ const toneClassName =
1073
+ tone === 'sky'
1074
+ ? 'border-sky-500/15 bg-sky-500/8'
1075
+ : tone === 'emerald'
1076
+ ? 'border-emerald-500/15 bg-emerald-500/8'
1077
+ : 'border-violet-500/15 bg-violet-500/8';
1078
+
1079
+ if (!items.length) {
1080
+ return null;
1081
+ }
1082
+
1083
+ return (
1084
+ <div className={cn('rounded-xl border px-2 py-2', toneClassName)}>
1085
+ <div className="mb-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
1086
+ {title}
1087
+ </div>
1088
+ <div className="flex flex-wrap gap-1">
1089
+ {items.map((item) => (
1090
+ <span
1091
+ key={item.key}
1092
+ className="inline-flex items-center gap-1.5 rounded-full border border-border/50 bg-background/90 px-2 py-0.5 text-[10px] text-foreground shadow-sm"
1093
+ >
1094
+ <span
1095
+ className="size-1.5 rounded-full"
1096
+ style={{ backgroundColor: item.color }}
1097
+ />
1098
+ <span className="max-w-44 truncate">{item.label}</span>
1099
+ <span className="font-semibold text-muted-foreground">
1100
+ {Math.round(item.percent)}%
1101
+ </span>
1102
+ </span>
1103
+ ))}
1104
+ </div>
1105
+ </div>
1106
+ );
1107
+ }
1108
+
1109
+ function SegmentCompositionRow<T>({
1110
+ items,
1111
+ getKey,
1112
+ getValue,
1113
+ getColor,
1114
+ }: {
1115
+ items: T[];
1116
+ getKey: (item: T) => number;
1117
+ getValue: (item: T) => number | null | undefined;
1118
+ getColor: (item: T, index: number) => string;
1119
+ }) {
1120
+ const normalizedItems = buildSegmentComposition(items, getValue);
1121
+
1122
+ if (!normalizedItems.length) {
1123
+ return <div className="h-1.5 bg-muted/60" />;
1124
+ }
1125
+
1126
+ return (
1127
+ <div className="flex h-1.5 w-full overflow-hidden">
1128
+ {normalizedItems.map(({ item, percent }, index) => (
1129
+ <div
1130
+ key={`${getKey(item)}-${index}`}
1131
+ className="h-full"
1132
+ style={{
1133
+ width: `${percent}%`,
1134
+ backgroundColor: getColor(item, index),
1135
+ }}
1136
+ />
1137
+ ))}
1138
+ </div>
1139
+ );
1140
+ }
1141
+
1142
+ function SegmentTooltipContent({ segment }: { segment: SegmentView }) {
1143
+ return (
1144
+ <div className="flex flex-col gap-2 text-xs">
1145
+ <div className="flex items-center justify-between gap-3">
1146
+ <span className="font-semibold text-foreground">{segment.label}</span>
1147
+ <span className="font-medium text-foreground">
1148
+ {segment.xpValue} XP
1149
+ </span>
1150
+ </div>
1151
+ <div className="text-muted-foreground">
1152
+ {formatSeconds(segment.startSeconds)} -{' '}
1153
+ {formatSeconds(segment.endSeconds)}
1154
+ </div>
1155
+ <div className="grid gap-1.5">
1156
+ <TooltipMetric
1157
+ label="Área macro"
1158
+ value={segment.areaLabel}
1159
+ color={segment.areaColor}
1160
+ breakdown={segment.areaBreakdown}
1161
+ />
1162
+ <TooltipMetric
1163
+ label="Skill"
1164
+ value={segment.skillLabel}
1165
+ color={segment.skillColor}
1166
+ breakdown={segment.skillBreakdown}
1167
+ />
1168
+ <TooltipMetric
1169
+ label="Tipo de aprendizado"
1170
+ value={segment.learningTypeLabel}
1171
+ color={segment.learningTypeColor}
1172
+ breakdown={segment.learningTypeBreakdown}
1173
+ />
1174
+ </div>
1175
+ </div>
1176
+ );
1177
+ }
1178
+
1179
+ function TooltipMetric({
1180
+ label,
1181
+ value,
1182
+ color,
1183
+ breakdown,
1184
+ }: {
1185
+ label: string;
1186
+ value: string;
1187
+ color: string;
1188
+ breakdown: Array<{
1189
+ key: number;
1190
+ label: string;
1191
+ percent: number;
1192
+ color: string;
1193
+ }>;
1194
+ }) {
1195
+ return (
1196
+ <div className="grid gap-1">
1197
+ <div className="flex items-center gap-2">
1198
+ <span
1199
+ className="size-2 rounded-full"
1200
+ style={{ backgroundColor: color }}
1201
+ />
1202
+ <span className="min-w-24 text-muted-foreground">{label}</span>
1203
+ <span className="truncate font-medium text-foreground">{value}</span>
1204
+ </div>
1205
+ {breakdown.length > 1 && (
1206
+ <div className="flex flex-wrap gap-1">
1207
+ {breakdown.map((item) => (
1208
+ <span
1209
+ key={item.key}
1210
+ className="inline-flex items-center gap-1 rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 text-[10px] text-muted-foreground"
1211
+ >
1212
+ <span
1213
+ className="size-1.5 rounded-full"
1214
+ style={{ backgroundColor: item.color }}
1215
+ />
1216
+ {item.label} {Math.round(item.percent)}%
1217
+ </span>
1218
+ ))}
1219
+ </div>
1220
+ )}
1221
+ </div>
1222
+ );
1223
+ }
1224
+
1225
+ function XpStatusBadge({ status }: { status: LessonXpMapStatus }) {
1226
+ const config: Record<
1227
+ LessonXpMapStatus,
1228
+ { label: string; className: string }
1229
+ > = {
1230
+ pending: { label: 'Pendente', className: 'bg-muted text-muted-foreground' },
1231
+ processing: {
1232
+ label: 'Calculando',
1233
+ className: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-400',
1234
+ },
1235
+ ready: {
1236
+ label: 'Calculado',
1237
+ className: 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-400',
1238
+ },
1239
+ needs_review: {
1240
+ label: 'Revisar',
1241
+ className: 'bg-orange-500/20 text-orange-700 dark:text-orange-400',
1242
+ },
1243
+ approved: {
1244
+ label: 'Aprovado',
1245
+ className: 'bg-blue-500/20 text-blue-700 dark:text-blue-400',
1246
+ },
1247
+ rejected: {
1248
+ label: 'Rejeitado',
1249
+ className: 'bg-destructive/20 text-destructive',
1250
+ },
1251
+ };
1252
+
1253
+ const cfg = config[status] ?? config.pending;
1254
+
1255
+ return (
1256
+ <Badge
1257
+ variant="outline"
1258
+ className={cn('px-1.5 py-0 text-[0.65rem]', cfg.className)}
1259
+ >
1260
+ {cfg.label}
1261
+ </Badge>
1262
+ );
1263
+ }
1264
+
1265
+ function DifficultyBadge({
1266
+ difficulty,
1267
+ }: {
1268
+ difficulty: LessonXpSegment['difficulty'];
1269
+ }) {
1270
+ const config = {
1271
+ easy: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
1272
+ medium: 'bg-blue-500/10 text-blue-700 dark:text-blue-400',
1273
+ hard: 'bg-orange-500/10 text-orange-700 dark:text-orange-400',
1274
+ expert: 'bg-rose-500/10 text-rose-700 dark:text-rose-400',
1275
+ };
1276
+ const labels = {
1277
+ easy: 'Fácil',
1278
+ medium: 'Médio',
1279
+ hard: 'Difícil',
1280
+ expert: 'Expert',
1281
+ };
1282
+
1283
+ return (
1284
+ <span
1285
+ className={cn('rounded px-1 py-0 text-[0.65rem]', config[difficulty])}
1286
+ >
1287
+ {labels[difficulty]}
1288
+ </span>
1289
+ );
1290
+ }
1291
+
1292
+ function aggregateAreas(segments: LessonXpSegment[]): Map<number, number> {
1293
+ const map = new Map<number, number>();
1294
+ for (const segment of segments) {
1295
+ if (!segment.shouldGrantXp) continue;
1296
+ for (const area of segment.areas) {
1297
+ const prev = map.get(area.xpAreaId) ?? 0;
1298
+ map.set(
1299
+ area.xpAreaId,
1300
+ prev + Math.round(segment.xpValue * (area.weightPercent / 100))
1301
+ );
1302
+ }
1303
+ }
1304
+ return new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
1305
+ }
1306
+
1307
+ function aggregateSkills(segments: LessonXpSegment[]): Map<number, number> {
1308
+ const map = new Map<number, number>();
1309
+ for (const segment of segments) {
1310
+ if (!segment.shouldGrantXp) continue;
1311
+ for (const skill of segment.skills) {
1312
+ const prev = map.get(skill.xpSkillId) ?? 0;
1313
+ map.set(
1314
+ skill.xpSkillId,
1315
+ prev + Math.round(segment.xpValue * (skill.weightPercent / 100))
1316
+ );
1317
+ }
1318
+ }
1319
+ return new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
1320
+ }
1321
+
1322
+ function aggregateLearningTypes(
1323
+ segments: LessonXpSegment[]
1324
+ ): Map<
1325
+ number,
1326
+ { xp: number; count: number; learningType: LessonXpSegmentLearningType }
1327
+ > {
1328
+ const map = new Map<
1329
+ number,
1330
+ { xp: number; count: number; learningType: LessonXpSegmentLearningType }
1331
+ >();
1332
+ for (const segment of segments) {
1333
+ if (!segment.shouldGrantXp) continue;
1334
+ for (const learningType of segment.learningTypes) {
1335
+ const prev = map.get(learningType.xpLearningTypeId) ?? {
1336
+ xp: 0,
1337
+ count: 0,
1338
+ learningType,
1339
+ };
1340
+ map.set(learningType.xpLearningTypeId, {
1341
+ xp:
1342
+ prev.xp +
1343
+ Math.round(segment.xpValue * (learningType.weightPercent / 100)),
1344
+ count: prev.count + 1,
1345
+ learningType: prev.learningType,
1346
+ });
1347
+ }
1348
+ }
1349
+ return new Map([...map.entries()].sort((a, b) => b[1].xp - a[1].xp));
1350
+ }
1351
+
1352
+ function buildDistributionItems(
1353
+ aggregated: Map<number, number>,
1354
+ totalXp: number,
1355
+ resolveLabel: (id: number) => string,
1356
+ resolveColor: (id: number, index: number) => string
1357
+ ): DistributionItem[] {
1358
+ return [...aggregated.entries()].map(([id, xp], index) => ({
1359
+ id,
1360
+ label: resolveLabel(id),
1361
+ xp,
1362
+ percent: totalXp > 0 ? Math.round((xp / totalXp) * 100) : 0,
1363
+ color: resolveColor(id, index),
1364
+ }));
1365
+ }
1366
+
1367
+ function pickDominantItem<T extends { weightPercent: number }>(
1368
+ items: T[]
1369
+ ): T | null {
1370
+ if (!items.length) return null;
1371
+ return (
1372
+ [...items].sort((a, b) => b.weightPercent - a.weightPercent)[0] ?? null
1373
+ );
1374
+ }
1375
+
1376
+ function buildSegmentComposition<T>(
1377
+ items: T[],
1378
+ getValue: (item: T) => number | null | undefined
1379
+ ) {
1380
+ if (!items.length) return [];
1381
+
1382
+ const resolved = items.map((item) => ({
1383
+ item,
1384
+ value: Math.max(getValue(item) ?? 0, 0),
1385
+ }));
1386
+ const total = resolved.reduce((sum, entry) => sum + entry.value, 0);
1387
+
1388
+ if (total > 0) {
1389
+ return resolved.map((entry) => ({
1390
+ item: entry.item,
1391
+ percent: (entry.value / total) * 100,
1392
+ }));
1393
+ }
1394
+
1395
+ const fallbackPercent = 100 / resolved.length;
1396
+ return resolved.map((entry) => ({
1397
+ item: entry.item,
1398
+ percent: fallbackPercent,
1399
+ }));
1400
+ }
1401
+
1402
+ function countUniqueAreas(segments: LessonXpSegment[]): number {
1403
+ const ids = new Set<number>();
1404
+ for (const segment of segments)
1405
+ segment.areas.forEach((area) => ids.add(area.xpAreaId));
1406
+ return ids.size;
1407
+ }
1408
+
1409
+ function countUniqueSkills(segments: LessonXpSegment[]): number {
1410
+ const ids = new Set<number>();
1411
+ for (const segment of segments)
1412
+ segment.skills.forEach((skill) => ids.add(skill.xpSkillId));
1413
+ return ids.size;
1414
+ }
1415
+
1416
+ function countUniqueLearningTypes(segments: LessonXpSegment[]): number {
1417
+ const ids = new Set<number>();
1418
+ for (const segment of segments) {
1419
+ segment.learningTypes.forEach((learningType) =>
1420
+ ids.add(learningType.xpLearningTypeId)
1421
+ );
1422
+ }
1423
+ return ids.size;
1424
+ }
1425
+
1426
+ function avgConfidence(segments: LessonXpSegment[]): number | null {
1427
+ const values = segments
1428
+ .map((segment) => segment.aiConfidence)
1429
+ .filter((value): value is number => value !== null);
1430
+ if (!values.length) return null;
1431
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
1432
+ }
1433
+
1434
+ function formatSeconds(seconds: number): string {
1435
+ const minutes = Math.floor(seconds / 60);
1436
+ const restSeconds = Math.floor(seconds % 60);
1437
+ return `${String(minutes).padStart(2, '0')}:${String(restSeconds).padStart(2, '0')}`;
1438
+ }
1439
+
1440
+ function formatDate(iso: string): string {
1441
+ try {
1442
+ return new Date(iso).toLocaleDateString('pt-BR', {
1443
+ day: '2-digit',
1444
+ month: '2-digit',
1445
+ year: 'numeric',
1446
+ });
1447
+ } catch {
1448
+ return iso;
1449
+ }
1450
+ }
1451
+
1452
+ function formatMultiplier(value: number, locale: string): string {
1453
+ const fractionDigits = Number.isInteger(value) ? 0 : 2;
1454
+ return `x${new Intl.NumberFormat(locale, {
1455
+ minimumFractionDigits: fractionDigits,
1456
+ maximumFractionDigits: 2,
1457
+ }).format(value)}`;
1458
+ }