@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
|
@@ -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
|
-
?
|
|
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
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
<
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
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>
|