@hed-hog/lms 0.0.365 → 0.0.370

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 (243) hide show
  1. package/dist/certificate/certificate.controller.d.ts +1 -1
  2. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  3. package/dist/certificate/certificate.controller.js +4 -2
  4. package/dist/certificate/certificate.controller.js.map +1 -1
  5. package/dist/certificate/certificate.service.d.ts +50 -0
  6. package/dist/certificate/certificate.service.d.ts.map +1 -1
  7. package/dist/certificate/certificate.service.js +73 -0
  8. package/dist/certificate/certificate.service.js.map +1 -1
  9. package/dist/class-group/class-group.controller.d.ts +1 -0
  10. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  11. package/dist/class-group/class-group.service.d.ts +1 -0
  12. package/dist/class-group/class-group.service.d.ts.map +1 -1
  13. package/dist/course/course-ai-usage.service.d.ts +58 -0
  14. package/dist/course/course-ai-usage.service.d.ts.map +1 -0
  15. package/dist/course/course-ai-usage.service.js +176 -0
  16. package/dist/course/course-ai-usage.service.js.map +1 -0
  17. package/dist/course/course-audio-transcription.service.d.ts +65 -1
  18. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  19. package/dist/course/course-audio-transcription.service.js +381 -29
  20. package/dist/course/course-audio-transcription.service.js.map +1 -1
  21. package/dist/course/course-export-scorm12.service.d.ts +3 -0
  22. package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
  23. package/dist/course/course-export-scorm12.service.js +141 -6
  24. package/dist/course/course-export-scorm12.service.js.map +1 -1
  25. package/dist/course/course-export.service.d.ts.map +1 -1
  26. package/dist/course/course-export.service.js +2 -1
  27. package/dist/course/course-export.service.js.map +1 -1
  28. package/dist/course/course-lesson.controller.d.ts +25 -3
  29. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  30. package/dist/course/course-lesson.controller.js +71 -8
  31. package/dist/course/course-lesson.controller.js.map +1 -1
  32. package/dist/course/course-structure.controller.d.ts +30 -7
  33. package/dist/course/course-structure.controller.d.ts.map +1 -1
  34. package/dist/course/course-structure.controller.js +37 -4
  35. package/dist/course/course-structure.controller.js.map +1 -1
  36. package/dist/course/course-structure.service.d.ts +37 -5
  37. package/dist/course/course-structure.service.d.ts.map +1 -1
  38. package/dist/course/course-structure.service.js +165 -20
  39. package/dist/course/course-structure.service.js.map +1 -1
  40. package/dist/course/course-transcription-translation.service.d.ts +31 -0
  41. package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
  42. package/dist/course/course-transcription-translation.service.js +227 -0
  43. package/dist/course/course-transcription-translation.service.js.map +1 -0
  44. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  45. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  46. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  47. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  48. package/dist/course/course-video-hls.service.d.ts +14 -0
  49. package/dist/course/course-video-hls.service.d.ts.map +1 -1
  50. package/dist/course/course-video-hls.service.js +25 -8
  51. package/dist/course/course-video-hls.service.js.map +1 -1
  52. package/dist/course/course.controller.d.ts +2 -0
  53. package/dist/course/course.controller.d.ts.map +1 -1
  54. package/dist/course/course.module.d.ts.map +1 -1
  55. package/dist/course/course.module.js +9 -0
  56. package/dist/course/course.module.js.map +1 -1
  57. package/dist/course/course.service.d.ts +2 -0
  58. package/dist/course/course.service.d.ts.map +1 -1
  59. package/dist/course/course.service.js +36 -2
  60. package/dist/course/course.service.js.map +1 -1
  61. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  62. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  63. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  64. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  65. package/dist/course/dto/create-course-export.dto.d.ts +1 -0
  66. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
  67. package/dist/course/dto/create-course-export.dto.js +6 -0
  68. package/dist/course/dto/create-course-export.dto.js.map +1 -1
  69. package/dist/course/ffmpeg.util.d.ts +10 -0
  70. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  71. package/dist/course/ffmpeg.util.js +79 -0
  72. package/dist/course/ffmpeg.util.js.map +1 -0
  73. package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
  74. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  75. package/dist/course/lms-bulk-upload-automation.service.js +33 -16
  76. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  77. package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
  78. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  79. package/dist/course/lms-bulk-upload.service.d.ts +3 -0
  80. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  81. package/dist/course/lms-bulk-upload.service.js +48 -29
  82. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  83. package/dist/course/subtitle.util.d.ts +46 -0
  84. package/dist/course/subtitle.util.d.ts.map +1 -0
  85. package/dist/course/subtitle.util.js +206 -0
  86. package/dist/course/subtitle.util.js.map +1 -0
  87. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  88. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  89. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  90. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  91. package/dist/enterprise/training/training-student.service.d.ts +27 -0
  92. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  93. package/dist/enterprise/training/training-student.service.js +197 -10
  94. package/dist/enterprise/training/training-student.service.js.map +1 -1
  95. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
  96. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  97. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
  98. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  99. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
  100. package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
  101. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
  102. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  103. package/dist/lms.module.d.ts.map +1 -1
  104. package/dist/lms.module.js +14 -0
  105. package/dist/lms.module.js.map +1 -1
  106. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  107. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  108. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  109. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  110. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  111. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  112. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  113. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  114. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  115. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  116. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  117. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  118. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  119. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  120. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  121. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  122. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  123. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  124. package/dist/platforma/platforma-performance.service.js +500 -0
  125. package/dist/platforma/platforma-performance.service.js.map +1 -0
  126. package/dist/platforma/platforma-search.service.d.ts +21 -0
  127. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  128. package/dist/platforma/platforma-search.service.js +64 -0
  129. package/dist/platforma/platforma-search.service.js.map +1 -0
  130. package/dist/platforma/platforma-video.service.d.ts +8 -0
  131. package/dist/platforma/platforma-video.service.d.ts.map +1 -1
  132. package/dist/platforma/platforma-video.service.js +45 -2
  133. package/dist/platforma/platforma-video.service.js.map +1 -1
  134. package/dist/platforma/platforma.controller.d.ts +213 -1
  135. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  136. package/dist/platforma/platforma.controller.js +159 -2
  137. package/dist/platforma/platforma.controller.js.map +1 -1
  138. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  139. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  140. package/dist/realtime/lms-realtime.controller.js +31 -0
  141. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  142. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  143. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  144. package/dist/realtime/lms-realtime.service.js.map +1 -1
  145. package/dist/training/dto/create-training.dto.d.ts +9 -0
  146. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  147. package/dist/training/dto/create-training.dto.js +45 -1
  148. package/dist/training/dto/create-training.dto.js.map +1 -1
  149. package/dist/training/training.controller.d.ts +144 -0
  150. package/dist/training/training.controller.d.ts.map +1 -1
  151. package/dist/training/training.service.d.ts +149 -0
  152. package/dist/training/training.service.d.ts.map +1 -1
  153. package/dist/training/training.service.js +332 -167
  154. package/dist/training/training.service.js.map +1 -1
  155. package/hedhog/data/image_type.yaml +10 -0
  156. package/hedhog/data/route.yaml +251 -0
  157. package/hedhog/data/setting_group.yaml +97 -0
  158. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
  159. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  160. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  161. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  162. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
  163. package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
  164. package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
  165. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
  166. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
  167. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
  168. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  169. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
  170. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  171. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
  172. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
  173. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
  174. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
  175. package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
  176. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
  177. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
  178. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  179. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
  180. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
  181. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
  182. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
  183. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
  184. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
  185. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
  186. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
  187. package/hedhog/frontend/app/courses/page.tsx.ejs +66 -13
  188. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  189. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  190. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  191. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  192. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  193. package/hedhog/frontend/app/paths/page.tsx.ejs +650 -168
  194. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  195. package/hedhog/frontend/messages/en.json +41 -12
  196. package/hedhog/frontend/messages/pt.json +44 -13
  197. package/hedhog/query/triggers.sql +33 -0
  198. package/hedhog/table/course_ai_usage.yaml +46 -0
  199. package/hedhog/table/course_enrollment.yaml +3 -0
  200. package/hedhog/table/course_lesson.yaml +3 -0
  201. package/hedhog/table/course_lesson_answer.yaml +37 -0
  202. package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
  203. package/hedhog/table/learning_path.yaml +6 -0
  204. package/hedhog/table/learning_path_module.yaml +22 -0
  205. package/hedhog/table/learning_path_step.yaml +9 -6
  206. package/hedhog/table/lesson_view_event.yaml +66 -0
  207. package/package.json +8 -7
  208. package/src/certificate/certificate.controller.ts +2 -0
  209. package/src/certificate/certificate.service.ts +99 -0
  210. package/src/course/course-ai-usage.service.ts +221 -0
  211. package/src/course/course-audio-transcription.service.ts +471 -43
  212. package/src/course/course-export-scorm12.service.ts +149 -5
  213. package/src/course/course-export.service.ts +1 -0
  214. package/src/course/course-lesson.controller.ts +59 -6
  215. package/src/course/course-structure.controller.ts +19 -1
  216. package/src/course/course-structure.service.ts +184 -10
  217. package/src/course/course-transcription-translation.service.ts +293 -0
  218. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  219. package/src/course/course-video-hls.service.ts +30 -10
  220. package/src/course/course.module.ts +9 -0
  221. package/src/course/course.service.ts +46 -1
  222. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  223. package/src/course/dto/create-course-export.dto.ts +6 -0
  224. package/src/course/ffmpeg.util.ts +65 -0
  225. package/src/course/lms-bulk-upload-automation.service.ts +33 -8
  226. package/src/course/lms-bulk-upload.service.ts +20 -1
  227. package/src/course/subtitle.util.ts +220 -0
  228. package/src/enterprise/training/training-student.service.ts +224 -4
  229. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
  230. package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
  231. package/src/lms.module.ts +14 -0
  232. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  233. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  234. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  235. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  236. package/src/platforma/platforma-performance.service.ts +606 -0
  237. package/src/platforma/platforma-search.service.ts +48 -0
  238. package/src/platforma/platforma-video.service.ts +59 -3
  239. package/src/platforma/platforma.controller.ts +130 -0
  240. package/src/realtime/lms-realtime.controller.ts +27 -1
  241. package/src/realtime/lms-realtime.service.ts +2 -1
  242. package/src/training/dto/create-training.dto.ts +36 -0
  243. package/src/training/training.service.ts +360 -163
