@hed-hog/lms 0.0.366 → 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/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 +26 -5
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +31 -1
- 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.js +7 -7
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +4 -0
- package/dist/course/course.module.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/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +26 -13
- 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-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 +4 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma-performance.service.js +121 -121
- 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 +99 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +111 -2
- package/dist/platforma/platforma.controller.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/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/detail-course.tsx.ejs +69 -1
- 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/_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-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 +26 -4
- package/hedhog/frontend/app/paths/page.tsx.ejs +612 -164
- package/hedhog/frontend/messages/en.json +23 -12
- package/hedhog/frontend/messages/pt.json +23 -12
- package/hedhog/query/triggers.sql +33 -0
- package/hedhog/table/course_ai_usage.yaml +46 -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 -66
- package/package.json +9 -9
- 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 +16 -0
- 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 -471
- package/src/course/course.module.ts +4 -0
- 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 -65
- package/src/course/lms-bulk-upload-automation.service.ts +29 -7
- 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 +4 -0
- package/src/platforma/dto/heartbeat.dto.ts +30 -30
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -117
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -343
- package/src/platforma/platforma-heartbeat.service.ts +33 -33
- package/src/platforma/platforma-performance.service.ts +606 -606
- package/src/platforma/platforma-search.service.ts +48 -48
- package/src/platforma/platforma-video.service.ts +59 -3
- package/src/platforma/platforma.controller.ts +88 -0
- package/src/training/dto/create-training.dto.ts +36 -0
- package/src/training/training.service.ts +360 -163
|
@@ -2,28 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
import { Badge } from '@/components/ui/badge';
|
|
4
4
|
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
5
6
|
import { Separator } from '@/components/ui/separator';
|
|
6
7
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
7
8
|
import { cn } from '@/lib/utils';
|
|
9
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
8
10
|
import { useQueryClient } from '@tanstack/react-query';
|
|
9
11
|
import {
|
|
10
12
|
Clock,
|
|
11
13
|
ExternalLink,
|
|
12
14
|
FileText,
|
|
13
15
|
HelpCircle,
|
|
16
|
+
Languages,
|
|
14
17
|
Loader2,
|
|
15
18
|
Mic,
|
|
16
19
|
Paperclip,
|
|
20
|
+
Plus,
|
|
21
|
+
Trash2,
|
|
17
22
|
Video,
|
|
23
|
+
X,
|
|
18
24
|
type LucideIcon,
|
|
19
25
|
} from 'lucide-react';
|
|
26
|
+
import Link from 'next/link';
|
|
20
27
|
import { useEffect, useState } from 'react';
|
|
21
28
|
import { toast } from 'sonner';
|
|
22
29
|
import { useLmsSettingsQuery } from '../_data/use-lms-settings-query';
|
|
23
30
|
import {
|
|
31
|
+
useDeleteTranscriptionLocaleMutation,
|
|
24
32
|
useStartTranscriptionMutation,
|
|
33
|
+
useTranscriptionLocalesQuery,
|
|
25
34
|
useTranscriptionSegmentsQuery,
|
|
35
|
+
useTranslateTranscriptionMutation,
|
|
36
|
+
type TranscriptionLocale,
|
|
26
37
|
} from '../_data/use-transcription-segments';
|
|
38
|
+
import { LessonVideoPreview } from './lesson-video-preview';
|
|
27
39
|
import { LessonXpTab } from './detail-lesson-xp-tab';
|
|
28
40
|
import { useStructureStore } from './store';
|
|
29
41
|
import type { LessonType } from './types';
|
|
@@ -69,7 +81,15 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
69
81
|
s.lessons.find((l) => l.id === lessonId)
|
|
70
82
|
);
|
|
71
83
|
const [activeTab, setActiveTab] = useState('dados');
|
|
84
|
+
const [selectedLocaleId, setSelectedLocaleId] = useState<number | null | undefined>(undefined);
|
|
85
|
+
const [translateOpen, setTranslateOpen] = useState(false);
|
|
86
|
+
const [targetLocaleId, setTargetLocaleId] = useState<number | null>(null);
|
|
87
|
+
const [activeTranslationJob, setActiveTranslationJob] = useState<{
|
|
88
|
+
queueJobId: number;
|
|
89
|
+
targetLocaleName: string;
|
|
90
|
+
} | null>(null);
|
|
72
91
|
const lmsSettings = useLmsSettingsQuery();
|
|
92
|
+
const { request } = useApp();
|
|
73
93
|
|
|
74
94
|
useEffect(() => {
|
|
75
95
|
if (!lmsSettings.transcriptionEnabled && activeTab === 'transcription') {
|
|
@@ -77,16 +97,77 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
77
97
|
}
|
|
78
98
|
}, [lmsSettings.transcriptionEnabled, activeTab]);
|
|
79
99
|
|
|
100
|
+
const isTranscriptionActive = activeTab === 'transcription' || activeTab === 'xp';
|
|
101
|
+
const activeLessonId = isTranscriptionActive ? (lesson?.id ?? null) : null;
|
|
102
|
+
|
|
103
|
+
const { data: transcriptionLocales = [], refetch: refetchLocales } =
|
|
104
|
+
useTranscriptionLocalesQuery(activeLessonId);
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (transcriptionLocales.length > 0 && selectedLocaleId === undefined) {
|
|
108
|
+
setSelectedLocaleId(transcriptionLocales[0]?.id ?? null);
|
|
109
|
+
}
|
|
110
|
+
if (transcriptionLocales.length === 0 && selectedLocaleId !== undefined) {
|
|
111
|
+
setSelectedLocaleId(undefined);
|
|
112
|
+
}
|
|
113
|
+
}, [transcriptionLocales, selectedLocaleId]);
|
|
114
|
+
|
|
80
115
|
const { data: segments = [], isLoading: segmentsLoading } =
|
|
81
|
-
useTranscriptionSegmentsQuery(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
)
|
|
116
|
+
useTranscriptionSegmentsQuery(activeLessonId, selectedLocaleId);
|
|
117
|
+
|
|
118
|
+
const { data: allLocales = [] } = useQuery<TranscriptionLocale[]>({
|
|
119
|
+
queryKey: ['all-locales'],
|
|
120
|
+
queryFn: async () => {
|
|
121
|
+
const res = await request<{ data: TranscriptionLocale[] }>({
|
|
122
|
+
url: '/core/locales?limit=200',
|
|
123
|
+
method: 'GET',
|
|
124
|
+
});
|
|
125
|
+
return (res.data?.data ?? []) as TranscriptionLocale[];
|
|
126
|
+
},
|
|
127
|
+
enabled: translateOpen,
|
|
128
|
+
initialData: [],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const availableTargetLocales = allLocales.filter(
|
|
132
|
+
(l) => l.id !== null && !transcriptionLocales.some((tl) => tl.id === l.id),
|
|
133
|
+
);
|
|
86
134
|
|
|
87
135
|
const queryClient = useQueryClient();
|
|
136
|
+
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
88
137
|
const { mutate: startTranscription, isPending: isStarting } =
|
|
89
138
|
useStartTranscriptionMutation(lesson?.id ?? null);
|
|
139
|
+
const { mutate: translateTranscription, isPending: isTranslating } =
|
|
140
|
+
useTranslateTranscriptionMutation(lesson?.id ?? null);
|
|
141
|
+
const { mutate: deleteTranscriptionLocale } =
|
|
142
|
+
useDeleteTranscriptionLocaleMutation(lesson?.id ?? null);
|
|
143
|
+
|
|
144
|
+
const handleDeleteLocale = (loc: TranscriptionLocale) => {
|
|
145
|
+
const name = loc.name ?? loc.code ?? 'este idioma';
|
|
146
|
+
showConfirm({
|
|
147
|
+
title: 'Excluir transcrição',
|
|
148
|
+
description: `Excluir a transcrição/tradução em "${name}" desta aula? Esta ação não pode ser desfeita.`,
|
|
149
|
+
confirmText: 'Excluir',
|
|
150
|
+
destructive: true,
|
|
151
|
+
onConfirm: () => {
|
|
152
|
+
deleteTranscriptionLocale(loc.id, {
|
|
153
|
+
onSuccess: () => {
|
|
154
|
+
toast.success('Transcrição excluída.');
|
|
155
|
+
if (selectedLocaleId === loc.id) setSelectedLocaleId(undefined);
|
|
156
|
+
refetchLocales();
|
|
157
|
+
queryClient.invalidateQueries({
|
|
158
|
+
queryKey: ['lesson-transcription-locales', lesson?.id],
|
|
159
|
+
});
|
|
160
|
+
queryClient.invalidateQueries({
|
|
161
|
+
queryKey: ['lesson-transcription-segments', lesson?.id],
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
onError: (err) => {
|
|
165
|
+
toast.error(err.message || 'Erro ao excluir a transcrição.');
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
};
|
|
90
171
|
|
|
91
172
|
const handleStartTranscription = () => {
|
|
92
173
|
startTranscription({
|
|
@@ -95,6 +176,9 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
95
176
|
queryClient.invalidateQueries({
|
|
96
177
|
queryKey: ['lesson-transcription-segments', lesson?.id],
|
|
97
178
|
});
|
|
179
|
+
queryClient.invalidateQueries({
|
|
180
|
+
queryKey: ['lesson-transcription-locales', lesson?.id],
|
|
181
|
+
});
|
|
98
182
|
},
|
|
99
183
|
onError: (err) => {
|
|
100
184
|
toast.error(err.message || 'Erro ao iniciar a transcrição.');
|
|
@@ -102,6 +186,32 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
102
186
|
});
|
|
103
187
|
};
|
|
104
188
|
|
|
189
|
+
const handleTranslate = () => {
|
|
190
|
+
if (!targetLocaleId) return;
|
|
191
|
+
const targetName =
|
|
192
|
+
availableTargetLocales.find((l) => l.id === targetLocaleId)?.name ?? String(targetLocaleId);
|
|
193
|
+
translateTranscription(targetLocaleId, selectedLocaleId ?? null, {
|
|
194
|
+
onSuccess: (result) => {
|
|
195
|
+
toast.success('Tradução iniciada! Acompanhe o progresso na fila.');
|
|
196
|
+
setTranslateOpen(false);
|
|
197
|
+
setTargetLocaleId(null);
|
|
198
|
+
setActiveTranslationJob({ queueJobId: result.queueJobId, targetLocaleName: targetName });
|
|
199
|
+
setTimeout(() => {
|
|
200
|
+
refetchLocales();
|
|
201
|
+
queryClient.invalidateQueries({
|
|
202
|
+
queryKey: ['lesson-transcription-segments', lesson?.id],
|
|
203
|
+
});
|
|
204
|
+
queryClient.invalidateQueries({
|
|
205
|
+
queryKey: ['lesson-transcription-locales', lesson?.id],
|
|
206
|
+
});
|
|
207
|
+
}, 2000);
|
|
208
|
+
},
|
|
209
|
+
onError: (err) => {
|
|
210
|
+
toast.error(err.message || 'Erro ao iniciar a tradução.');
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
|
|
105
215
|
if (!lesson) return null;
|
|
106
216
|
|
|
107
217
|
const cfg = LESSON_TYPE_CONFIG[lesson.type];
|
|
@@ -142,6 +252,9 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
142
252
|
>
|
|
143
253
|
<TabsList className="mx-4 mt-3 w-auto justify-start shrink-0">
|
|
144
254
|
<TabsTrigger value="dados">Dados</TabsTrigger>
|
|
255
|
+
{lesson.type === 'video' && (
|
|
256
|
+
<TabsTrigger value="conteudo">Conteúdo</TabsTrigger>
|
|
257
|
+
)}
|
|
145
258
|
{lmsSettings.transcriptionEnabled && (
|
|
146
259
|
<TabsTrigger value="transcription">Transcrição</TabsTrigger>
|
|
147
260
|
)}
|
|
@@ -243,27 +356,160 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
243
356
|
</div>
|
|
244
357
|
</TabsContent>
|
|
245
358
|
|
|
359
|
+
{/* ── Conteúdo (player de vídeo) ───────────────────────────────────── */}
|
|
360
|
+
{lesson.type === 'video' && (
|
|
361
|
+
<TabsContent
|
|
362
|
+
value="conteudo"
|
|
363
|
+
className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
|
|
364
|
+
>
|
|
365
|
+
{activeTab === 'conteudo' && (
|
|
366
|
+
<LessonVideoPreview lessonId={lesson.id} />
|
|
367
|
+
)}
|
|
368
|
+
</TabsContent>
|
|
369
|
+
)}
|
|
370
|
+
|
|
246
371
|
{/* ── Transcrição ───────────────────────────────────────────────────── */}
|
|
247
372
|
{lmsSettings.transcriptionEnabled && (
|
|
248
373
|
<TabsContent
|
|
249
374
|
value="transcription"
|
|
250
375
|
className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
|
|
251
376
|
>
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
377
|
+
{/* Banner de job de tradução ativo */}
|
|
378
|
+
{activeTranslationJob && (
|
|
379
|
+
<div className="flex items-center justify-between gap-2 mb-3 rounded-md border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30 px-3 py-2 text-sm">
|
|
380
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
381
|
+
<Loader2 className="size-4 shrink-0 animate-spin text-amber-600 dark:text-amber-400" />
|
|
382
|
+
<span className="text-amber-900 dark:text-amber-200 truncate">
|
|
383
|
+
Tradução para{' '}
|
|
384
|
+
<strong>{activeTranslationJob.targetLocaleName}</strong> em andamento
|
|
385
|
+
{' '}· Job #{activeTranslationJob.queueJobId}
|
|
386
|
+
</span>
|
|
387
|
+
</div>
|
|
388
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
389
|
+
<Link
|
|
390
|
+
href="/queue/jobs"
|
|
391
|
+
className="text-xs text-amber-700 dark:text-amber-300 underline underline-offset-2 hover:text-amber-900 dark:hover:text-amber-100"
|
|
392
|
+
>
|
|
393
|
+
Ver na fila →
|
|
394
|
+
</Link>
|
|
395
|
+
<button
|
|
396
|
+
onClick={() => setActiveTranslationJob(null)}
|
|
397
|
+
className="text-amber-600 dark:text-amber-400 hover:text-amber-900 dark:hover:text-amber-100"
|
|
398
|
+
>
|
|
399
|
+
<X className="size-3.5" />
|
|
400
|
+
</button>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
)}
|
|
404
|
+
|
|
405
|
+
{/* Toolbar */}
|
|
406
|
+
<div className="flex items-center justify-between gap-2 mb-3 flex-wrap">
|
|
407
|
+
{/* Locale pills */}
|
|
408
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
409
|
+
{transcriptionLocales.map((loc) => (
|
|
410
|
+
<div
|
|
411
|
+
key={loc.id ?? 'null'}
|
|
412
|
+
className={cn(
|
|
413
|
+
'flex items-center gap-1 rounded-full pl-2.5 pr-1 py-0.5 text-xs font-medium border transition-colors',
|
|
414
|
+
selectedLocaleId === loc.id
|
|
415
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
416
|
+
: 'bg-muted text-muted-foreground border-border hover:border-primary/50',
|
|
417
|
+
)}
|
|
418
|
+
>
|
|
419
|
+
<button
|
|
420
|
+
onClick={() => setSelectedLocaleId(loc.id)}
|
|
421
|
+
className="flex items-center gap-1"
|
|
422
|
+
>
|
|
423
|
+
<Languages className="size-3" />
|
|
424
|
+
{loc.name ?? loc.code ?? 'Desconhecido'}
|
|
425
|
+
</button>
|
|
426
|
+
<button
|
|
427
|
+
onClick={() => handleDeleteLocale(loc)}
|
|
428
|
+
title="Excluir transcrição deste idioma"
|
|
429
|
+
className={cn(
|
|
430
|
+
'rounded-full p-0.5 transition-colors',
|
|
431
|
+
selectedLocaleId === loc.id
|
|
432
|
+
? 'hover:bg-primary-foreground/20'
|
|
433
|
+
: 'hover:bg-foreground/10',
|
|
434
|
+
)}
|
|
435
|
+
>
|
|
436
|
+
<Trash2 className="size-3" />
|
|
437
|
+
</button>
|
|
438
|
+
</div>
|
|
439
|
+
))}
|
|
440
|
+
</div>
|
|
441
|
+
|
|
442
|
+
{/* Action buttons */}
|
|
443
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
444
|
+
{transcriptionLocales.length > 0 && (
|
|
445
|
+
<Popover open={translateOpen} onOpenChange={setTranslateOpen}>
|
|
446
|
+
<PopoverTrigger asChild>
|
|
447
|
+
<Button size="sm" variant="outline" disabled={isTranslating}>
|
|
448
|
+
{isTranslating ? (
|
|
449
|
+
<Loader2 className="size-4 mr-2 animate-spin" />
|
|
450
|
+
) : (
|
|
451
|
+
<Plus className="size-4 mr-2" />
|
|
452
|
+
)}
|
|
453
|
+
Traduzir
|
|
454
|
+
</Button>
|
|
455
|
+
</PopoverTrigger>
|
|
456
|
+
<PopoverContent className="w-64 p-3" align="end">
|
|
457
|
+
<p className="text-xs font-medium mb-2 text-muted-foreground">
|
|
458
|
+
Traduzir para:
|
|
459
|
+
</p>
|
|
460
|
+
{availableTargetLocales.length === 0 ? (
|
|
461
|
+
<p className="text-xs text-muted-foreground">
|
|
462
|
+
Todos os idiomas disponíveis já foram traduzidos.
|
|
463
|
+
</p>
|
|
464
|
+
) : (
|
|
465
|
+
<div className="flex flex-col gap-1">
|
|
466
|
+
{availableTargetLocales.map((loc) => (
|
|
467
|
+
<button
|
|
468
|
+
key={loc.id}
|
|
469
|
+
onClick={() => setTargetLocaleId(loc.id)}
|
|
470
|
+
className={cn(
|
|
471
|
+
'flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-left transition-colors',
|
|
472
|
+
targetLocaleId === loc.id
|
|
473
|
+
? 'bg-primary text-primary-foreground'
|
|
474
|
+
: 'hover:bg-muted',
|
|
475
|
+
)}
|
|
476
|
+
>
|
|
477
|
+
{loc.name ?? loc.code}
|
|
478
|
+
</button>
|
|
479
|
+
))}
|
|
480
|
+
<Button
|
|
481
|
+
size="sm"
|
|
482
|
+
className="mt-2 w-full"
|
|
483
|
+
disabled={!targetLocaleId || isTranslating}
|
|
484
|
+
onClick={handleTranslate}
|
|
485
|
+
>
|
|
486
|
+
{isTranslating && (
|
|
487
|
+
<Loader2 className="size-4 mr-2 animate-spin" />
|
|
488
|
+
)}
|
|
489
|
+
Confirmar tradução
|
|
490
|
+
</Button>
|
|
491
|
+
</div>
|
|
492
|
+
)}
|
|
493
|
+
</PopoverContent>
|
|
494
|
+
</Popover>
|
|
263
495
|
)}
|
|
264
|
-
|
|
265
|
-
|
|
496
|
+
<Button
|
|
497
|
+
size="sm"
|
|
498
|
+
variant="outline"
|
|
499
|
+
onClick={handleStartTranscription}
|
|
500
|
+
disabled={isStarting}
|
|
501
|
+
>
|
|
502
|
+
{isStarting ? (
|
|
503
|
+
<Loader2 className="size-4 mr-2 animate-spin" />
|
|
504
|
+
) : (
|
|
505
|
+
<Mic className="size-4 mr-2" />
|
|
506
|
+
)}
|
|
507
|
+
Iniciar transcrição
|
|
508
|
+
</Button>
|
|
509
|
+
</div>
|
|
266
510
|
</div>
|
|
511
|
+
|
|
512
|
+
{/* Segment list */}
|
|
267
513
|
{segmentsLoading ? (
|
|
268
514
|
<div className="p-4 text-sm text-muted-foreground">
|
|
269
515
|
Carregando transcrição...
|
|
@@ -284,7 +530,9 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
284
530
|
<EmptyState
|
|
285
531
|
icon={<FileText className="size-8 text-muted-foreground/40" />}
|
|
286
532
|
>
|
|
287
|
-
|
|
533
|
+
{transcriptionLocales.length === 0
|
|
534
|
+
? 'Transcrição não disponível para esta aula.'
|
|
535
|
+
: 'Nenhum segmento encontrado para este idioma.'}
|
|
288
536
|
</EmptyState>
|
|
289
537
|
)}
|
|
290
538
|
</TabsContent>
|
|
@@ -1,15 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
Eye,
|
|
5
|
-
EyeOff,
|
|
6
|
-
FolderInput,
|
|
7
|
-
Layers,
|
|
8
|
-
Lock,
|
|
9
|
-
Save,
|
|
10
|
-
Video,
|
|
11
|
-
X,
|
|
12
|
-
} from 'lucide-react';
|
|
3
|
+
import { FolderInput, Layers, Loader2, Save, Video, X } from 'lucide-react';
|
|
13
4
|
import { useTranslations } from 'next-intl';
|
|
14
5
|
import { useState } from 'react';
|
|
15
6
|
import { toast } from 'sonner';
|
|
@@ -28,19 +19,13 @@ import {
|
|
|
28
19
|
import { Separator } from '@/components/ui/separator';
|
|
29
20
|
import { cn } from '@/lib/utils';
|
|
30
21
|
|
|
22
|
+
import {
|
|
23
|
+
useBulkUpdateMutation,
|
|
24
|
+
useMoveLessonsMutation,
|
|
25
|
+
} from '../_data/use-course-structure-mutations';
|
|
31
26
|
import { IconActionTooltip } from './icon-action-tooltip';
|
|
32
27
|
import { useStructureStore } from './store';
|
|
33
|
-
import type {
|
|
34
|
-
|
|
35
|
-
// ── Config ────────────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
const STATUS_LABELS: Record<LessonStatus, string> = {
|
|
38
|
-
preparada: 'Preparada',
|
|
39
|
-
gravada: 'Gravada',
|
|
40
|
-
editada: 'Editada',
|
|
41
|
-
finalizada: 'Finalizada',
|
|
42
|
-
publicada: 'Publicada',
|
|
43
|
-
};
|
|
28
|
+
import type { Lesson, LessonStatus } from './types';
|
|
44
29
|
|
|
45
30
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
46
31
|
|
|
@@ -51,6 +36,9 @@ export function EditorBulk() {
|
|
|
51
36
|
const lessons = useStructureStore((s) => s.lessons);
|
|
52
37
|
const clearSelection = useStructureStore((s) => s.clearSelection);
|
|
53
38
|
|
|
39
|
+
const bulkUpdate = useBulkUpdateMutation();
|
|
40
|
+
const moveLessonsMutation = useMoveLessonsMutation();
|
|
41
|
+
|
|
54
42
|
const selectedArray = [...selectedIds];
|
|
55
43
|
const lessonIds = selectedArray.filter((id) =>
|
|
56
44
|
lessons.some((l) => l.id === id)
|
|
@@ -62,24 +50,68 @@ export function EditorBulk() {
|
|
|
62
50
|
const allLessons = lessonIds.length === selectedArray.length;
|
|
63
51
|
const allSessions = sessionIds.length === selectedArray.length;
|
|
64
52
|
|
|
65
|
-
// Local form state
|
|
53
|
+
// Local form state — empty means "keep current" (no change applied).
|
|
66
54
|
const [status, setStatus] = useState<LessonStatus | ''>('');
|
|
67
|
-
const [
|
|
55
|
+
const [publish, setPublish] = useState<'publish' | 'unpublish' | ''>('');
|
|
68
56
|
const [targetSession, setTargetSession] = useState<string>('');
|
|
69
57
|
|
|
58
|
+
const isPending = bulkUpdate.isPending || moveLessonsMutation.isPending;
|
|
59
|
+
|
|
60
|
+
function resetForm() {
|
|
61
|
+
setStatus('');
|
|
62
|
+
setPublish('');
|
|
63
|
+
setTargetSession('');
|
|
64
|
+
}
|
|
65
|
+
|
|
70
66
|
function handleSave() {
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
const changes: { published?: boolean; status?: LessonStatus } = {};
|
|
68
|
+
if (publish) changes.published = publish === 'publish';
|
|
69
|
+
// Production status only applies to lessons.
|
|
70
|
+
if (status && lessonIds.length > 0) changes.status = status;
|
|
71
|
+
|
|
72
|
+
const hasFieldChange =
|
|
73
|
+
changes.published !== undefined || changes.status !== undefined;
|
|
74
|
+
const hasMove = !!targetSession && lessonIds.length > 0;
|
|
75
|
+
|
|
76
|
+
if (!hasFieldChange && !hasMove) {
|
|
77
|
+
toast(t('toast.none'));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (hasFieldChange) {
|
|
82
|
+
bulkUpdate.mutate(
|
|
83
|
+
{
|
|
84
|
+
lessons: lessonIds.map((id) => ({
|
|
85
|
+
lessonId: id,
|
|
86
|
+
sessionId:
|
|
87
|
+
lessons.find((l) => l.id === id)?.sessionId ?? '',
|
|
88
|
+
})),
|
|
89
|
+
sessionIds: sessionIds.map((id) => ({ sessionId: id })),
|
|
90
|
+
changes,
|
|
91
|
+
},
|
|
92
|
+
{ onSuccess: resetForm }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (hasMove) {
|
|
97
|
+
const previousLessons = [...lessons];
|
|
98
|
+
const movingLessons = lessonIds
|
|
99
|
+
.map((id) => lessons.find((l) => l.id === id))
|
|
100
|
+
.filter(Boolean) as Lesson[];
|
|
101
|
+
const moves = movingLessons.map((l, i) => ({
|
|
102
|
+
lessonId: l.id,
|
|
103
|
+
fromSessionId: l.sessionId,
|
|
104
|
+
toSessionId: targetSession,
|
|
105
|
+
toIndex: i,
|
|
106
|
+
}));
|
|
107
|
+
// Optimistic store move (matches MultiSelectBar behaviour).
|
|
108
|
+
useStructureStore.getState().moveLessons(lessonIds, targetSession);
|
|
109
|
+
moveLessonsMutation.mutate(
|
|
110
|
+
{ moves, previousLessons },
|
|
111
|
+
{ onSuccess: () => setTargetSession('') }
|
|
77
112
|
);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
? t('toast.preview', { changes: lines.join(', ') })
|
|
81
|
-
: t('toast.none')
|
|
82
|
-
);
|
|
113
|
+
toast.success(t('toast.moving', { count: lessonIds.length }));
|
|
114
|
+
}
|
|
83
115
|
}
|
|
84
116
|
|
|
85
117
|
return (
|
|
@@ -89,8 +121,6 @@ export function EditorBulk() {
|
|
|
89
121
|
<div className="flex size-9 items-center justify-center rounded-lg bg-primary/10 shrink-0">
|
|
90
122
|
{allLessons ? (
|
|
91
123
|
<Video className="size-4 text-primary" />
|
|
92
|
-
) : allSessions ? (
|
|
93
|
-
<Layers className="size-4 text-primary" />
|
|
94
124
|
) : (
|
|
95
125
|
<Layers className="size-4 text-primary" />
|
|
96
126
|
)}
|
|
@@ -99,7 +129,11 @@ export function EditorBulk() {
|
|
|
99
129
|
<p className="text-sm font-semibold">{t('title')}</p>
|
|
100
130
|
<p className="text-[0.65rem] text-muted-foreground">
|
|
101
131
|
{selectedIds.size}{' '}
|
|
102
|
-
{allLessons
|
|
132
|
+
{allLessons
|
|
133
|
+
? t('types.lessons')
|
|
134
|
+
: allSessions
|
|
135
|
+
? t('types.sessions')
|
|
136
|
+
: t('types.items')}{' '}
|
|
103
137
|
{t('types.selected')}
|
|
104
138
|
</p>
|
|
105
139
|
</div>
|
|
@@ -143,11 +177,36 @@ export function EditorBulk() {
|
|
|
143
177
|
})}
|
|
144
178
|
{selectedArray.length > 12 && (
|
|
145
179
|
<Badge variant="outline" className="text-[0.65rem] h-5">
|
|
146
|
-
+{selectedArray.length - 12}
|
|
180
|
+
+{selectedArray.length - 12} {t('more')}
|
|
147
181
|
</Badge>
|
|
148
182
|
)}
|
|
149
183
|
</div>
|
|
150
184
|
|
|
185
|
+
{/* Publicação — aplica a aulas e sessões */}
|
|
186
|
+
<Card>
|
|
187
|
+
<CardHeader className="px-3 pt-3 pb-2">
|
|
188
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
189
|
+
{t('publishTitle')}
|
|
190
|
+
</CardTitle>
|
|
191
|
+
</CardHeader>
|
|
192
|
+
<CardContent className="px-3 pb-3">
|
|
193
|
+
<Select
|
|
194
|
+
value={publish}
|
|
195
|
+
onValueChange={(v) =>
|
|
196
|
+
setPublish(v as 'publish' | 'unpublish')
|
|
197
|
+
}
|
|
198
|
+
>
|
|
199
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
200
|
+
<SelectValue placeholder={t('keepCurrent')} />
|
|
201
|
+
</SelectTrigger>
|
|
202
|
+
<SelectContent>
|
|
203
|
+
<SelectItem value="publish">{t('publish.on')}</SelectItem>
|
|
204
|
+
<SelectItem value="unpublish">{t('publish.off')}</SelectItem>
|
|
205
|
+
</SelectContent>
|
|
206
|
+
</Select>
|
|
207
|
+
</CardContent>
|
|
208
|
+
</Card>
|
|
209
|
+
|
|
151
210
|
{/* Status — só mostra para aulas */}
|
|
152
211
|
{(allLessons || (!allSessions && lessonIds.length > 0)) && (
|
|
153
212
|
<Card>
|
|
@@ -165,55 +224,25 @@ export function EditorBulk() {
|
|
|
165
224
|
<SelectValue placeholder={t('keepCurrent')} />
|
|
166
225
|
</SelectTrigger>
|
|
167
226
|
<SelectContent>
|
|
168
|
-
<SelectItem value="preparada">
|
|
227
|
+
<SelectItem value="preparada">
|
|
228
|
+
{t('status.preparada')}
|
|
229
|
+
</SelectItem>
|
|
169
230
|
<SelectItem value="gravada">{t('status.gravada')}</SelectItem>
|
|
170
231
|
<SelectItem value="editada">{t('status.editada')}</SelectItem>
|
|
171
|
-
<SelectItem value="finalizada">
|
|
172
|
-
|
|
232
|
+
<SelectItem value="finalizada">
|
|
233
|
+
{t('status.finalizada')}
|
|
234
|
+
</SelectItem>
|
|
235
|
+
<SelectItem value="publicada">
|
|
236
|
+
{t('status.publicada')}
|
|
237
|
+
</SelectItem>
|
|
173
238
|
</SelectContent>
|
|
174
239
|
</Select>
|
|
175
240
|
</CardContent>
|
|
176
241
|
</Card>
|
|
177
242
|
)}
|
|
178
243
|
|
|
179
|
-
{/* Visibilidade */}
|
|
180
|
-
<Card>
|
|
181
|
-
<CardHeader className="px-3 pt-3 pb-2">
|
|
182
|
-
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
183
|
-
{t('visibilityTitle')}
|
|
184
|
-
</CardTitle>
|
|
185
|
-
</CardHeader>
|
|
186
|
-
<CardContent className="px-3 pb-3">
|
|
187
|
-
<Select
|
|
188
|
-
value={visibility}
|
|
189
|
-
onValueChange={(v) => setVisibility(v as Visibility)}
|
|
190
|
-
>
|
|
191
|
-
<SelectTrigger className="h-8 text-xs w-full">
|
|
192
|
-
<SelectValue placeholder={t('keepCurrent')} />
|
|
193
|
-
</SelectTrigger>
|
|
194
|
-
<SelectContent>
|
|
195
|
-
<SelectItem value="publico">
|
|
196
|
-
<span className="flex items-center gap-1.5">
|
|
197
|
-
<Eye className="size-3" /> {t('visibility.publico')}
|
|
198
|
-
</span>
|
|
199
|
-
</SelectItem>
|
|
200
|
-
<SelectItem value="privado">
|
|
201
|
-
<span className="flex items-center gap-1.5">
|
|
202
|
-
<EyeOff className="size-3" /> {t('visibility.privado')}
|
|
203
|
-
</span>
|
|
204
|
-
</SelectItem>
|
|
205
|
-
<SelectItem value="restrito">
|
|
206
|
-
<span className="flex items-center gap-1.5">
|
|
207
|
-
<Lock className="size-3" /> {t('visibility.restrito')}
|
|
208
|
-
</span>
|
|
209
|
-
</SelectItem>
|
|
210
|
-
</SelectContent>
|
|
211
|
-
</Select>
|
|
212
|
-
</CardContent>
|
|
213
|
-
</Card>
|
|
214
|
-
|
|
215
244
|
{/* Mover para sessão — só para aulas */}
|
|
216
|
-
{
|
|
245
|
+
{lessonIds.length > 0 && (
|
|
217
246
|
<Card>
|
|
218
247
|
<CardHeader className="px-3 pt-3 pb-2">
|
|
219
248
|
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider flex items-center gap-1.5">
|
|
@@ -240,12 +269,6 @@ export function EditorBulk() {
|
|
|
240
269
|
</CardContent>
|
|
241
270
|
</Card>
|
|
242
271
|
)}
|
|
243
|
-
|
|
244
|
-
<div className="rounded-md border border-amber-200 bg-amber-50/60 dark:border-amber-800 dark:bg-amber-950/30 px-3 py-2">
|
|
245
|
-
<p className="text-[0.65rem] text-amber-700 dark:text-amber-400">
|
|
246
|
-
{t('apiNotice')}
|
|
247
|
-
</p>
|
|
248
|
-
</div>
|
|
249
272
|
</div>
|
|
250
273
|
</ScrollArea>
|
|
251
274
|
|
|
@@ -269,9 +292,14 @@ export function EditorBulk() {
|
|
|
269
292
|
size="sm"
|
|
270
293
|
className="h-7 text-xs"
|
|
271
294
|
onClick={handleSave}
|
|
295
|
+
disabled={isPending}
|
|
272
296
|
>
|
|
273
|
-
|
|
274
|
-
|
|
297
|
+
{isPending ? (
|
|
298
|
+
<Loader2 className="size-3 mr-1 animate-spin" />
|
|
299
|
+
) : (
|
|
300
|
+
<Save className="size-3 mr-1" />
|
|
301
|
+
)}
|
|
302
|
+
{t('apply')}
|
|
275
303
|
</Button>
|
|
276
304
|
</div>
|
|
277
305
|
</div>
|