@hed-hog/lms 0.0.351 → 0.0.354
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/course/course-audio-transcription.service.d.ts +29 -0
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -0
- package/dist/course/course-audio-transcription.service.js +291 -0
- package/dist/course/course-audio-transcription.service.js.map +1 -0
- package/dist/course/course-lesson.controller.d.ts +10 -0
- package/dist/course/course-lesson.controller.d.ts.map +1 -0
- package/dist/course/course-lesson.controller.js +62 -0
- package/dist/course/course-lesson.controller.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +41 -15
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +50 -6
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +50 -15
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +238 -73
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +20 -2
- package/dist/course/course-video-conversion.service.d.ts.map +1 -1
- package/dist/course/course-video-conversion.service.js +730 -10
- package/dist/course/course-video-conversion.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +24 -8
- 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 +5 -3
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +24 -8
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +112 -176
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-lesson-frame.dto.d.ts +5 -0
- package/dist/course/dto/create-course-lesson-frame.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-lesson-frame.dto.js +30 -0
- package/dist/course/dto/create-course-lesson-frame.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +4 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +10 -3
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -1
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +6 -6
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/course/dto/update-course-lesson-frame.dto.d.ts +5 -0
- package/dist/course/dto/update-course-lesson-frame.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-lesson-frame.dto.js +32 -0
- package/dist/course/dto/update-course-lesson-frame.dto.js.map +1 -0
- package/dist/course/dto/update-course-resources.dto.d.ts +4 -2
- package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -1
- package/dist/course/dto/update-course-resources.dto.js +10 -3
- package/dist/course/dto/update-course-resources.dto.js.map +1 -1
- package/dist/course/dto/update-transcription-segments.dto.d.ts +10 -0
- package/dist/course/dto/update-transcription-segments.dto.d.ts.map +1 -0
- package/dist/course/dto/update-transcription-segments.dto.js +38 -0
- package/dist/course/dto/update-transcription-segments.dto.js.map +1 -0
- package/dist/course/lms-setting.controller.d.ts +13 -0
- package/dist/course/lms-setting.controller.d.ts.map +1 -0
- package/dist/course/lms-setting.controller.js +53 -0
- package/dist/course/lms-setting.controller.js.map +1 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.js +74 -33
- package/dist/enterprise/training/training-admin.service.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +6 -0
- package/dist/lms.module.js.map +1 -1
- package/hedhog/data/route.yaml +63 -0
- package/hedhog/data/setting_group.yaml +76 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +10 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +7 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +5 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +7 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +95 -50
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +11 -36
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +19 -5
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +30 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/CourseInstructorsSummaryCard.tsx.ejs +95 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +29 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +42 -31
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +10 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +25 -22
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +12 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +315 -229
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +3220 -534
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +20 -15
- package/hedhog/frontend/app/courses/[id]/structure/_components/icon-action-tooltip.tsx.ejs +35 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +76 -67
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +13 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +18 -16
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +6 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +23 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +48 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +11 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +121 -15
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +69 -14
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +11 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +31 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +32 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +57 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +46 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +14 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-audio-files.ts.ejs +23 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +34 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +76 -0
- package/hedhog/frontend/messages/en.json +39 -3
- package/hedhog/frontend/messages/pt.json +39 -3
- package/hedhog/table/course.yaml +8 -0
- package/hedhog/table/course_lesson_file.yaml +12 -4
- package/hedhog/table/course_lesson_transcription_segment.yaml +22 -0
- package/hedhog/table/course_lesson_video_frame.yaml +25 -0
- package/package.json +9 -9
- package/src/course/course-audio-transcription.service.ts +393 -0
- package/src/course/course-lesson.controller.ts +28 -0
- package/src/course/course-structure.controller.ts +49 -3
- package/src/course/course-structure.service.ts +294 -32
- package/src/course/course-video-conversion.service.ts +972 -6
- package/src/course/course.module.ts +5 -3
- package/src/course/course.service.ts +87 -139
- package/src/course/dto/create-course-lesson-frame.dto.ts +14 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +19 -3
- package/src/course/dto/create-course.dto.ts +5 -5
- package/src/course/dto/update-course-lesson-frame.dto.ts +16 -0
- package/src/course/dto/update-course-resources.dto.ts +18 -3
- package/src/course/dto/update-transcription-segments.dto.ts +20 -0
- package/src/course/lms-setting.controller.ts +30 -0
- package/src/enterprise/training/training-admin.service.ts +77 -24
- package/src/index.ts +2 -0
- package/src/lms.module.ts +6 -0
- package/hedhog/table/course_instructor.yaml +0 -27
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Input } from '@/components/ui/input';
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from '@/components/ui/tooltip';
|
|
4
9
|
import { cn } from '@/lib/utils';
|
|
5
10
|
import {
|
|
6
11
|
ClipboardList,
|
|
12
|
+
Clock3,
|
|
7
13
|
Eye,
|
|
8
14
|
EyeOff,
|
|
9
15
|
FileText,
|
|
10
16
|
Film,
|
|
11
17
|
HelpCircle,
|
|
18
|
+
Loader2,
|
|
12
19
|
Lock,
|
|
13
20
|
Paperclip,
|
|
14
21
|
ScrollText,
|
|
@@ -17,10 +24,19 @@ import {
|
|
|
17
24
|
} from 'lucide-react';
|
|
18
25
|
import { useTranslations } from 'next-intl';
|
|
19
26
|
import { useEffect, useRef } from 'react';
|
|
27
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
20
28
|
import { useUpdateLessonMutation } from '../_data/use-course-structure-mutations';
|
|
29
|
+
import {
|
|
30
|
+
getQueueJob,
|
|
31
|
+
type QueueJobStatus,
|
|
32
|
+
} from '../_data/services/course-structure.service';
|
|
21
33
|
import { HighlightedText } from './highlighted-text';
|
|
22
34
|
import { useStructureStore } from './store';
|
|
23
35
|
import type { Lesson, LessonStatus, LessonType, Visibility } from './types';
|
|
36
|
+
import {
|
|
37
|
+
getLessonAttachedResourceCount,
|
|
38
|
+
getLessonUploadedVideoCount,
|
|
39
|
+
} from './tree-helpers';
|
|
24
40
|
import { useTreeDisplaySettings } from './use-tree-display-settings';
|
|
25
41
|
|
|
26
42
|
// ── Type configs ──────────────────────────────────────────────────────────────
|
|
@@ -79,6 +95,7 @@ export function TreeRowLesson({
|
|
|
79
95
|
const t = useTranslations('lms.CoursesPage.StructurePage.lessonRow');
|
|
80
96
|
const cfg = LESSON_TYPE_CONFIG[data.type];
|
|
81
97
|
const Icon = cfg.icon;
|
|
98
|
+
const { request } = useApp();
|
|
82
99
|
|
|
83
100
|
const statusCfg = data.status ? STATUS_CONFIG[data.status] : null;
|
|
84
101
|
const visibilityCfg = data.visibility
|
|
@@ -101,6 +118,35 @@ export function TreeRowLesson({
|
|
|
101
118
|
|
|
102
119
|
const updateLesson = useUpdateLessonMutation();
|
|
103
120
|
|
|
121
|
+
const { data: conversionJobStatus } = useQuery<QueueJobStatus | null>({
|
|
122
|
+
queryKey: [
|
|
123
|
+
'lesson-conversion-job-status',
|
|
124
|
+
data.id,
|
|
125
|
+
data.videoConversionJobId,
|
|
126
|
+
],
|
|
127
|
+
enabled: data.type === 'video' && Boolean(data.videoConversionJobId),
|
|
128
|
+
queryFn: async () => {
|
|
129
|
+
if (!data.videoConversionJobId) return null;
|
|
130
|
+
const job = await getQueueJob(request, data.videoConversionJobId);
|
|
131
|
+
return job.status;
|
|
132
|
+
},
|
|
133
|
+
refetchInterval: (query) => {
|
|
134
|
+
const status = query.state.data as QueueJobStatus | null | undefined;
|
|
135
|
+
if (!status) return 3000;
|
|
136
|
+
|
|
137
|
+
if (
|
|
138
|
+
status === 'pending' ||
|
|
139
|
+
status === 'scheduled' ||
|
|
140
|
+
status === 'processing' ||
|
|
141
|
+
status === 'retrying'
|
|
142
|
+
) {
|
|
143
|
+
return 3000;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return false;
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
104
150
|
const isRenaming = inlineRenamingId === data.id;
|
|
105
151
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
106
152
|
|
|
@@ -151,6 +197,45 @@ export function TreeRowLesson({
|
|
|
151
197
|
durationH > 0
|
|
152
198
|
? `${durationH}h${durationM > 0 ? `${durationM}m` : ''}`
|
|
153
199
|
: `${durationM}m`;
|
|
200
|
+
const attachedResourceCount = getLessonAttachedResourceCount(data);
|
|
201
|
+
const uploadedVideoCount = getLessonUploadedVideoCount(data);
|
|
202
|
+
|
|
203
|
+
const videoJobVisualState =
|
|
204
|
+
data.type !== 'video'
|
|
205
|
+
? 'none'
|
|
206
|
+
: conversionJobStatus === 'pending' ||
|
|
207
|
+
conversionJobStatus === 'scheduled' ||
|
|
208
|
+
conversionJobStatus === 'retrying'
|
|
209
|
+
? 'waiting'
|
|
210
|
+
: conversionJobStatus === 'processing'
|
|
211
|
+
? 'processing'
|
|
212
|
+
: 'none';
|
|
213
|
+
|
|
214
|
+
const typeIconNode =
|
|
215
|
+
data.type !== 'video' ? (
|
|
216
|
+
<Icon className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
|
|
217
|
+
) : videoJobVisualState === 'waiting' ? (
|
|
218
|
+
<span
|
|
219
|
+
title="Conversão aguardando processamento"
|
|
220
|
+
aria-label="Conversão aguardando processamento"
|
|
221
|
+
className="inline-flex items-center"
|
|
222
|
+
>
|
|
223
|
+
<Clock3 className="size-3.5 shrink-0 text-amber-500" aria-hidden />
|
|
224
|
+
</span>
|
|
225
|
+
) : videoJobVisualState === 'processing' ? (
|
|
226
|
+
<span
|
|
227
|
+
title="Conversão em processamento"
|
|
228
|
+
aria-label="Conversão em processamento"
|
|
229
|
+
className="inline-flex items-center"
|
|
230
|
+
>
|
|
231
|
+
<Loader2
|
|
232
|
+
className="size-3.5 shrink-0 animate-spin text-blue-500"
|
|
233
|
+
aria-hidden
|
|
234
|
+
/>
|
|
235
|
+
</span>
|
|
236
|
+
) : (
|
|
237
|
+
<Video className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
|
|
238
|
+
);
|
|
154
239
|
|
|
155
240
|
return (
|
|
156
241
|
<div
|
|
@@ -172,7 +257,7 @@ export function TreeRowLesson({
|
|
|
172
257
|
)}
|
|
173
258
|
>
|
|
174
259
|
{/* Type icon */}
|
|
175
|
-
|
|
260
|
+
{typeIconNode}
|
|
176
261
|
|
|
177
262
|
{/* Inline rename or title */}
|
|
178
263
|
{isRenaming ? (
|
|
@@ -211,8 +296,8 @@ export function TreeRowLesson({
|
|
|
211
296
|
const VisIcon = visibilityCfg.icon;
|
|
212
297
|
return (
|
|
213
298
|
<span
|
|
214
|
-
|
|
215
|
-
|
|
299
|
+
title={t('visibility', { value: visibilityCfg.label })}
|
|
300
|
+
aria-label={t('visibility', { value: visibilityCfg.label })}
|
|
216
301
|
className="inline-flex items-center"
|
|
217
302
|
>
|
|
218
303
|
<VisIcon
|
|
@@ -239,20 +324,41 @@ export function TreeRowLesson({
|
|
|
239
324
|
<Film className="size-3 shrink-0 text-violet-500" />
|
|
240
325
|
</span>
|
|
241
326
|
)}
|
|
327
|
+
{/* Uploaded videos indicator */}
|
|
328
|
+
{showVideoIndicator && uploadedVideoCount > 0 && (
|
|
329
|
+
<Tooltip>
|
|
330
|
+
<TooltipTrigger asChild>
|
|
331
|
+
<span
|
|
332
|
+
aria-label={t('uploadedVideosAria', {
|
|
333
|
+
count: uploadedVideoCount,
|
|
334
|
+
})}
|
|
335
|
+
className="inline-flex items-center"
|
|
336
|
+
>
|
|
337
|
+
<Video className="size-3 shrink-0 text-violet-500" />
|
|
338
|
+
</span>
|
|
339
|
+
</TooltipTrigger>
|
|
340
|
+
<TooltipContent>
|
|
341
|
+
{t('uploadedVideos', { count: uploadedVideoCount })}
|
|
342
|
+
</TooltipContent>
|
|
343
|
+
</Tooltip>
|
|
344
|
+
)}
|
|
242
345
|
{/* Resources indicator */}
|
|
243
|
-
{showResourcesIndicator &&
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
346
|
+
{showResourcesIndicator && attachedResourceCount > 0 && (
|
|
347
|
+
<Tooltip>
|
|
348
|
+
<TooltipTrigger asChild>
|
|
349
|
+
<span
|
|
350
|
+
aria-label={t('resourcesAria', {
|
|
351
|
+
count: attachedResourceCount,
|
|
352
|
+
})}
|
|
353
|
+
className="inline-flex items-center"
|
|
354
|
+
>
|
|
355
|
+
<Paperclip className="size-3 shrink-0 text-sky-500" />
|
|
253
356
|
</span>
|
|
254
|
-
|
|
255
|
-
|
|
357
|
+
</TooltipTrigger>
|
|
358
|
+
<TooltipContent>
|
|
359
|
+
{t('resources', { count: attachedResourceCount })}
|
|
360
|
+
</TooltipContent>
|
|
361
|
+
</Tooltip>
|
|
256
362
|
)}
|
|
257
363
|
{/* Transcription indicator */}
|
|
258
364
|
{showTranscriptionIndicator &&
|
|
@@ -1,18 +1,33 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Input } from '@/components/ui/input';
|
|
4
|
+
import {
|
|
5
|
+
Tooltip,
|
|
6
|
+
TooltipContent,
|
|
7
|
+
TooltipTrigger,
|
|
8
|
+
} from '@/components/ui/tooltip';
|
|
4
9
|
import { cn } from '@/lib/utils';
|
|
5
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
ChevronDown,
|
|
12
|
+
ChevronRight,
|
|
13
|
+
Layers,
|
|
14
|
+
Paperclip,
|
|
15
|
+
Video,
|
|
16
|
+
} from 'lucide-react';
|
|
6
17
|
import { useTranslations } from 'next-intl';
|
|
7
18
|
import { useEffect, useRef } from 'react';
|
|
8
19
|
import { useUpdateSessionMutation } from '../_data/use-course-structure-mutations';
|
|
9
20
|
import { HighlightedText } from './highlighted-text';
|
|
21
|
+
import { IconActionTooltip } from './icon-action-tooltip';
|
|
10
22
|
import { useStructureStore } from './store';
|
|
11
23
|
import type { Session } from './types';
|
|
24
|
+
import { useTreeDisplaySettings } from './use-tree-display-settings';
|
|
12
25
|
|
|
13
26
|
interface TreeRowSessionProps {
|
|
14
27
|
data: Session;
|
|
15
28
|
lessonCount: number;
|
|
29
|
+
resourceCount: number;
|
|
30
|
+
videoCount: number;
|
|
16
31
|
isExpanded: boolean;
|
|
17
32
|
isSelected: boolean;
|
|
18
33
|
isActive: boolean;
|
|
@@ -25,6 +40,8 @@ interface TreeRowSessionProps {
|
|
|
25
40
|
export function TreeRowSession({
|
|
26
41
|
data,
|
|
27
42
|
lessonCount,
|
|
43
|
+
resourceCount,
|
|
44
|
+
videoCount,
|
|
28
45
|
isExpanded,
|
|
29
46
|
isSelected,
|
|
30
47
|
isActive,
|
|
@@ -37,6 +54,7 @@ export function TreeRowSession({
|
|
|
37
54
|
const cancelRename = useStructureStore((s) => s.cancelRename);
|
|
38
55
|
const startRename = useStructureStore((s) => s.startRename);
|
|
39
56
|
const renameItem = useStructureStore((s) => s.renameItem);
|
|
57
|
+
const { showCode } = useTreeDisplaySettings();
|
|
40
58
|
|
|
41
59
|
const updateSession = useUpdateSessionMutation();
|
|
42
60
|
|
|
@@ -103,18 +121,20 @@ export function TreeRowSession({
|
|
|
103
121
|
)}
|
|
104
122
|
>
|
|
105
123
|
{/* Expand/collapse chevron */}
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
<IconActionTooltip label={isExpanded ? t('collapse') : t('expand')}>
|
|
125
|
+
<button
|
|
126
|
+
className="shrink-0 text-muted-foreground hover:text-foreground transition-colors p-0.5 rounded focus-visible:ring-1 focus-visible:ring-ring"
|
|
127
|
+
onClick={onToggleExpand}
|
|
128
|
+
aria-label={isExpanded ? t('collapse') : t('expand')}
|
|
129
|
+
tabIndex={-1}
|
|
130
|
+
>
|
|
131
|
+
{isExpanded ? (
|
|
132
|
+
<ChevronDown className="size-3.5" />
|
|
133
|
+
) : (
|
|
134
|
+
<ChevronRight className="size-3.5" />
|
|
135
|
+
)}
|
|
136
|
+
</button>
|
|
137
|
+
</IconActionTooltip>
|
|
118
138
|
|
|
119
139
|
{/* Session icon */}
|
|
120
140
|
<Layers
|
|
@@ -142,6 +162,11 @@ export function TreeRowSession({
|
|
|
142
162
|
{/* Meta badges (hidden during rename) */}
|
|
143
163
|
{!isRenaming && (
|
|
144
164
|
<div className="flex items-center gap-1 shrink-0">
|
|
165
|
+
{showCode && (
|
|
166
|
+
<span className="inline-flex items-center rounded border border-border/60 bg-muted/60 px-1 text-[0.6rem] font-mono text-muted-foreground leading-4">
|
|
167
|
+
{data.code}
|
|
168
|
+
</span>
|
|
169
|
+
)}
|
|
145
170
|
{/* Duration - show on hover */}
|
|
146
171
|
{data.duration > 0 && (
|
|
147
172
|
<span className="text-[0.6rem] text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity tabular-nums">
|
|
@@ -149,9 +174,39 @@ export function TreeRowSession({
|
|
|
149
174
|
</span>
|
|
150
175
|
)}
|
|
151
176
|
{/* Lesson count - always visible */}
|
|
152
|
-
<span className="text-[0.62rem] font-medium tabular-nums text-muted-foreground min-w-
|
|
177
|
+
<span className="text-[0.62rem] font-medium tabular-nums text-muted-foreground min-w-6 text-right">
|
|
153
178
|
{lessonCount}
|
|
154
179
|
</span>
|
|
180
|
+
{videoCount > 0 && (
|
|
181
|
+
<Tooltip>
|
|
182
|
+
<TooltipTrigger asChild>
|
|
183
|
+
<span
|
|
184
|
+
className="inline-flex items-center"
|
|
185
|
+
aria-label={t('uploadedVideosAria', { count: videoCount })}
|
|
186
|
+
>
|
|
187
|
+
<Video className="size-3 shrink-0 text-violet-500" />
|
|
188
|
+
</span>
|
|
189
|
+
</TooltipTrigger>
|
|
190
|
+
<TooltipContent>
|
|
191
|
+
{t('uploadedVideos', { count: videoCount })}
|
|
192
|
+
</TooltipContent>
|
|
193
|
+
</Tooltip>
|
|
194
|
+
)}
|
|
195
|
+
{resourceCount > 0 && (
|
|
196
|
+
<Tooltip>
|
|
197
|
+
<TooltipTrigger asChild>
|
|
198
|
+
<span
|
|
199
|
+
className="inline-flex items-center"
|
|
200
|
+
aria-label={t('resourcesAria', { count: resourceCount })}
|
|
201
|
+
>
|
|
202
|
+
<Paperclip className="size-3 shrink-0 text-sky-500" />
|
|
203
|
+
</span>
|
|
204
|
+
</TooltipTrigger>
|
|
205
|
+
<TooltipContent>
|
|
206
|
+
{t('resources', { count: resourceCount })}
|
|
207
|
+
</TooltipContent>
|
|
208
|
+
</Tooltip>
|
|
209
|
+
)}
|
|
155
210
|
{/* Published status dot */}
|
|
156
211
|
<span
|
|
157
212
|
className={cn(
|
|
@@ -17,6 +17,10 @@ interface TreeRowProps {
|
|
|
17
17
|
isMatched: boolean;
|
|
18
18
|
isEffectivelyExpanded: boolean;
|
|
19
19
|
lessonCountMap: Map<string, number>;
|
|
20
|
+
sessionIndicatorMap: Map<
|
|
21
|
+
string,
|
|
22
|
+
{ resourceCount: number; videoCount: number }
|
|
23
|
+
>;
|
|
20
24
|
/** Full ordered visible list — required for SHIFT-range selection. */
|
|
21
25
|
visibleItems: FlatItem[];
|
|
22
26
|
}
|
|
@@ -29,6 +33,7 @@ export function TreeRow({
|
|
|
29
33
|
isMatched,
|
|
30
34
|
isEffectivelyExpanded,
|
|
31
35
|
lessonCountMap,
|
|
36
|
+
sessionIndicatorMap,
|
|
32
37
|
visibleItems,
|
|
33
38
|
}: TreeRowProps) {
|
|
34
39
|
const selectItem = useStructureStore((s) => s.selectItem);
|
|
@@ -76,11 +81,17 @@ export function TreeRow({
|
|
|
76
81
|
|
|
77
82
|
if (item.type === 'session') {
|
|
78
83
|
const lessonCount = lessonCountMap.get(item.id) ?? 0;
|
|
84
|
+
const indicatorCounts = sessionIndicatorMap.get(item.id) ?? {
|
|
85
|
+
resourceCount: 0,
|
|
86
|
+
videoCount: 0,
|
|
87
|
+
};
|
|
79
88
|
return (
|
|
80
89
|
<TreeContextMenu item={item}>
|
|
81
90
|
<TreeRowSession
|
|
82
91
|
data={item.data as Session}
|
|
83
92
|
lessonCount={lessonCount}
|
|
93
|
+
resourceCount={indicatorCounts.resourceCount}
|
|
94
|
+
videoCount={indicatorCounts.videoCount}
|
|
84
95
|
isExpanded={isEffectivelyExpanded}
|
|
85
96
|
isSelected={isSelected}
|
|
86
97
|
isActive={isActive}
|
|
@@ -46,16 +46,43 @@ export interface Resource {
|
|
|
46
46
|
size: string;
|
|
47
47
|
type: string;
|
|
48
48
|
public: boolean;
|
|
49
|
+
uploadedAt?: string;
|
|
49
50
|
/** Blob URL (for newly uploaded files) or remote URL for preview/download */
|
|
50
51
|
url?: string;
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
export interface VideoFrame {
|
|
55
|
+
id: string;
|
|
56
|
+
fileId: number;
|
|
57
|
+
name: string;
|
|
58
|
+
timeSeconds: number;
|
|
59
|
+
url?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
export interface LessonInstructor {
|
|
54
63
|
id: string;
|
|
55
64
|
role?: 'lead' | 'assistant';
|
|
56
65
|
name?: string;
|
|
57
66
|
}
|
|
58
67
|
|
|
68
|
+
export interface TranscriptionSegment {
|
|
69
|
+
id: number;
|
|
70
|
+
startSeconds: number;
|
|
71
|
+
endSeconds: number;
|
|
72
|
+
text: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface LessonAudioFile {
|
|
76
|
+
id: number;
|
|
77
|
+
fileId: number | null;
|
|
78
|
+
title: string;
|
|
79
|
+
tipo: string;
|
|
80
|
+
localeId: number | null;
|
|
81
|
+
locale: { id: number; name: string; code: string; region: string } | null;
|
|
82
|
+
file: { id: number; filename: string; path: string } | null;
|
|
83
|
+
createdAt: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
59
86
|
export interface Lesson {
|
|
60
87
|
id: string;
|
|
61
88
|
code: string;
|
|
@@ -73,6 +100,8 @@ export interface Lesson {
|
|
|
73
100
|
videoUrl?: string;
|
|
74
101
|
autoDuration?: boolean;
|
|
75
102
|
transcription?: string;
|
|
103
|
+
transcriptionSegments?: TranscriptionSegment[];
|
|
104
|
+
localeId?: number | null;
|
|
76
105
|
videoConversionJobId?: number;
|
|
77
106
|
// Questão
|
|
78
107
|
linkedExam?: string;
|
|
@@ -80,6 +109,7 @@ export interface Lesson {
|
|
|
80
109
|
postContent?: string;
|
|
81
110
|
// Relations
|
|
82
111
|
resources: Resource[];
|
|
112
|
+
frames?: VideoFrame[];
|
|
83
113
|
instructors?: LessonInstructor[];
|
|
84
114
|
}
|
|
85
115
|
|
|
@@ -118,6 +148,7 @@ export type LessonFormValues = {
|
|
|
118
148
|
};
|
|
119
149
|
|
|
120
150
|
export type CourseFormValues = {
|
|
151
|
+
code: string;
|
|
121
152
|
name: string;
|
|
122
153
|
title: string;
|
|
123
154
|
description: string;
|
package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
Resource,
|
|
31
31
|
Session,
|
|
32
32
|
SessionFormValues,
|
|
33
|
+
VideoFrame,
|
|
33
34
|
} from '../../_components/types';
|
|
34
35
|
|
|
35
36
|
// Alias to keep internal adapter code concise when mapping resources.
|
|
@@ -41,6 +42,7 @@ import type {
|
|
|
41
42
|
ApiCreateSessionPayload,
|
|
42
43
|
ApiGetStructureResponse,
|
|
43
44
|
ApiLesson,
|
|
45
|
+
ApiLessonFrame,
|
|
44
46
|
ApiLessonResource,
|
|
45
47
|
ApiSession,
|
|
46
48
|
ApiUpdateLessonPayload,
|
|
@@ -72,18 +74,30 @@ function normalizeVideoProvider(
|
|
|
72
74
|
|
|
73
75
|
/** Normalize a raw API resource to the frontend Resource shape. */
|
|
74
76
|
function normalizeResource(raw: ApiLessonResource): Resource {
|
|
77
|
+
const withUpload = raw as ApiLessonResource & {
|
|
78
|
+
createdAt?: string;
|
|
79
|
+
created_at?: string;
|
|
80
|
+
uploadedAt?: string;
|
|
81
|
+
uploaded_at?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
75
84
|
return {
|
|
76
85
|
id: String(raw.id),
|
|
77
86
|
fileId: raw.fileId ?? undefined,
|
|
78
87
|
name: raw.nome,
|
|
79
|
-
type: raw.
|
|
88
|
+
type: raw.type ?? '',
|
|
80
89
|
/**
|
|
81
90
|
* ⚠️ BACKEND NOTE: file size is not returned in the API response.
|
|
82
91
|
* TODO[BACKEND]: Include `size` in the lesson resource response from
|
|
83
92
|
* CourseStructureService.getLessonById().
|
|
84
93
|
*/
|
|
85
94
|
size: '',
|
|
86
|
-
public: raw.
|
|
95
|
+
public: raw.is_public ?? true,
|
|
96
|
+
uploadedAt:
|
|
97
|
+
withUpload.uploadedAt ??
|
|
98
|
+
withUpload.uploaded_at ??
|
|
99
|
+
withUpload.createdAt ??
|
|
100
|
+
withUpload.created_at,
|
|
87
101
|
/**
|
|
88
102
|
* ⚠️ BACKEND NOTE: a pre-signed or static URL is not returned.
|
|
89
103
|
* TODO[BACKEND]: Include a resolved URL (e.g. `/file/open/:id`) in the
|
|
@@ -93,6 +107,16 @@ function normalizeResource(raw: ApiLessonResource): Resource {
|
|
|
93
107
|
};
|
|
94
108
|
}
|
|
95
109
|
|
|
110
|
+
function normalizeFrame(raw: ApiLessonFrame): VideoFrame {
|
|
111
|
+
return {
|
|
112
|
+
id: String(raw.id),
|
|
113
|
+
fileId: raw.fileId,
|
|
114
|
+
name: raw.nome,
|
|
115
|
+
timeSeconds: raw.timeSeconds,
|
|
116
|
+
url: raw.url,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
96
120
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
97
121
|
// Backend → Frontend (normalise)
|
|
98
122
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -164,6 +188,7 @@ export function normalizeLesson(raw: ApiLesson, order = 0): Lesson {
|
|
|
164
188
|
videoUrl: raw.videoUrl,
|
|
165
189
|
autoDuration: raw.duracaoAutomatica,
|
|
166
190
|
transcription: raw.transcricao,
|
|
191
|
+
localeId: raw.locale_id ?? null,
|
|
167
192
|
videoConversionJobId: raw.videoConversionJobId,
|
|
168
193
|
/**
|
|
169
194
|
* ⚠️ TYPE MISMATCH: backend sends `exameVinculado` as number;
|
|
@@ -173,6 +198,7 @@ export function normalizeLesson(raw: ApiLesson, order = 0): Lesson {
|
|
|
173
198
|
raw.exameVinculado != null ? String(raw.exameVinculado) : undefined,
|
|
174
199
|
postContent: raw.conteudoPost,
|
|
175
200
|
resources: (raw.recursos ?? []).map(normalizeResource),
|
|
201
|
+
frames: (raw.frames ?? []).map(normalizeFrame),
|
|
176
202
|
instructors,
|
|
177
203
|
/**
|
|
178
204
|
* ⚠️ BACKEND NOTE: status and visibility are not in the DB schema.
|
|
@@ -305,8 +331,8 @@ export function toCreateLessonPayload(
|
|
|
305
331
|
: Number.isInteger(Number(r.id)) && Number(r.id) > 0
|
|
306
332
|
? { fileId: Number(r.id) }
|
|
307
333
|
: {}),
|
|
308
|
-
...(r.type ? {
|
|
309
|
-
|
|
334
|
+
...(r.type ? { type: r.type } : {}),
|
|
335
|
+
is_public: r.public,
|
|
310
336
|
})),
|
|
311
337
|
}),
|
|
312
338
|
};
|
|
@@ -363,8 +389,8 @@ export function toUpdateLessonPayload(
|
|
|
363
389
|
: Number.isInteger(Number(r.id)) && Number(r.id) > 0
|
|
364
390
|
? { fileId: Number(r.id) }
|
|
365
391
|
: {}),
|
|
366
|
-
...(r.type ? {
|
|
367
|
-
|
|
392
|
+
...(r.type ? { type: r.type } : {}),
|
|
393
|
+
is_public: r.public,
|
|
368
394
|
}));
|
|
369
395
|
}
|
|
370
396
|
return payload;
|
package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs
CHANGED
|
@@ -21,9 +21,12 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type {
|
|
24
|
+
ApiCreateLessonFramePayload,
|
|
25
|
+
ApiCreateLessonFrameResponse,
|
|
24
26
|
ApiCourseResource,
|
|
25
27
|
ApiCreateLessonPayload,
|
|
26
28
|
ApiCreateSessionPayload,
|
|
29
|
+
ApiDeleteLessonFrameResponse,
|
|
27
30
|
ApiDuplicateSessionResponse,
|
|
28
31
|
ApiGetStructureResponse,
|
|
29
32
|
ApiLesson,
|
|
@@ -32,6 +35,8 @@ import type {
|
|
|
32
35
|
ApiPasteLessonsResponse,
|
|
33
36
|
ApiReorderLessonsPayload,
|
|
34
37
|
ApiReorderSessionsPayload,
|
|
38
|
+
ApiUpdateLessonFramePayload,
|
|
39
|
+
ApiUpdateLessonFrameResponse,
|
|
35
40
|
ApiSession,
|
|
36
41
|
ApiUpdateCoursePayload,
|
|
37
42
|
ApiUpdateLessonPayload,
|
|
@@ -88,6 +93,7 @@ export interface QueueJobEvent {
|
|
|
88
93
|
| 'requeued'
|
|
89
94
|
| 'unlocked';
|
|
90
95
|
message?: string | null;
|
|
96
|
+
metadata?: Record<string, unknown> | null;
|
|
91
97
|
created_at: string;
|
|
92
98
|
}
|
|
93
99
|
|
|
@@ -101,6 +107,10 @@ export interface QueueJobResponse {
|
|
|
101
107
|
finished_at?: string | null;
|
|
102
108
|
next_retry_at?: string | null;
|
|
103
109
|
last_error?: string | null;
|
|
110
|
+
result?: {
|
|
111
|
+
transcriptionJobId?: number | null;
|
|
112
|
+
[key: string]: unknown;
|
|
113
|
+
} | null;
|
|
104
114
|
queue_job_attempt: QueueJobAttempt[];
|
|
105
115
|
queue_job_event: QueueJobEvent[];
|
|
106
116
|
}
|
|
@@ -191,8 +201,8 @@ export async function updateCourseResources(
|
|
|
191
201
|
recursos: Array<{
|
|
192
202
|
nome: string;
|
|
193
203
|
fileId?: number;
|
|
194
|
-
|
|
195
|
-
|
|
204
|
+
type?: string;
|
|
205
|
+
is_public?: boolean;
|
|
196
206
|
}>;
|
|
197
207
|
}
|
|
198
208
|
): Promise<ApiCourseResource[]> {
|
|
@@ -337,6 +347,51 @@ export async function deleteLesson(
|
|
|
337
347
|
return extractData<{ success: boolean }>(response);
|
|
338
348
|
}
|
|
339
349
|
|
|
350
|
+
export async function deleteLessonFrame(
|
|
351
|
+
request: RequestFn,
|
|
352
|
+
courseId: string | number,
|
|
353
|
+
sessionId: string | number,
|
|
354
|
+
lessonId: string | number,
|
|
355
|
+
frameId: string | number
|
|
356
|
+
): Promise<ApiDeleteLessonFrameResponse> {
|
|
357
|
+
const response = await request({
|
|
358
|
+
url: `${LESSON_PATH(courseId, sessionId, lessonId)}/frames/${frameId}`,
|
|
359
|
+
method: 'DELETE',
|
|
360
|
+
});
|
|
361
|
+
return extractData<ApiDeleteLessonFrameResponse>(response);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function createLessonFrame(
|
|
365
|
+
request: RequestFn,
|
|
366
|
+
courseId: string | number,
|
|
367
|
+
sessionId: string | number,
|
|
368
|
+
lessonId: string | number,
|
|
369
|
+
payload: ApiCreateLessonFramePayload
|
|
370
|
+
): Promise<ApiCreateLessonFrameResponse> {
|
|
371
|
+
const response = await request({
|
|
372
|
+
url: `${LESSON_PATH(courseId, sessionId, lessonId)}/frames`,
|
|
373
|
+
method: 'POST',
|
|
374
|
+
data: payload,
|
|
375
|
+
});
|
|
376
|
+
return extractData<ApiCreateLessonFrameResponse>(response);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export async function updateLessonFrame(
|
|
380
|
+
request: RequestFn,
|
|
381
|
+
courseId: string | number,
|
|
382
|
+
sessionId: string | number,
|
|
383
|
+
lessonId: string | number,
|
|
384
|
+
frameId: string | number,
|
|
385
|
+
payload: ApiUpdateLessonFramePayload
|
|
386
|
+
): Promise<ApiUpdateLessonFrameResponse> {
|
|
387
|
+
const response = await request({
|
|
388
|
+
url: `${LESSON_PATH(courseId, sessionId, lessonId)}/frames/${frameId}`,
|
|
389
|
+
method: 'PATCH',
|
|
390
|
+
data: payload,
|
|
391
|
+
});
|
|
392
|
+
return extractData<ApiUpdateLessonFrameResponse>(response);
|
|
393
|
+
}
|
|
394
|
+
|
|
340
395
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
341
396
|
// Pending backend endpoints (stubs — throw until implemented)
|
|
342
397
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -502,7 +557,6 @@ export async function uploadFile(
|
|
|
502
557
|
url: '/file',
|
|
503
558
|
method: 'POST',
|
|
504
559
|
data: formData,
|
|
505
|
-
headers: { 'Content-Type': 'multipart/form-data' },
|
|
506
560
|
onUploadProgress: options?.onUploadProgress,
|
|
507
561
|
});
|
|
508
562
|
return extractData<{ id: number; filename: string }>(response);
|