@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.
- package/dist/certificate/certificate.controller.d.ts +1 -1
- package/dist/certificate/certificate.controller.d.ts.map +1 -1
- package/dist/certificate/certificate.controller.js +4 -2
- package/dist/certificate/certificate.controller.js.map +1 -1
- package/dist/certificate/certificate.service.d.ts +50 -0
- package/dist/certificate/certificate.service.d.ts.map +1 -1
- package/dist/certificate/certificate.service.js +73 -0
- package/dist/certificate/certificate.service.js.map +1 -1
- package/dist/class-group/class-group.controller.d.ts +1 -0
- package/dist/class-group/class-group.controller.d.ts.map +1 -1
- package/dist/class-group/class-group.service.d.ts +1 -0
- package/dist/class-group/class-group.service.d.ts.map +1 -1
- package/dist/course/course-ai-usage.service.d.ts +58 -0
- package/dist/course/course-ai-usage.service.d.ts.map +1 -0
- package/dist/course/course-ai-usage.service.js +176 -0
- package/dist/course/course-ai-usage.service.js.map +1 -0
- package/dist/course/course-audio-transcription.service.d.ts +65 -1
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +381 -29
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-export-scorm12.service.d.ts +3 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
- package/dist/course/course-export-scorm12.service.js +141 -6
- package/dist/course/course-export-scorm12.service.js.map +1 -1
- package/dist/course/course-export.service.d.ts.map +1 -1
- package/dist/course/course-export.service.js +2 -1
- package/dist/course/course-export.service.js.map +1 -1
- package/dist/course/course-lesson.controller.d.ts +25 -3
- package/dist/course/course-lesson.controller.d.ts.map +1 -1
- package/dist/course/course-lesson.controller.js +71 -8
- package/dist/course/course-lesson.controller.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +30 -7
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +37 -4
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +37 -5
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +165 -20
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-transcription-translation.service.d.ts +31 -0
- package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
- package/dist/course/course-transcription-translation.service.js +227 -0
- package/dist/course/course-transcription-translation.service.js.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.js +398 -0
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
- package/dist/course/course-video-hls.service.d.ts +14 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -1
- package/dist/course/course-video-hls.service.js +25 -8
- package/dist/course/course-video-hls.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +2 -0
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +9 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +2 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +36 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
- package/dist/course/dto/create-course-export.dto.d.ts +1 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-export.dto.js +6 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -1
- package/dist/course/ffmpeg.util.d.ts +10 -0
- package/dist/course/ffmpeg.util.d.ts.map +1 -0
- package/dist/course/ffmpeg.util.js +79 -0
- package/dist/course/ffmpeg.util.js.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +33 -16
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +3 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +48 -29
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/subtitle.util.d.ts +46 -0
- package/dist/course/subtitle.util.d.ts.map +1 -0
- package/dist/course/subtitle.util.js +206 -0
- package/dist/course/subtitle.util.js.map +1 -0
- package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +2 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +27 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +197 -10
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +14 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
- package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
- package/dist/platforma/dto/heartbeat.dto.js +50 -0
- package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
- package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.js +50 -0
- package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
- package/dist/platforma/platforma-performance.service.d.ts +121 -0
- package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
- package/dist/platforma/platforma-performance.service.js +500 -0
- package/dist/platforma/platforma-performance.service.js.map +1 -0
- package/dist/platforma/platforma-search.service.d.ts +21 -0
- package/dist/platforma/platforma-search.service.d.ts.map +1 -0
- package/dist/platforma/platforma-search.service.js +64 -0
- package/dist/platforma/platforma-search.service.js.map +1 -0
- package/dist/platforma/platforma-video.service.d.ts +8 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -1
- package/dist/platforma/platforma-video.service.js +45 -2
- package/dist/platforma/platforma-video.service.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +213 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +159 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.controller.d.ts +2 -0
- package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.controller.js +31 -0
- package/dist/realtime/lms-realtime.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.service.d.ts +1 -1
- package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.service.js.map +1 -1
- package/dist/training/dto/create-training.dto.d.ts +9 -0
- package/dist/training/dto/create-training.dto.d.ts.map +1 -1
- package/dist/training/dto/create-training.dto.js +45 -1
- package/dist/training/dto/create-training.dto.js.map +1 -1
- package/dist/training/training.controller.d.ts +144 -0
- package/dist/training/training.controller.d.ts.map +1 -1
- package/dist/training/training.service.d.ts +149 -0
- package/dist/training/training.service.d.ts.map +1 -1
- package/dist/training/training.service.js +332 -167
- package/dist/training/training.service.js.map +1 -1
- package/hedhog/data/image_type.yaml +10 -0
- package/hedhog/data/route.yaml +251 -0
- package/hedhog/data/setting_group.yaml +97 -0
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
- package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
- package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
- package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
- package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
- package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
- package/hedhog/frontend/app/courses/page.tsx.ejs +66 -13
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
- package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
- package/hedhog/frontend/app/paths/page.tsx.ejs +650 -168
- package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
- package/hedhog/frontend/messages/en.json +41 -12
- package/hedhog/frontend/messages/pt.json +44 -13
- package/hedhog/query/triggers.sql +33 -0
- package/hedhog/table/course_ai_usage.yaml +46 -0
- package/hedhog/table/course_enrollment.yaml +3 -0
- package/hedhog/table/course_lesson.yaml +3 -0
- package/hedhog/table/course_lesson_answer.yaml +37 -0
- package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
- package/hedhog/table/learning_path.yaml +6 -0
- package/hedhog/table/learning_path_module.yaml +22 -0
- package/hedhog/table/learning_path_step.yaml +9 -6
- package/hedhog/table/lesson_view_event.yaml +66 -0
- package/package.json +8 -7
- package/src/certificate/certificate.controller.ts +2 -0
- package/src/certificate/certificate.service.ts +99 -0
- package/src/course/course-ai-usage.service.ts +221 -0
- package/src/course/course-audio-transcription.service.ts +471 -43
- package/src/course/course-export-scorm12.service.ts +149 -5
- package/src/course/course-export.service.ts +1 -0
- package/src/course/course-lesson.controller.ts +59 -6
- package/src/course/course-structure.controller.ts +19 -1
- package/src/course/course-structure.service.ts +184 -10
- package/src/course/course-transcription-translation.service.ts +293 -0
- package/src/course/course-video-agent-pipeline.service.ts +471 -0
- package/src/course/course-video-hls.service.ts +30 -10
- package/src/course/course.module.ts +9 -0
- package/src/course/course.service.ts +46 -1
- package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
- package/src/course/dto/create-course-export.dto.ts +6 -0
- package/src/course/ffmpeg.util.ts +65 -0
- package/src/course/lms-bulk-upload-automation.service.ts +33 -8
- package/src/course/lms-bulk-upload.service.ts +20 -1
- package/src/course/subtitle.util.ts +220 -0
- package/src/enterprise/training/training-student.service.ts +224 -4
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
- package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
- package/src/lms.module.ts +14 -0
- package/src/platforma/dto/heartbeat.dto.ts +30 -0
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
- package/src/platforma/platforma-heartbeat.service.ts +33 -0
- package/src/platforma/platforma-performance.service.ts +606 -0
- package/src/platforma/platforma-search.service.ts +48 -0
- package/src/platforma/platforma-video.service.ts +59 -3
- package/src/platforma/platforma.controller.ts +130 -0
- package/src/realtime/lms-realtime.controller.ts +27 -1
- package/src/realtime/lms-realtime.service.ts +2 -1
- package/src/training/dto/create-training.dto.ts +36 -0
- package/src/training/training.service.ts +360 -163
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from '@/components/ui/dropdown-menu';
|
|
10
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
11
|
+
import type HlsType from 'hls.js';
|
|
12
|
+
import { ChevronDown, Loader2, Subtitles, VideoOff } from 'lucide-react';
|
|
13
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
14
|
+
import type { TranscriptionLocale } from '../_data/use-transcription-segments';
|
|
15
|
+
|
|
16
|
+
interface LessonVideoPreviewProps {
|
|
17
|
+
lessonId: string;
|
|
18
|
+
locales?: TranscriptionLocale[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function LessonVideoPreview({ lessonId, locales = [] }: LessonVideoPreviewProps) {
|
|
22
|
+
const { request } = useApp();
|
|
23
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
24
|
+
const hlsRef = useRef<HlsType | null>(null);
|
|
25
|
+
|
|
26
|
+
const [hlsToken, setHlsToken] = useState<string | null>(null);
|
|
27
|
+
const [loadError, setLoadError] = useState<string | null>(null);
|
|
28
|
+
const [selectedLocaleId, setSelectedLocaleId] = useState<number | null>(null);
|
|
29
|
+
const [subtitleLabel, setSubtitleLabel] = useState<string>('Desativada');
|
|
30
|
+
const [subtitleLoading, setSubtitleLoading] = useState(false);
|
|
31
|
+
|
|
32
|
+
const numericId = parseInt(lessonId, 10);
|
|
33
|
+
const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL ?? '';
|
|
34
|
+
|
|
35
|
+
// Filter out null-id locales (no locale assigned)
|
|
36
|
+
const availableLocales = locales.filter((l) => l.id !== null);
|
|
37
|
+
|
|
38
|
+
// Fetch HLS token on mount
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (isNaN(numericId)) return;
|
|
41
|
+
setHlsToken(null);
|
|
42
|
+
setLoadError(null);
|
|
43
|
+
setSelectedLocaleId(null);
|
|
44
|
+
setSubtitleLabel('Desativada');
|
|
45
|
+
request<{ token: string }>({
|
|
46
|
+
url: `/lms/lessons/${numericId}/preview/hls-token`,
|
|
47
|
+
method: 'GET',
|
|
48
|
+
})
|
|
49
|
+
.then((res) => setHlsToken(res.data.token))
|
|
50
|
+
.catch(() => setLoadError('Nenhum stream HLS disponível para esta aula.'));
|
|
51
|
+
}, [numericId]);
|
|
52
|
+
|
|
53
|
+
// Mount HLS player when token is ready
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const video = videoRef.current;
|
|
56
|
+
if (!video || !hlsToken) return;
|
|
57
|
+
|
|
58
|
+
const src = `${apiBase}/lms/platforma/hls/${hlsToken}/master.m3u8`;
|
|
59
|
+
|
|
60
|
+
let hls: HlsType | null = null;
|
|
61
|
+
|
|
62
|
+
import('hls.js').then(({ default: Hls }) => {
|
|
63
|
+
if (Hls.isSupported()) {
|
|
64
|
+
hls = new Hls({ enableWorker: false });
|
|
65
|
+
hls.loadSource(src);
|
|
66
|
+
hls.attachMedia(video);
|
|
67
|
+
hlsRef.current = hls;
|
|
68
|
+
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
|
69
|
+
video.src = src;
|
|
70
|
+
} else {
|
|
71
|
+
setLoadError('Este navegador não suporta reprodução HLS.');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return () => {
|
|
76
|
+
hls?.destroy();
|
|
77
|
+
hlsRef.current = null;
|
|
78
|
+
};
|
|
79
|
+
}, [hlsToken, apiBase]);
|
|
80
|
+
|
|
81
|
+
const applySubtitle = useCallback(
|
|
82
|
+
async (localeId: number | null, label: string) => {
|
|
83
|
+
const video = videoRef.current;
|
|
84
|
+
if (!video) return;
|
|
85
|
+
|
|
86
|
+
// Remove existing tracks
|
|
87
|
+
const existingTrack = video.querySelector('track');
|
|
88
|
+
if (existingTrack) existingTrack.remove();
|
|
89
|
+
|
|
90
|
+
setSelectedLocaleId(localeId);
|
|
91
|
+
setSubtitleLabel(label);
|
|
92
|
+
|
|
93
|
+
if (localeId === null) return;
|
|
94
|
+
|
|
95
|
+
setSubtitleLoading(true);
|
|
96
|
+
try {
|
|
97
|
+
const tokenRes = await request<{ token: string }>({
|
|
98
|
+
url: `/lms/lessons/${numericId}/preview/subtitles-token?locale_id=${localeId}`,
|
|
99
|
+
method: 'GET',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const vttUrl = `${apiBase}/lms/platforma/subtitles/${tokenRes.data.token}/captions.vtt`;
|
|
103
|
+
const vttRes = await fetch(vttUrl);
|
|
104
|
+
const vttText = await vttRes.text();
|
|
105
|
+
const blob = new Blob([vttText], { type: 'text/vtt' });
|
|
106
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
107
|
+
|
|
108
|
+
const track = document.createElement('track');
|
|
109
|
+
track.kind = 'subtitles';
|
|
110
|
+
track.src = blobUrl;
|
|
111
|
+
track.srclang = 'pt';
|
|
112
|
+
track.label = label;
|
|
113
|
+
track.default = true;
|
|
114
|
+
video.appendChild(track);
|
|
115
|
+
|
|
116
|
+
track.addEventListener(
|
|
117
|
+
'load',
|
|
118
|
+
() => {
|
|
119
|
+
if (video.textTracks[0]) {
|
|
120
|
+
video.textTracks[0].mode = 'showing';
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{ once: true },
|
|
124
|
+
);
|
|
125
|
+
} catch {
|
|
126
|
+
setSubtitleLabel('Desativada');
|
|
127
|
+
setSelectedLocaleId(null);
|
|
128
|
+
} finally {
|
|
129
|
+
setSubtitleLoading(false);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
[numericId, apiBase, request],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
if (loadError) {
|
|
136
|
+
return (
|
|
137
|
+
<div className="flex flex-col items-center gap-3 py-16 text-center">
|
|
138
|
+
<VideoOff className="size-10 text-muted-foreground/40" />
|
|
139
|
+
<p className="text-sm text-muted-foreground">{loadError}</p>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!hlsToken) {
|
|
145
|
+
return (
|
|
146
|
+
<div className="flex items-center justify-center py-16">
|
|
147
|
+
<Loader2 className="size-6 animate-spin text-muted-foreground" />
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
<div className="flex flex-col gap-3">
|
|
154
|
+
<div className="relative w-full overflow-hidden rounded-lg bg-black" style={{ aspectRatio: '16/9' }}>
|
|
155
|
+
<video
|
|
156
|
+
ref={videoRef}
|
|
157
|
+
controls
|
|
158
|
+
className="h-full w-full"
|
|
159
|
+
playsInline
|
|
160
|
+
/>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{availableLocales.length > 0 && (
|
|
164
|
+
<div className="flex items-center gap-2">
|
|
165
|
+
<Subtitles className="size-4 text-muted-foreground shrink-0" />
|
|
166
|
+
<span className="text-xs text-muted-foreground">Legendas:</span>
|
|
167
|
+
<DropdownMenu>
|
|
168
|
+
<DropdownMenuTrigger asChild>
|
|
169
|
+
<Button variant="outline" size="sm" className="h-7 gap-1 text-xs" disabled={subtitleLoading}>
|
|
170
|
+
{subtitleLoading ? (
|
|
171
|
+
<Loader2 className="size-3 animate-spin" />
|
|
172
|
+
) : (
|
|
173
|
+
subtitleLabel
|
|
174
|
+
)}
|
|
175
|
+
<ChevronDown className="size-3" />
|
|
176
|
+
</Button>
|
|
177
|
+
</DropdownMenuTrigger>
|
|
178
|
+
<DropdownMenuContent align="start">
|
|
179
|
+
<DropdownMenuItem
|
|
180
|
+
onClick={() => applySubtitle(null, 'Desativada')}
|
|
181
|
+
className={selectedLocaleId === null ? 'font-medium' : ''}
|
|
182
|
+
>
|
|
183
|
+
Desativada
|
|
184
|
+
</DropdownMenuItem>
|
|
185
|
+
{availableLocales.map((loc) => (
|
|
186
|
+
<DropdownMenuItem
|
|
187
|
+
key={loc.id}
|
|
188
|
+
onClick={() => applySubtitle(loc.id!, loc.name ?? loc.code ?? String(loc.id))}
|
|
189
|
+
className={selectedLocaleId === loc.id ? 'font-medium' : ''}
|
|
190
|
+
>
|
|
191
|
+
{loc.name ?? loc.code ?? `Idioma ${loc.id}`}
|
|
192
|
+
</DropdownMenuItem>
|
|
193
|
+
))}
|
|
194
|
+
</DropdownMenuContent>
|
|
195
|
+
</DropdownMenu>
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
@@ -185,6 +185,7 @@ export function TreeRowLesson({
|
|
|
185
185
|
const attachedResourceCount = getLessonAttachedResourceCount(data);
|
|
186
186
|
const uploadedVideoCount = getLessonUploadedVideoCount(data);
|
|
187
187
|
const hasTranscription =
|
|
188
|
+
Boolean(data.hasTranscription) ||
|
|
188
189
|
Boolean(data.transcription?.trim()) ||
|
|
189
190
|
Boolean(data.transcriptionSegments?.some((segment) => segment.text.trim()));
|
|
190
191
|
|
|
@@ -67,6 +67,7 @@ export interface LessonInstructor {
|
|
|
67
67
|
|
|
68
68
|
export interface TranscriptionSegment {
|
|
69
69
|
id: number;
|
|
70
|
+
localeId?: number | null;
|
|
70
71
|
startSeconds: number;
|
|
71
72
|
endSeconds: number;
|
|
72
73
|
text: string;
|
|
@@ -102,6 +103,8 @@ export interface Lesson {
|
|
|
102
103
|
autoDuration?: boolean;
|
|
103
104
|
transcription?: string;
|
|
104
105
|
transcriptionSegments?: TranscriptionSegment[];
|
|
106
|
+
/** Denormalized flag from the backend (course_lesson.has_transcription). */
|
|
107
|
+
hasTranscription?: boolean;
|
|
105
108
|
localeId?: number | null;
|
|
106
109
|
videoConversionJobId?: number;
|
|
107
110
|
// Questão
|
|
@@ -11,14 +11,7 @@ export function XpHighlightPill({
|
|
|
11
11
|
value: string;
|
|
12
12
|
tone: 'sky' | 'amber' | 'emerald' | 'violet';
|
|
13
13
|
}) {
|
|
14
|
-
const toneClassName =
|
|
15
|
-
tone === 'sky'
|
|
16
|
-
? 'border-sky-500/15 bg-sky-500/10 text-sky-700 dark:text-sky-300'
|
|
17
|
-
: tone === 'amber'
|
|
18
|
-
? 'border-amber-500/15 bg-amber-500/10 text-amber-700 dark:text-amber-300'
|
|
19
|
-
: tone === 'emerald'
|
|
20
|
-
? 'border-emerald-500/15 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
|
21
|
-
: 'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300';
|
|
14
|
+
const toneClassName = 'border-border/60 bg-muted/30 text-muted-foreground';
|
|
22
15
|
|
|
23
16
|
return (
|
|
24
17
|
<div
|
package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* ⚠️ KNOWN MISMATCHES (documented with inline comments):
|
|
16
16
|
* 1. exameVinculado (number) ↔ linkedExam (string) — converted with String()/Number()
|
|
17
17
|
* 2. Session.order — not returned by API; inferred from sorted array position.
|
|
18
|
-
* 3. Resource.
|
|
18
|
+
* 3. Resource.url — not returned by API; left as undefined.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import type {
|
|
@@ -69,6 +69,22 @@ function normalizeVideoProvider(
|
|
|
69
69
|
return undefined;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Format a byte count into a human-readable file size (B/KB/MB/GB).
|
|
74
|
+
* Returns an empty string when the size is unknown or zero.
|
|
75
|
+
*/
|
|
76
|
+
function formatFileSize(bytes?: number | null): string {
|
|
77
|
+
if (!bytes || bytes <= 0) return '';
|
|
78
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
79
|
+
const exponent = Math.min(
|
|
80
|
+
Math.floor(Math.log(bytes) / Math.log(1024)),
|
|
81
|
+
units.length - 1
|
|
82
|
+
);
|
|
83
|
+
const value = bytes / 1024 ** exponent;
|
|
84
|
+
const formatted = exponent === 0 ? String(value) : value.toFixed(1);
|
|
85
|
+
return `${formatted} ${units[exponent]}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
/** Normalize a raw API resource to the frontend Resource shape. */
|
|
73
89
|
function normalizeResource(raw: ApiLessonResource): Resource {
|
|
74
90
|
const withUpload = raw as ApiLessonResource & {
|
|
@@ -83,12 +99,7 @@ function normalizeResource(raw: ApiLessonResource): Resource {
|
|
|
83
99
|
fileId: raw.fileId ?? undefined,
|
|
84
100
|
name: raw.nome,
|
|
85
101
|
type: raw.type ?? '',
|
|
86
|
-
|
|
87
|
-
* ⚠️ BACKEND NOTE: file size is not returned in the API response.
|
|
88
|
-
* TODO[BACKEND]: Include `size` in the lesson resource response from
|
|
89
|
-
* CourseStructureService.getLessonById().
|
|
90
|
-
*/
|
|
91
|
-
size: '',
|
|
102
|
+
size: formatFileSize(raw.tamanho),
|
|
92
103
|
public: raw.is_public ?? true,
|
|
93
104
|
uploadedAt:
|
|
94
105
|
withUpload.uploadedAt ??
|
|
@@ -184,6 +195,7 @@ export function normalizeLesson(raw: ApiLesson, order = 0): Lesson {
|
|
|
184
195
|
videoUrl: raw.videoUrl,
|
|
185
196
|
autoDuration: raw.duracaoAutomatica,
|
|
186
197
|
transcription: raw.transcricao,
|
|
198
|
+
hasTranscription: Boolean(raw.temTranscricao),
|
|
187
199
|
localeId: raw.locale_id ?? null,
|
|
188
200
|
videoConversionJobId: raw.videoConversionJobId,
|
|
189
201
|
/**
|
|
@@ -21,6 +21,8 @@ export interface ApiLessonResource {
|
|
|
21
21
|
id: number;
|
|
22
22
|
nome: string;
|
|
23
23
|
fileId?: number | null;
|
|
24
|
+
/** File size in bytes (0 when unknown). */
|
|
25
|
+
tamanho?: number;
|
|
24
26
|
/** MIME category or custom type label; may be null. */
|
|
25
27
|
type?: string | null;
|
|
26
28
|
is_public?: boolean;
|
|
@@ -96,6 +98,8 @@ export interface ApiLesson {
|
|
|
96
98
|
videoUrl?: string;
|
|
97
99
|
duracaoAutomatica?: boolean;
|
|
98
100
|
transcricao?: string;
|
|
101
|
+
/** Whether the lesson already has transcription segments (denormalized flag). */
|
|
102
|
+
temTranscricao?: boolean;
|
|
99
103
|
videoConversionJobId?: number;
|
|
100
104
|
/**
|
|
101
105
|
* ID of the linked exam (when tipo === 'questao').
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
4
|
+
|
|
5
|
+
export type CourseAiCostByJobType = {
|
|
6
|
+
jobType: string;
|
|
7
|
+
costUsd: number;
|
|
8
|
+
runs: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type CourseAiCostByLesson = {
|
|
12
|
+
lessonId: number | null;
|
|
13
|
+
lessonTitle: string | null;
|
|
14
|
+
costUsd: number;
|
|
15
|
+
runs: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type CourseAiCosts = {
|
|
19
|
+
currency: string;
|
|
20
|
+
totalCostUsd: number;
|
|
21
|
+
byJobType: CourseAiCostByJobType[];
|
|
22
|
+
byLesson: CourseAiCostByLesson[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function useCourseAiCostsQuery(courseId: string | null) {
|
|
26
|
+
const { request } = useApp();
|
|
27
|
+
|
|
28
|
+
return useQuery<CourseAiCosts | null>({
|
|
29
|
+
queryKey: ['course-ai-costs', courseId],
|
|
30
|
+
enabled: Boolean(courseId),
|
|
31
|
+
queryFn: async () => {
|
|
32
|
+
const response = await request<CourseAiCosts>({
|
|
33
|
+
url: `/lms/courses/${courseId}/structure/ai-costs`,
|
|
34
|
+
method: 'GET',
|
|
35
|
+
});
|
|
36
|
+
return response.data ?? null;
|
|
37
|
+
},
|
|
38
|
+
initialData: null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
4
4
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
5
5
|
|
|
6
|
+
export type CourseTranscriptionLocale = {
|
|
7
|
+
id: number;
|
|
8
|
+
code: string;
|
|
9
|
+
name: string;
|
|
10
|
+
region: string | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function useCourseTranscriptionLocalesQuery(courseId: string | null) {
|
|
14
|
+
const { request } = useApp();
|
|
15
|
+
|
|
16
|
+
return useQuery<CourseTranscriptionLocale[]>({
|
|
17
|
+
queryKey: ['course-transcription-locales', courseId],
|
|
18
|
+
enabled: Boolean(courseId),
|
|
19
|
+
queryFn: async () => {
|
|
20
|
+
const res = await request<CourseTranscriptionLocale[]>({
|
|
21
|
+
url: `/lms/courses/${courseId}/structure/transcription-locales`,
|
|
22
|
+
method: 'GET',
|
|
23
|
+
});
|
|
24
|
+
return res.data ?? [];
|
|
25
|
+
},
|
|
26
|
+
initialData: [],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
export type ScormVisualSettings = {
|
|
7
31
|
primaryColor?: string;
|
|
8
32
|
fontFamily?: string;
|
|
@@ -66,6 +90,7 @@ export function useCreateCourseExportMutation(courseId: string | null) {
|
|
|
66
90
|
mutationFn: async (payload: {
|
|
67
91
|
format: 'scorm_1_2';
|
|
68
92
|
visualSettings?: ScormVisualSettings;
|
|
93
|
+
subtitleLocaleIds?: number[];
|
|
69
94
|
}) => {
|
|
70
95
|
const res = await request<{
|
|
71
96
|
exportId: number;
|
package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs
CHANGED
|
@@ -29,6 +29,7 @@ import { useApp } from '@hed-hog/next-app-provider';
|
|
|
29
29
|
import { useStructureStore } from '../_components/store';
|
|
30
30
|
import type {
|
|
31
31
|
LessonFormValues,
|
|
32
|
+
LessonStatus,
|
|
32
33
|
Resource,
|
|
33
34
|
SessionFormValues,
|
|
34
35
|
} from '../_components/types';
|
|
@@ -605,6 +606,153 @@ export function useBulkDeleteMutation() {
|
|
|
605
606
|
});
|
|
606
607
|
}
|
|
607
608
|
|
|
609
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
610
|
+
// useBulkUpdateMutation
|
|
611
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
interface BulkUpdateVars {
|
|
614
|
+
/** Lessons to update (publish state and/or production status). */
|
|
615
|
+
lessons: Array<{ lessonId: string; sessionId: string }>;
|
|
616
|
+
/** Sessions to update (publish state only). */
|
|
617
|
+
sessionIds: Array<{ sessionId: string }>;
|
|
618
|
+
/** Field changes to apply to every selected item. */
|
|
619
|
+
changes: { published?: boolean; status?: LessonStatus };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Bulk field update: iterate PATCH calls since no batch endpoint exists.
|
|
624
|
+
*
|
|
625
|
+
* Strategy mirrors `useBulkDeleteMutation`:
|
|
626
|
+
* 1. PATCH each lesson with the requested changes (published / production status).
|
|
627
|
+
* When publishing (`published === true`), send `confirmarPublicacaoComStatus`
|
|
628
|
+
* so the backend doesn't reject lessons whose status isn't `publicada`.
|
|
629
|
+
* 2. PATCH each session (only `published` applies to sessions).
|
|
630
|
+
* 3. Use Promise.allSettled so one failure doesn't abort the rest.
|
|
631
|
+
* 4. Apply successful items to the store + cache; show partial-error toast.
|
|
632
|
+
*/
|
|
633
|
+
export function useBulkUpdateMutation() {
|
|
634
|
+
const t = useTranslations('lms.courseStructure');
|
|
635
|
+
const { request } = useApp();
|
|
636
|
+
const queryClient = useQueryClient();
|
|
637
|
+
const courseId = useStructureStore((s) => s.courseId);
|
|
638
|
+
const updateLessonInStore = useStructureStore((s) => s.updateLesson);
|
|
639
|
+
const updateSessionInStore = useStructureStore((s) => s.updateSession);
|
|
640
|
+
|
|
641
|
+
return useMutation({
|
|
642
|
+
mutationFn: async ({ lessons, sessionIds, changes }: BulkUpdateVars) => {
|
|
643
|
+
const lessonPayload = toUpdateLessonPayload({
|
|
644
|
+
...(changes.published !== undefined && { published: changes.published }),
|
|
645
|
+
...(changes.status !== undefined && { status: changes.status }),
|
|
646
|
+
...(changes.published === true && {
|
|
647
|
+
confirmarPublicacaoComStatus: true,
|
|
648
|
+
}),
|
|
649
|
+
});
|
|
650
|
+
const lessonResults = await Promise.allSettled(
|
|
651
|
+
lessons.map(({ lessonId, sessionId }) =>
|
|
652
|
+
apiUpdateLesson(request, courseId, sessionId, lessonId, lessonPayload)
|
|
653
|
+
)
|
|
654
|
+
);
|
|
655
|
+
// Sessions only carry a `published` flag.
|
|
656
|
+
const sessionPayload = toUpdateSessionPayload(
|
|
657
|
+
changes.published !== undefined ? { published: changes.published } : {}
|
|
658
|
+
);
|
|
659
|
+
const sessionResults =
|
|
660
|
+
changes.published !== undefined
|
|
661
|
+
? await Promise.allSettled(
|
|
662
|
+
sessionIds.map(({ sessionId }) =>
|
|
663
|
+
apiUpdateSession(request, courseId, sessionId, sessionPayload)
|
|
664
|
+
)
|
|
665
|
+
)
|
|
666
|
+
: [];
|
|
667
|
+
return {
|
|
668
|
+
lessons,
|
|
669
|
+
sessionIds,
|
|
670
|
+
changes,
|
|
671
|
+
lessonResults,
|
|
672
|
+
sessionResults,
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
onSuccess: ({
|
|
676
|
+
lessons,
|
|
677
|
+
sessionIds,
|
|
678
|
+
changes,
|
|
679
|
+
lessonResults,
|
|
680
|
+
sessionResults,
|
|
681
|
+
}) => {
|
|
682
|
+
const successLessonIds = lessons
|
|
683
|
+
.filter((_, i) => lessonResults[i]?.status === 'fulfilled')
|
|
684
|
+
.map((l) => l.lessonId);
|
|
685
|
+
const successSessionIds = sessionIds
|
|
686
|
+
.filter((_, i) => sessionResults[i]?.status === 'fulfilled')
|
|
687
|
+
.map((s) => s.sessionId);
|
|
688
|
+
const errorCount = [...lessonResults, ...sessionResults].filter(
|
|
689
|
+
(r) => r.status === 'rejected'
|
|
690
|
+
).length;
|
|
691
|
+
|
|
692
|
+
const lessonPatch: Partial<LessonFormValues> = {
|
|
693
|
+
...(changes.published !== undefined && { published: changes.published }),
|
|
694
|
+
// Publishing forces production status to `publicada` on the backend.
|
|
695
|
+
...(changes.published === true
|
|
696
|
+
? { status: 'publicada' as LessonStatus }
|
|
697
|
+
: changes.status !== undefined && { status: changes.status }),
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
successLessonIds.forEach((id) => updateLessonInStore(id, lessonPatch));
|
|
701
|
+
if (changes.published !== undefined) {
|
|
702
|
+
successSessionIds.forEach((id) =>
|
|
703
|
+
updateSessionInStore(id, {
|
|
704
|
+
published: changes.published,
|
|
705
|
+
} as SessionFormValues)
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Patch the structure cache so the tree reflects changes immediately.
|
|
710
|
+
const lessonSet = new Set(successLessonIds);
|
|
711
|
+
const sessionSet = new Set(successSessionIds);
|
|
712
|
+
queryClient.setQueryData<CourseStructureCacheData>(
|
|
713
|
+
courseStructureQueryKey(courseId),
|
|
714
|
+
(old) => {
|
|
715
|
+
if (!old) return old;
|
|
716
|
+
return {
|
|
717
|
+
...old,
|
|
718
|
+
lessons: old.lessons.map((l) =>
|
|
719
|
+
lessonSet.has(l.id) ? { ...l, ...lessonPatch } : l
|
|
720
|
+
),
|
|
721
|
+
sessions:
|
|
722
|
+
changes.published === undefined
|
|
723
|
+
? old.sessions
|
|
724
|
+
: old.sessions.map((s) =>
|
|
725
|
+
sessionSet.has(s.id)
|
|
726
|
+
? { ...s, published: changes.published! }
|
|
727
|
+
: s
|
|
728
|
+
),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
void queryClient.invalidateQueries({
|
|
733
|
+
queryKey: courseStructureQueryKey(courseId),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
const successCount = successLessonIds.length + successSessionIds.length;
|
|
737
|
+
if (errorCount === 0) {
|
|
738
|
+
toast.success(
|
|
739
|
+
`${successCount} ${successCount === 1 ? 'item atualizado' : 'itens atualizados'}`
|
|
740
|
+
);
|
|
741
|
+
} else {
|
|
742
|
+
toast.error(
|
|
743
|
+
`${errorCount} falha${errorCount > 1 ? 's' : ''} — ${successCount} atualizado${successCount !== 1 ? 's' : ''}`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
onError: () => {
|
|
748
|
+
void queryClient.invalidateQueries({
|
|
749
|
+
queryKey: courseStructureQueryKey(courseId),
|
|
750
|
+
});
|
|
751
|
+
toast.error(t('mutations.lesson.saveError'));
|
|
752
|
+
},
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
608
756
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
609
757
|
// useReorderSessionsMutation
|
|
610
758
|
// ─────────────────────────────────────────────────────────────────────────────
|