@@ -16,6 +16,7 @@ import {
16
16
  GripVertical,
17
17
  HelpCircle,
18
18
  Image,
19
+ Languages,
19
20
  ListChecks,
20
21
  Loader2,
21
22
  Lock,
@@ -33,6 +34,7 @@ import {
33
34
  type LucideIcon,
34
35
  } from 'lucide-react';
35
36
  import { useTranslations } from 'next-intl';
37
+ import Link from 'next/link';
36
38
  import { useEffect, useRef, useState } from 'react';
37
39
  import { useForm, useWatch } from 'react-hook-form';
38
40
  import { toast } from 'sonner';
@@ -70,6 +72,7 @@ import {
70
72
  SelectTrigger,
71
73
  SelectValue,
72
74
  } from '@/components/ui/select';
75
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
73
76
  import { Separator } from '@/components/ui/separator';
74
77
  import {
75
78
  Sheet,
@@ -130,11 +133,16 @@ import {
130
133
  } from '../_data/use-course-structure-query';
131
134
  import { useLmsSettingsQuery } from '../_data/use-lms-settings-query';
132
135
  import {
136
+ useDeleteTranscriptionLocaleMutation,
133
137
  useStartTranscriptionMutation,
138
+ useTranscriptionLocalesQuery,
134
139
  useTranscriptionSegmentsQuery,
140
+ useTranslateTranscriptionMutation,
135
141
  useUpdateTranscriptionSegmentsMutation,
142
+ type TranscriptionLocale,
136
143
  } from '../_data/use-transcription-segments';
137
144
  import { LessonXpTab } from './detail-lesson-xp-tab';
145
+ import { LessonVideoPreview } from './lesson-video-preview';
138
146
  import { IconActionTooltip } from './icon-action-tooltip';
139
147
  import { useStructureStore } from './store';
140
148
  import type {
@@ -148,6 +156,14 @@ import type {
148
156
 
149
157
  const EMPTY_VIDEO_FRAMES: VideoFrame[] = [];
150
158
 
159
+ function toFileSafeName(value: string): string {
160
+ return value
161
+ .replace(/[<>:"/\\|?*]/g, '') // caracteres ilegais em Win/Unix
162
+ .replace(/\s+/g, '_') // espaços/tabs/quebras -> underscore
163
+ .replace(/_+/g, '_') // colapsa underscores
164
+ .replace(/^[._]+|[._]+$/g, ''); // remove dots/underscores nas pontas
165
+ }
166
+
151
167
  function formatFileSize(bytes: number): string {
152
168
  if (bytes < 1024) return `${bytes} B`;
153
169
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -795,6 +811,7 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
795
811
  );
796
812
  const updateLessonInStore = useStructureStore((s) => s.updateLesson);
797
813
  const sessions = useStructureStore((s) => s.sessions);
814
+ const course = useStructureStore((s) => s.course);
798
815
  const persistedVideoProvider: VideoProvider | undefined =
799
816
  lesson?.videoProvider === 'youtube' || lesson?.videoProvider === 'vimeo'
800
817
  ? lesson.videoProvider
@@ -802,12 +819,24 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
802
819
  ? 'file_storage'
803
820
  : undefined;
804
821
  const videoFrames = lesson?.frames ?? EMPTY_VIDEO_FRAMES;
822
+ const [selectedLocaleId, setSelectedLocaleId] = useState<number | null | undefined>(undefined);
823
+ const [translateOpen, setTranslateOpen] = useState(false);
824
+ const [targetLocaleId, setTargetLocaleId] = useState<number | null>(null);
825
+ const [activeTranslationJob, setActiveTranslationJob] = useState<{
826
+ queueJobId: number;
827
+ targetLocaleName: string;
828
+ } | null>(null);
805
829
  const updateLesson = useUpdateLessonMutation();
806
830
  const updateTranscriptionSegments = useUpdateTranscriptionSegmentsMutation(
807
- lesson?.id ?? null
831
+ lesson?.id ?? null,
832
+ selectedLocaleId,
808
833
  );
809
834
  const { mutate: startTranscription, isPending: isStartingTranscription } =
810
835
  useStartTranscriptionMutation(lesson?.id ?? null);
836
+ const { mutate: translateTranscription, isPending: isTranslating } =
837
+ useTranslateTranscriptionMutation(lesson?.id ?? null);
838
+ const { mutate: deleteTranscriptionLocale } =
839
+ useDeleteTranscriptionLocaleMutation(lesson?.id ?? null);
811
840
  const updateResourceTypeMutation = useUpdateResourceTypeMutation();
812
841
  const deleteLesson = useDeleteLessonMutation();
813
842
  const showConfirm = useStructureStore((s) => s.showConfirm);
@@ -1077,7 +1106,55 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
1077
1106
  const {
1078
1107
  data: fetchedTranscriptionSegments = [],
1079
1108
  isLoading: isLoadingTranscription,
1080
- } = useTranscriptionSegmentsQuery(lesson?.id ?? null);
1109
+ } = useTranscriptionSegmentsQuery(lesson?.id ?? null, selectedLocaleId);
1110
+
1111
+ const { data: transcriptionLocales = [], refetch: refetchLocales } =
1112
+ useTranscriptionLocalesQuery(lesson?.id ?? null);
1113
+
1114
+ const { data: allLocales = [] } = useQuery<TranscriptionLocale[]>({
1115
+ queryKey: ['all-locales-editor'],
1116
+ queryFn: async () => {
1117
+ const res = await request<{ data: TranscriptionLocale[] }>({
1118
+ url: '/core/locales?limit=200',
1119
+ method: 'GET',
1120
+ });
1121
+ return (res.data?.data ?? []) as TranscriptionLocale[];
1122
+ },
1123
+ enabled: translateOpen,
1124
+ initialData: [],
1125
+ });
1126
+
1127
+ const availableTargetLocales = allLocales.filter(
1128
+ (l) => l.id !== null && !transcriptionLocales.some((tl) => tl.id === l.id),
1129
+ );
1130
+
1131
+ const handleDeleteLocale = (loc: TranscriptionLocale) => {
1132
+ const name = loc.name ?? loc.code ?? 'este idioma';
1133
+ showConfirm({
1134
+ title: 'Excluir transcrição',
1135
+ description: `Excluir a transcrição/tradução em "${name}" desta aula? Esta ação não pode ser desfeita.`,
1136
+ confirmText: 'Excluir',
1137
+ destructive: true,
1138
+ onConfirm: () => {
1139
+ deleteTranscriptionLocale(loc.id, {
1140
+ onSuccess: () => {
1141
+ toast.success('Transcrição excluída.');
1142
+ if (selectedLocaleId === loc.id) setSelectedLocaleId(undefined);
1143
+ refetchLocales();
1144
+ queryClient.invalidateQueries({
1145
+ queryKey: ['lesson-transcription-locales', lesson?.id],
1146
+ });
1147
+ queryClient.invalidateQueries({
1148
+ queryKey: ['lesson-transcription-segments', lesson?.id],
1149
+ });
1150
+ },
1151
+ onError: (err) => {
1152
+ toast.error(err.message || 'Erro ao excluir a transcrição.');
1153
+ },
1154
+ });
1155
+ },
1156
+ });
1157
+ };
1081
1158
 
1082
1159
  const {
1083
1160
  data: conversionJob,
@@ -1153,6 +1230,15 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
1153
1230
  useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
1154
1231
  );
1155
1232
 
1233
+ useEffect(() => {
1234
+ if (transcriptionLocales.length > 0 && selectedLocaleId === undefined) {
1235
+ setSelectedLocaleId(transcriptionLocales[0]?.id ?? null);
1236
+ }
1237
+ if (transcriptionLocales.length === 0 && selectedLocaleId !== undefined) {
1238
+ setSelectedLocaleId(undefined);
1239
+ }
1240
+ }, [transcriptionLocales, selectedLocaleId]);
1241
+
1156
1242
  useEffect(() => {
1157
1243
  setLocalResources(lesson?.resources ?? []);
1158
1244
  setResourcesDirty(false);
@@ -1165,6 +1251,8 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
1165
1251
  setFrameImageErrorIds(new Set<string>());
1166
1252
  resourceMetadataLoadedRef.current.clear();
1167
1253
  frameMetadataLoadedRef.current.clear();
1254
+ setSelectedLocaleId(undefined);
1255
+ setActiveTranslationJob(null);
1168
1256
  }, [lesson?.id, lesson?.resources, lesson?.videoConversionJobId]); // eslint-disable-line react-hooks/exhaustive-deps
1169
1257
 
1170
1258
  useEffect(() => {
@@ -1424,7 +1512,14 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
1424
1512
  const lessonTypeLabel = t(cfg.labelKey);
1425
1513
  const session = sessions.find((s) => s.id === lesson.sessionId);
1426
1514
  const lessonFullCode = session
1427
- ? `${session.code}_${lesson.code}_${lesson.title}`
1515
+ ? [
1516
+ session.code,
1517
+ course?.code?.trim() || null,
1518
+ lesson.code,
1519
+ toFileSafeName(lesson.title),
1520
+ ]
1521
+ .filter(Boolean)
1522
+ .join('_')
1428
1523
  : null;
1429
1524
  const originalVideoResource =
1430
1525
  localResources.find((res) => res.type === 'video_original') ?? null;
@@ -1591,6 +1686,27 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
1591
1686
  originalFileId
1592
1687
  );
1593
1688
  setConversionJobId(queued.queueJobId);
1689
+ // Optimistically seed the job cache so the card reflects the active state
1690
+ // (na fila → processando) immediately, instead of briefly showing the
1691
+ // previous job's terminal status while the new job is fetched.
1692
+ queryClient.setQueryData<QueueJobResponse>(['queue-job', queued.queueJobId], {
1693
+ id: queued.queueJobId,
1694
+ status: 'pending',
1695
+ attempts: 0,
1696
+ max_attempts: 0,
1697
+ created_at: new Date().toISOString(),
1698
+ started_at: null,
1699
+ finished_at: null,
1700
+ next_retry_at: null,
1701
+ last_error: null,
1702
+ result: null,
1703
+ queue_job_attempt: [],
1704
+ queue_job_event: [],
1705
+ });
1706
+ // Reflect the new job id on the lesson in the tree store right away so the
1707
+ // árvore shows the phase icon (waiting → processing) without waiting for
1708
+ // the structure refetch to propagate.
1709
+ updateLessonInStore(lessonId, { videoConversionJobId: queued.queueJobId });
1594
1710
  toast.success(
1595
1711
  t('lessonForm.videoConversionQueued', {
1596
1712
  id: queued.queueJobId,
@@ -2414,6 +2530,28 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
2414
2530
  setTranscriptionSegments(updater);
2415
2531
  }
2416
2532
 
2533
+ function handleTranslateInEditor() {
2534
+ if (!targetLocaleId) return;
2535
+ const targetName =
2536
+ availableTargetLocales.find((l) => l.id === targetLocaleId)?.name ?? String(targetLocaleId);
2537
+ translateTranscription(targetLocaleId, selectedLocaleId ?? null, {
2538
+ onSuccess: (result) => {
2539
+ toast.success('Tradução iniciada! Acompanhe o progresso na fila.');
2540
+ setTranslateOpen(false);
2541
+ setTargetLocaleId(null);
2542
+ setActiveTranslationJob({ queueJobId: result.queueJobId, targetLocaleName: targetName });
2543
+ setTimeout(() => {
2544
+ void refetchLocales();
2545
+ void queryClient.invalidateQueries({ queryKey: ['lesson-transcription-segments', lesson?.id] });
2546
+ void queryClient.invalidateQueries({ queryKey: ['lesson-transcription-locales', lesson?.id] });
2547
+ }, 2000);
2548
+ },
2549
+ onError: (err) => {
2550
+ toast.error(err.message || 'Erro ao iniciar a tradução.');
2551
+ },
2552
+ });
2553
+ }
2554
+
2417
2555
  function persistLesson(values: FormValues, shouldConfirmAutoStatus: boolean) {
2418
2556
  if (values.type === 'video') {
2419
2557
  const segmentsPayload = transcriptionSegments
@@ -3112,6 +3250,21 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
3112
3250
  <TabsContent value="conteudo" className="flex-1 min-h-0 mt-0">
3113
3251
  <ScrollArea className="h-full">
3114
3252
  <div className="flex flex-col gap-2 p-2 sm:gap-3 sm:p-3">
3253
+ {/* Player de preview para aulas de vídeo */}
3254
+ {watchedType === 'video' && (
3255
+ <Card className="bg-muted/20 py-2 gap-2">
3256
+ <CardHeader className="px-3 pt-2 pb-1">
3257
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
3258
+ <Play className="size-3 text-blue-500" />
3259
+ Preview
3260
+ </CardTitle>
3261
+ </CardHeader>
3262
+ <CardContent className="px-3 pb-2">
3263
+ <LessonVideoPreview lessonId={lessonId} locales={transcriptionLocales} />
3264
+ </CardContent>
3265
+ </Card>
3266
+ )}
3267
+
3115
3268
  {/* Descrição pública */}
3116
3269
  <Card className="bg-muted/20 py-2 gap-2">
3117
3270
  <CardHeader className="px-3 pt-2 pb-1">
@@ -4262,64 +4415,196 @@ export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLesson
4262
4415
  <TabsContent value="transcricao" className="flex-1 min-h-0 mt-0">
4263
4416
  <ScrollArea className="h-full">
4264
4417
  <div className="flex flex-col gap-2 p-2 sm:gap-3 sm:p-3">
4418
+ {/* Banner de job de tradução ativo */}
4419
+ {activeTranslationJob && (
4420
+ <div className="flex items-center justify-between gap-2 rounded-md border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30 px-3 py-2 text-sm">
4421
+ <div className="flex items-center gap-2 min-w-0">
4422
+ <Loader2 className="size-4 shrink-0 animate-spin text-amber-600 dark:text-amber-400" />
4423
+ <span className="text-amber-900 dark:text-amber-200 truncate">
4424
+ Tradução para{' '}
4425
+ <strong>{activeTranslationJob.targetLocaleName}</strong> em andamento
4426
+ {' '}· Job #{activeTranslationJob.queueJobId}
4427
+ </span>
4428
+ </div>
4429
+ <div className="flex items-center gap-2 shrink-0">
4430
+ <Link
4431
+ href="/queue/jobs"
4432
+ className="text-xs text-amber-700 dark:text-amber-300 underline underline-offset-2 hover:text-amber-900 dark:hover:text-amber-100"
4433
+ >
4434
+ Ver na fila →
4435
+ </Link>
4436
+ <button
4437
+ onClick={() => setActiveTranslationJob(null)}
4438
+ className="text-amber-600 dark:text-amber-400 hover:text-amber-900 dark:hover:text-amber-100"
4439
+ >
4440
+ <X className="size-3.5" />
4441
+ </button>
4442
+ </div>
4443
+ </div>
4444
+ )}
4265
4445
  <Card className="bg-muted/20 py-2 gap-2">
4266
4446
  <CardHeader className="px-3 pt-2 pb-1">
4267
- <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center justify-between gap-2">
4268
- <span>{t('lessonForm.transcriptionSegments')}</span>
4269
- <div className="flex items-center gap-1.5">
4270
- <Button
4271
- type="button"
4272
- variant="outline"
4273
- size="sm"
4274
- className="h-6 text-xs px-2"
4275
- disabled={isStartingTranscription}
4276
- onClick={() =>
4277
- startTranscription({
4278
- onSuccess: () => {
4279
- toast.success('Transcrição iniciada!');
4280
- queryClient.invalidateQueries({
4281
- queryKey: [
4282
- 'lesson-transcription-segments',
4283
- lesson?.id,
4284
- ],
4285
- });
4286
- },
4287
- onError: (err) => {
4288
- toast.error(
4289
- err.message ||
4290
- 'Erro ao iniciar a transcrição.'
4291
- );
4292
- },
4293
- })
4294
- }
4295
- >
4296
- {isStartingTranscription ? (
4297
- <Loader2 className="size-3 mr-1 animate-spin" />
4298
- ) : (
4299
- <Mic className="size-3 mr-1" />
4300
- )}
4301
- Iniciar transcrição
4302
- </Button>
4303
- <Button
4304
- type="button"
4305
- variant="outline"
4306
- size="sm"
4307
- className="h-6 text-xs px-2"
4308
- onClick={() =>
4309
- updateTranscriptionSegmentsState((prev) => [
4310
- ...prev,
4311
- {
4312
- id: segmentId(),
4313
- start: '00:00',
4314
- end: '00:15',
4315
- text: '',
4316
- },
4317
- ])
4318
- }
4319
- >
4320
- <Plus className="size-3 mr-1" />
4321
- {t('lessonForm.newTranscriptionSegment')}
4322
- </Button>
4447
+ <CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex flex-col gap-2">
4448
+ {/* Linha 1: pills de locale + botão Traduzir */}
4449
+ {transcriptionLocales.length > 0 && (
4450
+ <div className="flex items-center justify-between gap-2 flex-wrap">
4451
+ <div className="flex items-center gap-1.5 flex-wrap">
4452
+ {transcriptionLocales.map((loc) => (
4453
+ <div
4454
+ key={loc.id ?? 'null'}
4455
+ className={cn(
4456
+ 'flex items-center gap-1 rounded-full pl-2 pr-1 py-0.5 text-[10px] font-medium border transition-colors normal-case',
4457
+ selectedLocaleId === loc.id
4458
+ ? 'bg-primary text-primary-foreground border-primary'
4459
+ : 'bg-muted text-muted-foreground border-border hover:border-primary/50',
4460
+ )}
4461
+ >
4462
+ <button
4463
+ type="button"
4464
+ onClick={() => setSelectedLocaleId(loc.id)}
4465
+ className="flex items-center gap-1"
4466
+ >
4467
+ <Languages className="size-3" />
4468
+ {loc.name ?? loc.code ?? 'Desconhecido'}
4469
+ </button>
4470
+ <button
4471
+ type="button"
4472
+ onClick={() => handleDeleteLocale(loc)}
4473
+ title="Excluir transcrição deste idioma"
4474
+ className={cn(
4475
+ 'rounded-full p-0.5 transition-colors',
4476
+ selectedLocaleId === loc.id
4477
+ ? 'hover:bg-primary-foreground/20'
4478
+ : 'hover:bg-foreground/10',
4479
+ )}
4480
+ >
4481
+ <Trash2 className="size-3" />
4482
+ </button>
4483
+ </div>
4484
+ ))}
4485
+ </div>
4486
+ <Popover open={translateOpen} onOpenChange={setTranslateOpen}>
4487
+ <PopoverTrigger asChild>
4488
+ <Button
4489
+ type="button"
4490
+ size="sm"
4491
+ variant="outline"
4492
+ className="h-6 text-xs px-2 normal-case"
4493
+ disabled={isTranslating}
4494
+ >
4495
+ {isTranslating ? (
4496
+ <Loader2 className="size-3 mr-1 animate-spin" />
4497
+ ) : (
4498
+ <Plus className="size-3 mr-1" />
4499
+ )}
4500
+ Traduzir
4501
+ </Button>
4502
+ </PopoverTrigger>
4503
+ <PopoverContent className="w-56 p-3" align="end">
4504
+ <p className="text-xs font-medium mb-2 text-muted-foreground normal-case">
4505
+ Traduzir para:
4506
+ </p>
4507
+ {availableTargetLocales.length === 0 ? (
4508
+ <p className="text-xs text-muted-foreground normal-case">
4509
+ Todos os idiomas disponíveis já foram traduzidos.
4510
+ </p>
4511
+ ) : (
4512
+ <div className="flex flex-col gap-1">
4513
+ {availableTargetLocales.map((loc) => (
4514
+ <button
4515
+ key={loc.id}
4516
+ type="button"
4517
+ onClick={() => setTargetLocaleId(loc.id)}
4518
+ className={cn(
4519
+ 'flex items-center gap-2 rounded-md px-2 py-1.5 text-xs text-left transition-colors normal-case',
4520
+ targetLocaleId === loc.id
4521
+ ? 'bg-primary text-primary-foreground'
4522
+ : 'hover:bg-muted',
4523
+ )}
4524
+ >
4525
+ {loc.name ?? loc.code}
4526
+ </button>
4527
+ ))}
4528
+ <Button
4529
+ type="button"
4530
+ size="sm"
4531
+ className="mt-2 w-full h-7 text-xs normal-case"
4532
+ disabled={!targetLocaleId || isTranslating}
4533
+ onClick={handleTranslateInEditor}
4534
+ >
4535
+ {isTranslating && (
4536
+ <Loader2 className="size-3 mr-1 animate-spin" />
4537
+ )}
4538
+ Confirmar tradução
4539
+ </Button>
4540
+ </div>
4541
+ )}
4542
+ </PopoverContent>
4543
+ </Popover>
4544
+ </div>
4545
+ )}
4546
+ {/* Linha 2: título + botões de ação */}
4547
+ <div className="flex items-center justify-between gap-2">
4548
+ <span>{t('lessonForm.transcriptionSegments')}</span>
4549
+ <div className="flex items-center gap-1.5">
4550
+ <Button
4551
+ type="button"
4552
+ variant="outline"
4553
+ size="sm"
4554
+ className="h-6 text-xs px-2 normal-case"
4555
+ disabled={isStartingTranscription}
4556
+ onClick={() =>
4557
+ startTranscription({
4558
+ onSuccess: () => {
4559
+ toast.success('Transcrição iniciada!');
4560
+ void queryClient.invalidateQueries({
4561
+ queryKey: [
4562
+ 'lesson-transcription-segments',
4563
+ lesson?.id,
4564
+ ],
4565
+ });
4566
+ void queryClient.invalidateQueries({
4567
+ queryKey: ['lesson-transcription-locales', lesson?.id],
4568
+ });
4569
+ void refetchLocales();
4570
+ },
4571
+ onError: (err) => {
4572
+ toast.error(
4573
+ err.message ||
4574
+ 'Erro ao iniciar a transcrição.'
4575
+ );
4576
+ },
4577
+ })
4578
+ }
4579
+ >
4580
+ {isStartingTranscription ? (
4581
+ <Loader2 className="size-3 mr-1 animate-spin" />
4582
+ ) : (
4583
+ <Mic className="size-3 mr-1" />
4584
+ )}
4585
+ Iniciar transcrição
4586
+ </Button>
4587
+ <Button
4588
+ type="button"
4589
+ variant="outline"
4590
+ size="sm"
4591
+ className="h-6 text-xs px-2 normal-case"
4592
+ onClick={() =>
4593
+ updateTranscriptionSegmentsState((prev) => [
4594
+ ...prev,
4595
+ {
4596
+ id: segmentId(),
4597
+ start: '00:00',
4598
+ end: '00:15',
4599
+ text: '',
4600
+ },
4601
+ ])
4602
+ }
4603
+ >
4604
+ <Plus className="size-3 mr-1" />
4605
+ {t('lessonForm.newTranscriptionSegment')}
4606
+ </Button>
4607
+ </div>
4323
4608
  </div>
4324
4609
  </CardTitle>
4325
4610
  </CardHeader>