@hed-hog/lms 0.0.364 → 0.0.366
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/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
- package/dist/class-group/class-group.controller.d.ts +1 -0
- package/dist/class-group/class-group.controller.d.ts.map +1 -1
- package/dist/class-group/class-group.service.d.ts +1 -0
- package/dist/class-group/class-group.service.d.ts.map +1 -1
- package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
- package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12-worker.service.js +109 -0
- package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
- package/dist/course/course-export-scorm12.service.d.ts +42 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12.service.js +628 -0
- package/dist/course/course-export-scorm12.service.js.map +1 -0
- package/dist/course/course-export.service.d.ts +84 -0
- package/dist/course/course-export.service.d.ts.map +1 -0
- package/dist/course/course-export.service.js +237 -0
- package/dist/course/course-export.service.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +20 -10
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +20 -4
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +12 -4
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +98 -23
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.js +398 -0
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
- package/dist/course/course-video-hls.service.d.ts +71 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -0
- package/dist/course/course-video-hls.service.js +784 -0
- package/dist/course/course-video-hls.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +47 -13
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +40 -26
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.mcp-tools.js +1 -1
- package/dist/course/course.mcp-tools.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +16 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +8 -9
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +93 -50
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
- package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.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 +14 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-export.dto.js +71 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -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 +3 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/ffmpeg.util.d.ts +10 -0
- package/dist/course/ffmpeg.util.d.ts.map +1 -0
- package/dist/course/ffmpeg.util.js +79 -0
- package/dist/course/ffmpeg.util.js.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts +18 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +106 -8
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-infra.service.js +32 -8
- package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
- package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +43 -2
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +11 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +59 -6
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +2 -1
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +4 -2
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/course/scorm12-schemas.d.ts +4 -0
- package/dist/course/scorm12-schemas.d.ts.map +1 -0
- package/dist/course/scorm12-schemas.js +9 -0
- package/dist/course/scorm12-schemas.js.map +1 -0
- package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +2 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +51 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +217 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/evaluation/evaluation.service.d.ts +18 -0
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +125 -0
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
- package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-standalone-question.dto.js +70 -0
- package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
- package/dist/exam/exam.module.d.ts.map +1 -1
- package/dist/exam/exam.module.js +2 -1
- package/dist/exam/exam.module.js.map +1 -1
- package/dist/exam/exam.service.d.ts +21 -0
- package/dist/exam/exam.service.d.ts.map +1 -1
- package/dist/exam/exam.service.js +80 -0
- package/dist/exam/exam.service.js.map +1 -1
- package/dist/exam/question.controller.d.ts +27 -0
- package/dist/exam/question.controller.d.ts.map +1 -0
- package/dist/exam/question.controller.js +53 -0
- package/dist/exam/question.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
- 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 +161 -25
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
- package/dist/lms-commerce-access.subscriber.d.ts +11 -0
- package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
- package/dist/lms-commerce-access.subscriber.js +74 -0
- package/dist/lms-commerce-access.subscriber.js.map +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +16 -5
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
- package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
- package/dist/platforma/dto/heartbeat.dto.js +50 -0
- package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
- package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.js +50 -0
- package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
- package/dist/platforma/platforma-performance.service.d.ts +121 -0
- package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
- package/dist/platforma/platforma-performance.service.js +500 -0
- package/dist/platforma/platforma-performance.service.js.map +1 -0
- package/dist/platforma/platforma-search.service.d.ts +21 -0
- package/dist/platforma/platforma-search.service.d.ts.map +1 -0
- package/dist/platforma/platforma-search.service.js +64 -0
- package/dist/platforma/platforma-search.service.js.map +1 -0
- package/dist/platforma/platforma-video.service.d.ts +39 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -0
- package/dist/platforma/platforma-video.service.js +301 -0
- package/dist/platforma/platforma-video.service.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +209 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +208 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.controller.d.ts +2 -0
- package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.controller.js +31 -0
- package/dist/realtime/lms-realtime.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.service.d.ts +1 -1
- package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.service.js.map +1 -1
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
- package/dist/student-xp/student-xp.controller.d.ts +15 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
- package/dist/student-xp/student-xp.controller.js +24 -0
- package/dist/student-xp/student-xp.controller.js.map +1 -1
- package/dist/student-xp/student-xp.service.d.ts +16 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -1
- package/dist/student-xp/student-xp.service.js +51 -1
- package/dist/student-xp/student-xp.service.js.map +1 -1
- package/hedhog/data/evaluation_topic.yaml +17 -0
- package/hedhog/data/menu.yaml +0 -17
- package/hedhog/data/queue_definition.yaml +48 -0
- package/hedhog/data/route.yaml +94 -124
- package/hedhog/data/setting_group.yaml +19 -19
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
- package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
- package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +158 -45
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
- package/hedhog/frontend/app/courses/page.tsx.ejs +85 -9
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
- package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
- package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
- package/hedhog/frontend/messages/en.json +44 -28
- package/hedhog/frontend/messages/pt.json +47 -29
- package/hedhog/table/course_enrollment.yaml +3 -0
- package/hedhog/table/course_export.yaml +62 -0
- package/hedhog/table/lesson_view_event.yaml +66 -0
- package/package.json +14 -9
- package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
- package/src/course/course-export-scorm12-worker.service.ts +124 -0
- package/src/course/course-export-scorm12.service.ts +668 -0
- package/src/course/course-export.service.ts +280 -0
- package/src/course/course-structure.controller.ts +16 -2
- package/src/course/course-structure.service.ts +100 -7
- package/src/course/course-video-agent-pipeline.service.ts +471 -0
- package/src/course/course-video-hls.service.ts +966 -0
- package/src/course/course.controller.ts +33 -19
- package/src/course/course.mcp-tools.ts +1 -1
- package/src/course/course.module.ts +16 -0
- package/src/course/course.service.ts +119 -61
- package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
- package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
- package/src/course/dto/create-course-export.dto.ts +56 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
- package/src/course/ffmpeg.util.ts +65 -0
- package/src/course/lms-bulk-upload-automation.service.ts +156 -6
- package/src/course/lms-bulk-upload-infra.service.ts +39 -6
- package/src/course/lms-bulk-upload.controller.ts +32 -2
- package/src/course/lms-bulk-upload.service.ts +70 -7
- package/src/course/lms-setting.controller.ts +4 -2
- package/src/course/scorm12-schemas.ts +9 -0
- package/src/enterprise/training/training-student.service.ts +221 -2
- package/src/evaluation/evaluation.service.ts +123 -0
- package/src/exam/dto/create-standalone-question.dto.ts +66 -0
- package/src/exam/exam.module.ts +2 -1
- package/src/exam/exam.service.ts +86 -0
- package/src/exam/question.controller.ts +28 -0
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
- package/src/lms-commerce-access.subscriber.ts +88 -0
- package/src/lms.module.ts +16 -5
- package/src/platforma/dto/heartbeat.dto.ts +30 -0
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
- package/src/platforma/platforma-heartbeat.service.ts +33 -0
- package/src/platforma/platforma-performance.service.ts +606 -0
- package/src/platforma/platforma-search.service.ts +48 -0
- package/src/platforma/platforma-video.service.ts +346 -0
- package/src/platforma/platforma.controller.ts +137 -1
- package/src/platforma/platforma.service.ts +268 -268
- package/src/realtime/lms-realtime.controller.ts +27 -1
- package/src/realtime/lms-realtime.service.ts +2 -1
- package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
- package/src/student-xp/student-xp.controller.ts +18 -2
- package/src/student-xp/student-xp.service.ts +84 -2
- package/hedhog/data/video_resolution_profile.yaml +0 -7
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
- package/hedhog/table/course_video_resolution_profile.yaml +0 -22
- package/hedhog/table/video_resolution_profile.yaml +0 -18
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
- package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
- package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
ChevronUp,
|
|
9
9
|
CircleDot,
|
|
10
10
|
CircleOff,
|
|
11
|
-
ClipboardList,
|
|
12
11
|
Clock,
|
|
13
12
|
Download,
|
|
14
13
|
ExternalLink,
|
|
@@ -41,7 +40,6 @@ import { z } from 'zod';
|
|
|
41
40
|
|
|
42
41
|
import { CopyButton } from '@/components/copy-button';
|
|
43
42
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
44
|
-
import { Badge } from '@/components/ui/badge';
|
|
45
43
|
import { Button } from '@/components/ui/button';
|
|
46
44
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
47
45
|
import {
|
|
@@ -65,7 +63,6 @@ import { Label } from '@/components/ui/label';
|
|
|
65
63
|
import { Progress } from '@/components/ui/progress';
|
|
66
64
|
import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
|
|
67
65
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
68
|
-
import { Skeleton } from '@/components/ui/skeleton';
|
|
69
66
|
import {
|
|
70
67
|
Select,
|
|
71
68
|
SelectContent,
|
|
@@ -80,6 +77,7 @@ import {
|
|
|
80
77
|
SheetHeader,
|
|
81
78
|
SheetTitle,
|
|
82
79
|
} from '@/components/ui/sheet';
|
|
80
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
83
81
|
import { Switch } from '@/components/ui/switch';
|
|
84
82
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
85
83
|
import { Textarea } from '@/components/ui/textarea';
|
|
@@ -115,11 +113,14 @@ import {
|
|
|
115
113
|
getQueueJob,
|
|
116
114
|
updateLessonFrame,
|
|
117
115
|
uploadFile,
|
|
116
|
+
type ApiQuestion,
|
|
118
117
|
type QueueJobResponse,
|
|
119
118
|
type QueueJobStatus,
|
|
120
119
|
} from '../_data/services/course-structure.service';
|
|
121
120
|
import {
|
|
121
|
+
useCreateQuestionMutation,
|
|
122
122
|
useDeleteLessonMutation,
|
|
123
|
+
useQuestionsQuery,
|
|
123
124
|
useUpdateLessonMutation,
|
|
124
125
|
useUpdateResourceTypeMutation,
|
|
125
126
|
} from '../_data/use-course-structure-mutations';
|
|
@@ -187,10 +188,6 @@ function getInstructorAvatarUrl(avatarId?: number | null): string | undefined {
|
|
|
187
188
|
: `/person/avatar/${avatarId}`;
|
|
188
189
|
}
|
|
189
190
|
|
|
190
|
-
function videoProfileResourceType(profileId: number): string {
|
|
191
|
-
return `video_profile:${profileId}`;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
191
|
const MAX_VIDEO_UPLOAD_SIZE_BYTES = 100 * 1024 * 1024;
|
|
195
192
|
|
|
196
193
|
type LessonTypeLabelKey = `types.${LessonType}`;
|
|
@@ -205,7 +202,8 @@ type VideoJobEventMessageKey = `lessonForm.videoJobEvents.${VideoJobEventType}`;
|
|
|
205
202
|
type VideoJobProgressPhase =
|
|
206
203
|
| 'download_original'
|
|
207
204
|
| 'probe_duration'
|
|
208
|
-
| '
|
|
205
|
+
| 'hls_encode'
|
|
206
|
+
| 'hls_upload'
|
|
209
207
|
| 'extract_frames'
|
|
210
208
|
| 'extract_frames_done'
|
|
211
209
|
| 'extract_audio'
|
|
@@ -317,7 +315,8 @@ const VIDEO_JOB_STATUS_COLORS: Record<QueueJobStatus, string> = {
|
|
|
317
315
|
const VIDEO_JOB_PROGRESS_PHASES = new Set<VideoJobProgressPhase>([
|
|
318
316
|
'download_original',
|
|
319
317
|
'probe_duration',
|
|
320
|
-
'
|
|
318
|
+
'hls_encode',
|
|
319
|
+
'hls_upload',
|
|
321
320
|
'extract_frames',
|
|
322
321
|
'extract_frames_done',
|
|
323
322
|
'extract_audio',
|
|
@@ -410,19 +409,6 @@ function getVideoJobProgressMessage(
|
|
|
410
409
|
const phase = getVideoJobProgressPhase(event);
|
|
411
410
|
const metadata = event.metadata ?? {};
|
|
412
411
|
|
|
413
|
-
if (phase === 'convert_profile') {
|
|
414
|
-
const profileName =
|
|
415
|
-
typeof metadata.profileName === 'string' && metadata.profileName.trim()
|
|
416
|
-
? metadata.profileName.trim()
|
|
417
|
-
: typeof metadata.profileId === 'number'
|
|
418
|
-
? `#${metadata.profileId}`
|
|
419
|
-
: '—';
|
|
420
|
-
|
|
421
|
-
return t('lessonForm.videoJobProgress.convert_profile', {
|
|
422
|
-
profileName,
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
412
|
if (phase === 'extract_frames_done') {
|
|
427
413
|
const count =
|
|
428
414
|
typeof metadata.frames === 'number' && Number.isFinite(metadata.frames)
|
|
@@ -504,12 +490,6 @@ const TYPE_CONFIG: Record<
|
|
|
504
490
|
bg: 'bg-amber-500/10',
|
|
505
491
|
labelKey: 'types.questao',
|
|
506
492
|
},
|
|
507
|
-
exercicio: {
|
|
508
|
-
icon: ClipboardList,
|
|
509
|
-
color: 'text-purple-500',
|
|
510
|
-
bg: 'bg-purple-500/10',
|
|
511
|
-
labelKey: 'types.exercicio',
|
|
512
|
-
},
|
|
513
493
|
};
|
|
514
494
|
|
|
515
495
|
const STATUS_COLORS: Record<LessonStatus, string> = {
|
|
@@ -553,39 +533,15 @@ function generateAltId(): string {
|
|
|
553
533
|
return Math.random().toString(36).slice(2, 9);
|
|
554
534
|
}
|
|
555
535
|
|
|
556
|
-
|
|
557
|
-
{
|
|
558
|
-
id:
|
|
559
|
-
title:
|
|
560
|
-
type: 'multiple_choice',
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
texto: 'Uma biblioteca JavaScript para interfaces',
|
|
566
|
-
correta: true,
|
|
567
|
-
},
|
|
568
|
-
{ id: 'a2', texto: 'Um framework CSS', correta: false },
|
|
569
|
-
{ id: 'a3', texto: 'Um banco de dados', correta: false },
|
|
570
|
-
],
|
|
571
|
-
},
|
|
572
|
-
{
|
|
573
|
-
id: 'q2',
|
|
574
|
-
title: 'Defina componente funcional',
|
|
575
|
-
type: 'essay',
|
|
576
|
-
points: 2,
|
|
577
|
-
},
|
|
578
|
-
{
|
|
579
|
-
id: 'q3',
|
|
580
|
-
title: 'TypeScript é um superset de JavaScript?',
|
|
581
|
-
type: 'true_false',
|
|
582
|
-
points: 1,
|
|
583
|
-
alternatives: [
|
|
584
|
-
{ id: 'true', texto: 'Verdadeiro', correta: true },
|
|
585
|
-
{ id: 'false', texto: 'Falso', correta: false },
|
|
586
|
-
],
|
|
587
|
-
},
|
|
588
|
-
];
|
|
536
|
+
function apiQuestionToMock(q: ApiQuestion): MockQuestion {
|
|
537
|
+
return {
|
|
538
|
+
id: String(q.id),
|
|
539
|
+
title: q.statement.replace(/<[^>]+>/g, '').slice(0, 80),
|
|
540
|
+
type: (q.questionType as QuestionType) ?? 'multiple_choice',
|
|
541
|
+
statement: q.statement,
|
|
542
|
+
points: q.points,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
589
545
|
|
|
590
546
|
type FormValues = {
|
|
591
547
|
code: string;
|
|
@@ -600,7 +556,7 @@ type FormValues = {
|
|
|
600
556
|
videoUrl?: string;
|
|
601
557
|
transcription?: string;
|
|
602
558
|
postContent?: string;
|
|
603
|
-
|
|
559
|
+
linkedExam?: string | null;
|
|
604
560
|
};
|
|
605
561
|
|
|
606
562
|
type EditableTranscriptionSegment = {
|
|
@@ -792,17 +748,23 @@ function SortableAlternativa({
|
|
|
792
748
|
|
|
793
749
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
794
750
|
|
|
751
|
+
const LESSON_TABS: LessonEditorTab[] = [
|
|
752
|
+
'dados',
|
|
753
|
+
'conteudo',
|
|
754
|
+
'videos',
|
|
755
|
+
'imagens',
|
|
756
|
+
'transcricao',
|
|
757
|
+
'audios',
|
|
758
|
+
'recursos',
|
|
759
|
+
'xp',
|
|
760
|
+
];
|
|
761
|
+
|
|
795
762
|
interface EditorLessonProps {
|
|
796
763
|
lessonId: string;
|
|
764
|
+
defaultTab?: string;
|
|
765
|
+
onTabChange?: (tab: string) => void;
|
|
797
766
|
}
|
|
798
767
|
|
|
799
|
-
type VideoProfileOption = {
|
|
800
|
-
id: number;
|
|
801
|
-
name: string;
|
|
802
|
-
ffmpeg_params: string;
|
|
803
|
-
status: string;
|
|
804
|
-
};
|
|
805
|
-
|
|
806
768
|
type FramePreviewSource = {
|
|
807
769
|
key: string;
|
|
808
770
|
label: string;
|
|
@@ -819,7 +781,7 @@ type FrameAssetMetadata = {
|
|
|
819
781
|
sizeLabel: string;
|
|
820
782
|
};
|
|
821
783
|
|
|
822
|
-
export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
784
|
+
export function EditorLesson({ lessonId, defaultTab, onTabChange }: EditorLessonProps) {
|
|
823
785
|
const t = useTranslations('lms.CoursesPage.StructurePage');
|
|
824
786
|
const tabAudiosLabel = (() => {
|
|
825
787
|
try {
|
|
@@ -881,11 +843,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
881
843
|
fill_blank: t('questionEditor.types.fillBlank'),
|
|
882
844
|
matching: t('questionEditor.types.matching'),
|
|
883
845
|
};
|
|
884
|
-
const isVideoConversionEnabled = lmsSettings.videoConversionEnabled;
|
|
885
846
|
const schema = z.object({
|
|
886
847
|
code: z.string().min(1, t('questionEditor.validation.codeRequired')),
|
|
887
848
|
title: z.string().min(1, t('questionEditor.validation.titleRequired')),
|
|
888
|
-
type: z.enum(['video', 'post', 'questao'
|
|
849
|
+
type: z.enum(['video', 'post', 'questao'] as const),
|
|
889
850
|
duration: z.coerce.number().min(0),
|
|
890
851
|
status: z.enum([
|
|
891
852
|
'preparada',
|
|
@@ -903,7 +864,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
903
864
|
videoUrl: z.string().optional(),
|
|
904
865
|
transcription: z.string().optional(),
|
|
905
866
|
postContent: z.string().optional(),
|
|
906
|
-
|
|
867
|
+
linkedExam: z.string().nullable().optional(),
|
|
907
868
|
});
|
|
908
869
|
|
|
909
870
|
const instructorPool =
|
|
@@ -927,7 +888,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
927
888
|
videoUrl: lesson?.videoUrl ?? '',
|
|
928
889
|
transcription: lesson?.transcription ?? '',
|
|
929
890
|
postContent: lesson?.postContent ?? '',
|
|
930
|
-
|
|
891
|
+
linkedExam: lesson?.linkedExam ?? null,
|
|
931
892
|
});
|
|
932
893
|
|
|
933
894
|
const form = useForm<FormValues>({
|
|
@@ -938,6 +899,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
938
899
|
const { isDirty } = form.formState;
|
|
939
900
|
const watchedType = useWatch({ control: form.control, name: 'type' });
|
|
940
901
|
const watchedStatus = useWatch({ control: form.control, name: 'status' });
|
|
902
|
+
const watchedPublished = useWatch({
|
|
903
|
+
control: form.control,
|
|
904
|
+
name: 'published',
|
|
905
|
+
});
|
|
941
906
|
const watchedVideoProvider = useWatch({
|
|
942
907
|
control: form.control,
|
|
943
908
|
name: 'videoProvider',
|
|
@@ -953,7 +918,11 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
953
918
|
() => lesson?.resources ?? []
|
|
954
919
|
);
|
|
955
920
|
const [jobTimerNowMs, setJobTimerNowMs] = useState<number>(() => Date.now());
|
|
956
|
-
const [activeTab, setActiveTab] = useState<LessonEditorTab>(
|
|
921
|
+
const [activeTab, setActiveTab] = useState<LessonEditorTab>(() =>
|
|
922
|
+
defaultTab && LESSON_TABS.includes(defaultTab as LessonEditorTab)
|
|
923
|
+
? (defaultTab as LessonEditorTab)
|
|
924
|
+
: 'dados'
|
|
925
|
+
);
|
|
957
926
|
const [isJobFeedbackCollapsed, setIsJobFeedbackCollapsed] = useState<boolean>(
|
|
958
927
|
() => readVideoJobFeedbackCollapsedPreference()
|
|
959
928
|
);
|
|
@@ -973,9 +942,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
973
942
|
>(null);
|
|
974
943
|
const [isRequeueingOriginalVideo, setIsRequeueingOriginalVideo] =
|
|
975
944
|
useState(false);
|
|
976
|
-
const [profileUploadProgress, setProfileUploadProgress] = useState<
|
|
977
|
-
Record<number, number>
|
|
978
|
-
>({});
|
|
979
945
|
const [isResolvingVideoPreview, setIsResolvingVideoPreview] = useState(false);
|
|
980
946
|
const [videoPreviewOpen, setVideoPreviewOpen] = useState(false);
|
|
981
947
|
const [videoPreviewResource, setVideoPreviewResource] =
|
|
@@ -1113,24 +1079,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1113
1079
|
isLoading: isLoadingTranscription,
|
|
1114
1080
|
} = useTranscriptionSegmentsQuery(lesson?.id ?? null);
|
|
1115
1081
|
|
|
1116
|
-
const {
|
|
1117
|
-
data: courseVideoProfiles = [],
|
|
1118
|
-
isFetching: isFetchingCourseVideoProfiles,
|
|
1119
|
-
isError: hasCourseVideoProfilesError,
|
|
1120
|
-
refetch: refetchCourseVideoProfiles,
|
|
1121
|
-
} = useQuery<VideoProfileOption[]>({
|
|
1122
|
-
queryKey: ['lms-course-video-resolution-profiles', courseId],
|
|
1123
|
-
queryFn: async () => {
|
|
1124
|
-
const response = await request<VideoProfileOption[]>({
|
|
1125
|
-
url: `/lms/courses/${courseId}/video-resolution-profiles`,
|
|
1126
|
-
method: 'GET',
|
|
1127
|
-
});
|
|
1128
|
-
return response.data ?? [];
|
|
1129
|
-
},
|
|
1130
|
-
enabled: Boolean(courseId),
|
|
1131
|
-
initialData: [],
|
|
1132
|
-
});
|
|
1133
|
-
|
|
1134
1082
|
const {
|
|
1135
1083
|
data: conversionJob,
|
|
1136
1084
|
isFetching: isFetchingConversionJob,
|
|
@@ -1180,13 +1128,19 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1180
1128
|
);
|
|
1181
1129
|
|
|
1182
1130
|
// ── Question sheet state ────────────────────────────────────────────────────
|
|
1131
|
+
const { data: questionsData, isLoading: questionsLoading } = useQuestionsQuery();
|
|
1132
|
+
const createQuestionMutation = useCreateQuestionMutation();
|
|
1183
1133
|
const [questionSheetOpen, setQuestionSheetOpen] = useState(false);
|
|
1184
1134
|
const [editingQuestion, setEditingQuestion] = useState<MockQuestion | null>(
|
|
1185
1135
|
null
|
|
1186
1136
|
);
|
|
1187
|
-
const [selectedQuestion, setSelectedQuestion] = useState<MockQuestion | null>(
|
|
1188
|
-
|
|
1189
|
-
)
|
|
1137
|
+
const [selectedQuestion, setSelectedQuestion] = useState<MockQuestion | null>(null);
|
|
1138
|
+
|
|
1139
|
+
useEffect(() => {
|
|
1140
|
+
if (!lesson?.linkedExam || !questionsData) return;
|
|
1141
|
+
const found = questionsData.data.find((q) => String(q.id) === lesson.linkedExam);
|
|
1142
|
+
if (found) setSelectedQuestion(apiQuestionToMock(found));
|
|
1143
|
+
}, [lesson?.linkedExam, questionsData]);
|
|
1190
1144
|
const [qSheetStatement, setQSheetStatement] = useState('');
|
|
1191
1145
|
const [qSheetType, setQSheetType] = useState<QuestionType>('multiple_choice');
|
|
1192
1146
|
const [qSheetPoints, setQSheetPoints] = useState(1);
|
|
@@ -1479,11 +1433,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1479
1433
|
String(originalVideoResource.fileId ?? originalVideoResource.id)
|
|
1480
1434
|
)
|
|
1481
1435
|
: false;
|
|
1482
|
-
const profileVideoResources = new Map(
|
|
1483
|
-
localResources
|
|
1484
|
-
.filter((res) => res.type.startsWith('video_profile:'))
|
|
1485
|
-
.map((res) => [Number(res.type.replace('video_profile:', '')), res])
|
|
1486
|
-
);
|
|
1487
1436
|
const audioResources = localResources.filter(
|
|
1488
1437
|
(res) => res.type === 'lesson_audio'
|
|
1489
1438
|
);
|
|
@@ -1585,7 +1534,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1585
1534
|
persistedVideoProvider !== 'file_storage');
|
|
1586
1535
|
const canRequeueSavedOriginalVideo =
|
|
1587
1536
|
Boolean(originalVideoResource?.fileId) && !isOriginalVideoUploadBlocked;
|
|
1588
|
-
const isProfileVideoUploadBlocked = isConversionJobActive;
|
|
1589
1537
|
const currentQueueJobId =
|
|
1590
1538
|
focusedPipelineJob?.id ?? transcriptionJobId ?? conversionJobId;
|
|
1591
1539
|
const isTranscriptionJobActive = transcriptionJob
|
|
@@ -1626,18 +1574,6 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1626
1574
|
},
|
|
1627
1575
|
]
|
|
1628
1576
|
: []),
|
|
1629
|
-
...courseVideoProfiles.flatMap((profile) => {
|
|
1630
|
-
const resource = profileVideoResources.get(profile.id);
|
|
1631
|
-
if (!resource) return [];
|
|
1632
|
-
|
|
1633
|
-
return [
|
|
1634
|
-
{
|
|
1635
|
-
key: `profile:${profile.id}`,
|
|
1636
|
-
label: profile.name,
|
|
1637
|
-
resource,
|
|
1638
|
-
},
|
|
1639
|
-
];
|
|
1640
|
-
}),
|
|
1641
1577
|
];
|
|
1642
1578
|
const activeFramePreviewSource =
|
|
1643
1579
|
framePreviewSources.find(
|
|
@@ -2396,63 +2332,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2396
2332
|
return frameAssetMetadataById[frame.id]?.sizeLabel ?? '—';
|
|
2397
2333
|
}
|
|
2398
2334
|
|
|
2399
|
-
async function handleVideoProfileFile(profileId: number, file: File) {
|
|
2400
|
-
if (file.size > MAX_VIDEO_UPLOAD_SIZE_BYTES) {
|
|
2401
|
-
const message = t('lessonForm.videoUploadMaxSizeError', {
|
|
2402
|
-
size: '100MB',
|
|
2403
|
-
});
|
|
2404
|
-
setVideoUploadError(message);
|
|
2405
|
-
toast.error(message);
|
|
2406
|
-
return;
|
|
2407
|
-
}
|
|
2408
|
-
|
|
2409
|
-
setVideoUploadError(null);
|
|
2410
|
-
setProfileUploadProgress((prev) => ({ ...prev, [profileId]: 0 }));
|
|
2411
|
-
try {
|
|
2412
|
-
const uploaded = await uploadFile(request, file, 'lms/lessons/videos', {
|
|
2413
|
-
onUploadProgress: (event) => {
|
|
2414
|
-
const total = event.total ?? 0;
|
|
2415
|
-
const progress =
|
|
2416
|
-
total > 0 ? Math.round((event.loaded / total) * 100) : 0;
|
|
2417
|
-
setProfileUploadProgress((prev) => ({
|
|
2418
|
-
...prev,
|
|
2419
|
-
[profileId]: progress,
|
|
2420
|
-
}));
|
|
2421
|
-
},
|
|
2422
|
-
});
|
|
2423
|
-
const type = videoProfileResourceType(profileId);
|
|
2424
|
-
const resource: Resource = {
|
|
2425
|
-
id: `new-${uploaded.id}`,
|
|
2426
|
-
fileId: uploaded.id,
|
|
2427
|
-
name: file.name,
|
|
2428
|
-
size: formatFileSize(file.size),
|
|
2429
|
-
type,
|
|
2430
|
-
public: false,
|
|
2431
|
-
uploadedAt: new Date().toISOString(),
|
|
2432
|
-
url: undefined,
|
|
2433
|
-
};
|
|
2434
|
-
setLocalResources((prev) => [
|
|
2435
|
-
...prev.filter((item) => item.type !== type),
|
|
2436
|
-
resource,
|
|
2437
|
-
]);
|
|
2438
|
-
setResourcesDirty(true);
|
|
2439
|
-
} catch {
|
|
2440
|
-
toast.error(t('questionEditor.videoUploadFailed', { count: 1 }));
|
|
2441
|
-
} finally {
|
|
2442
|
-
setProfileUploadProgress((prev) => {
|
|
2443
|
-
const next = { ...prev };
|
|
2444
|
-
delete next[profileId];
|
|
2445
|
-
return next;
|
|
2446
|
-
});
|
|
2447
|
-
}
|
|
2448
|
-
}
|
|
2449
|
-
|
|
2450
2335
|
async function handleOriginalVideoFile(file: File) {
|
|
2451
|
-
if (!isVideoConversionEnabled) {
|
|
2452
|
-
toast.error(t('lessonForm.videoConversionFailed'));
|
|
2453
|
-
return;
|
|
2454
|
-
}
|
|
2455
|
-
|
|
2456
2336
|
if (
|
|
2457
2337
|
watchedVideoProvider === 'file_storage' &&
|
|
2458
2338
|
persistedVideoProvider !== 'file_storage'
|
|
@@ -2579,6 +2459,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2579
2459
|
videoConversionJobId: conversionJobId ?? lesson?.videoConversionJobId,
|
|
2580
2460
|
resources: localResources,
|
|
2581
2461
|
instructorIds: selectedInstructorIds.map(Number),
|
|
2462
|
+
linkedExam: values.linkedExam ?? undefined,
|
|
2582
2463
|
},
|
|
2583
2464
|
},
|
|
2584
2465
|
{
|
|
@@ -2696,28 +2577,43 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2696
2577
|
setQSheetErrors(errors);
|
|
2697
2578
|
return;
|
|
2698
2579
|
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
title: qSheetStatement.replace(/<[^>]+>/g, '').slice(0, 80),
|
|
2702
|
-
type: qSheetType,
|
|
2580
|
+
|
|
2581
|
+
const dto = {
|
|
2703
2582
|
statement: qSheetStatement,
|
|
2583
|
+
questionType: qSheetType,
|
|
2704
2584
|
points: qSheetPoints,
|
|
2705
2585
|
alternatives:
|
|
2706
2586
|
qSheetType === 'multiple_choice' || qSheetType === 'true_false'
|
|
2707
|
-
? qSheetAlts
|
|
2587
|
+
? qSheetAlts.map((a) => ({ text: a.texto, isCorrect: a.correta }))
|
|
2708
2588
|
: undefined,
|
|
2709
2589
|
fillBlankAnswers:
|
|
2710
|
-
qSheetType === 'fill_blank'
|
|
2711
|
-
|
|
2590
|
+
qSheetType === 'fill_blank'
|
|
2591
|
+
? qSheetFillBlanks.map((f) => ({
|
|
2592
|
+
answer: f.answer,
|
|
2593
|
+
alternatives: f.alternativesText
|
|
2594
|
+
? f.alternativesText.split(',').map((s) => s.trim()).filter(Boolean)
|
|
2595
|
+
: undefined,
|
|
2596
|
+
}))
|
|
2597
|
+
: undefined,
|
|
2598
|
+
matchingPairs:
|
|
2599
|
+
qSheetType === 'matching'
|
|
2600
|
+
? qSheetPairs.map((p) => ({ id: p.id, leftText: p.leftText, rightText: p.rightText }))
|
|
2601
|
+
: undefined,
|
|
2712
2602
|
};
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2603
|
+
|
|
2604
|
+
createQuestionMutation.mutate(dto, {
|
|
2605
|
+
onSuccess: (result) => {
|
|
2606
|
+
const mock = apiQuestionToMock(result);
|
|
2607
|
+
setSelectedQuestion(mock);
|
|
2608
|
+
form.setValue('linkedExam', String(result.id), { shouldDirty: true });
|
|
2609
|
+
setQuestionSheetOpen(false);
|
|
2610
|
+
toast.success(
|
|
2611
|
+
editingQuestion
|
|
2612
|
+
? t('questionEditor.updated')
|
|
2613
|
+
: t('questionEditor.created')
|
|
2614
|
+
);
|
|
2615
|
+
},
|
|
2616
|
+
});
|
|
2721
2617
|
}
|
|
2722
2618
|
|
|
2723
2619
|
function handleDelete() {
|
|
@@ -2807,7 +2703,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2807
2703
|
{/* ── Tabs ─────────────────────────────────────────────────────────── */}
|
|
2808
2704
|
<Tabs
|
|
2809
2705
|
value={activeTab}
|
|
2810
|
-
onValueChange={(value) =>
|
|
2706
|
+
onValueChange={(value) => {
|
|
2707
|
+
setActiveTab(value as LessonEditorTab);
|
|
2708
|
+
onTabChange?.(value);
|
|
2709
|
+
}}
|
|
2811
2710
|
className="flex flex-col flex-1 min-h-0 min-w-0"
|
|
2812
2711
|
>
|
|
2813
2712
|
<TabsList className="mt-0 h-auto w-full justify-start shrink-0 rounded-none border-b bg-muted/50 px-2 py-1 overflow-x-auto overflow-y-hidden whitespace-nowrap sm:px-3">
|
|
@@ -2990,48 +2889,57 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2990
2889
|
</CardTitle>
|
|
2991
2890
|
</CardHeader>
|
|
2992
2891
|
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
2993
|
-
<div
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
{
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
2892
|
+
<div
|
|
2893
|
+
className={cn(
|
|
2894
|
+
'grid grid-cols-1 gap-2',
|
|
2895
|
+
watchedType === 'video' && !watchedPublished
|
|
2896
|
+
? 'sm:grid-cols-2'
|
|
2897
|
+
: 'sm:grid-cols-1'
|
|
2898
|
+
)}
|
|
2899
|
+
>
|
|
2900
|
+
{watchedType === 'video' && !watchedPublished && (
|
|
2901
|
+
<FormField
|
|
2902
|
+
control={form.control}
|
|
2903
|
+
name="status"
|
|
2904
|
+
render={({ field }) => (
|
|
2905
|
+
<FormItem>
|
|
2906
|
+
<FormLabel className="text-xs">
|
|
2907
|
+
{t('questionEditor.productionStatus')}
|
|
2908
|
+
</FormLabel>
|
|
2909
|
+
<Select
|
|
2910
|
+
value={field.value}
|
|
2911
|
+
onValueChange={field.onChange}
|
|
2912
|
+
>
|
|
2913
|
+
<FormControl>
|
|
2914
|
+
<SelectTrigger className="h-8 text-xs w-full">
|
|
2915
|
+
<SelectValue />
|
|
2916
|
+
</SelectTrigger>
|
|
2917
|
+
</FormControl>
|
|
2918
|
+
<SelectContent>
|
|
2919
|
+
{(
|
|
2920
|
+
Object.entries(statusLabels) as [
|
|
2921
|
+
LessonStatus,
|
|
2922
|
+
string,
|
|
2923
|
+
][]
|
|
2924
|
+
).map(([val, lbl]) => (
|
|
2925
|
+
<SelectItem key={val} value={val}>
|
|
2926
|
+
<span
|
|
2927
|
+
className={cn(
|
|
2928
|
+
'text-xs px-1.5 py-0.5 rounded',
|
|
2929
|
+
STATUS_COLORS[val]
|
|
2930
|
+
)}
|
|
2931
|
+
>
|
|
2932
|
+
{lbl}
|
|
2933
|
+
</span>
|
|
2934
|
+
</SelectItem>
|
|
2935
|
+
))}
|
|
2936
|
+
</SelectContent>
|
|
2937
|
+
</Select>
|
|
2938
|
+
<FormMessage className="text-xs" />
|
|
2939
|
+
</FormItem>
|
|
2940
|
+
)}
|
|
2941
|
+
/>
|
|
2942
|
+
)}
|
|
3035
2943
|
|
|
3036
2944
|
<FormField
|
|
3037
2945
|
control={form.control}
|
|
@@ -3311,20 +3219,25 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3311
3219
|
<CardContent className="px-3 pb-2 flex flex-col gap-2">
|
|
3312
3220
|
<FormField
|
|
3313
3221
|
control={form.control}
|
|
3314
|
-
name="
|
|
3222
|
+
name="linkedExam"
|
|
3315
3223
|
render={({ field }) => (
|
|
3316
3224
|
<FormItem>
|
|
3317
3225
|
<FormControl>
|
|
3318
|
-
<EntityPicker<
|
|
3226
|
+
<EntityPicker<ApiQuestion>
|
|
3319
3227
|
value={field.value ?? null}
|
|
3320
3228
|
onChange={(val) => {
|
|
3321
3229
|
field.onChange(val);
|
|
3322
3230
|
const found =
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3231
|
+
(questionsData?.data ?? []).find(
|
|
3232
|
+
(q) => String(q.id) === val
|
|
3233
|
+
) ?? null;
|
|
3234
|
+
setSelectedQuestion(found ? apiQuestionToMock(found) : null);
|
|
3326
3235
|
}}
|
|
3327
|
-
placeholder={
|
|
3236
|
+
placeholder={
|
|
3237
|
+
questionsLoading
|
|
3238
|
+
? t('questionEditor.loadingQuestions')
|
|
3239
|
+
: t('questionEditor.selectQuestion')
|
|
3240
|
+
}
|
|
3328
3241
|
searchPlaceholder={t(
|
|
3329
3242
|
'questionEditor.searchQuestion'
|
|
3330
3243
|
)}
|
|
@@ -3332,11 +3245,14 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3332
3245
|
'questionEditor.noQuestionsFound'
|
|
3333
3246
|
)}
|
|
3334
3247
|
entityLabel={t('questionEditor.questionEntity')}
|
|
3335
|
-
options={
|
|
3336
|
-
getOptionValue={(o) => o.id}
|
|
3337
|
-
getOptionLabel={(o) =>
|
|
3248
|
+
options={questionsData?.data ?? []}
|
|
3249
|
+
getOptionValue={(o) => String(o.id)}
|
|
3250
|
+
getOptionLabel={(o) =>
|
|
3251
|
+
o.statement.replace(/<[^>]+>/g, '').slice(0, 80)
|
|
3252
|
+
}
|
|
3338
3253
|
getOptionDescription={(o) =>
|
|
3339
|
-
questionTypeLabels[o.
|
|
3254
|
+
questionTypeLabels[o.questionType as QuestionType] ??
|
|
3255
|
+
o.questionType
|
|
3340
3256
|
}
|
|
3341
3257
|
/>
|
|
3342
3258
|
</FormControl>
|
|
@@ -3461,181 +3377,140 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3461
3377
|
</p>
|
|
3462
3378
|
) : null}
|
|
3463
3379
|
|
|
3464
|
-
|
|
3465
|
-
<
|
|
3466
|
-
<
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
<
|
|
3472
|
-
<div className="
|
|
3473
|
-
<div className="flex items-
|
|
3474
|
-
<
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
<div className="
|
|
3478
|
-
<div className="
|
|
3479
|
-
<
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
{
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
<
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
'lessonForm.playVideoAria',
|
|
3517
|
-
{
|
|
3518
|
-
name: originalVideoResource.name,
|
|
3519
|
-
}
|
|
3520
|
-
)}
|
|
3521
|
-
>
|
|
3522
|
-
{isResolvingVideoPreview ? (
|
|
3523
|
-
<Loader2 className="size-3 animate-spin" />
|
|
3524
|
-
) : (
|
|
3525
|
-
<Play className="size-3" />
|
|
3526
|
-
)}
|
|
3527
|
-
</Button>
|
|
3528
|
-
</IconActionTooltip>
|
|
3529
|
-
<IconActionTooltip
|
|
3530
|
-
label={t(
|
|
3531
|
-
'lessonForm.downloadVideoAria',
|
|
3380
|
+
<Card className="bg-muted/20 py-2 gap-2 order-3">
|
|
3381
|
+
<CardHeader className="px-3 pt-2 pb-1">
|
|
3382
|
+
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
3383
|
+
{t('lessonForm.originalVideoTitle')}
|
|
3384
|
+
</CardTitle>
|
|
3385
|
+
</CardHeader>
|
|
3386
|
+
<CardContent className="px-3 pb-2 flex flex-col gap-3">
|
|
3387
|
+
<div className="rounded-lg border bg-background/90 p-3 shadow-sm">
|
|
3388
|
+
<div className="flex items-start gap-3">
|
|
3389
|
+
<div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10 text-blue-600">
|
|
3390
|
+
<Video className="size-4" />
|
|
3391
|
+
</div>
|
|
3392
|
+
<div className="min-w-0 flex-1 space-y-1">
|
|
3393
|
+
<div className="flex items-start justify-between gap-2">
|
|
3394
|
+
<div className="min-w-0">
|
|
3395
|
+
<p className="truncate text-sm font-medium">
|
|
3396
|
+
{originalVideoResource
|
|
3397
|
+
? originalVideoResource.name
|
|
3398
|
+
: t('lessonForm.originalVideoTitle')}
|
|
3399
|
+
</p>
|
|
3400
|
+
<p className="text-xs text-muted-foreground">
|
|
3401
|
+
{conversionJobId
|
|
3402
|
+
? t('lessonForm.videoConversionJob', {
|
|
3403
|
+
id: conversionJobId,
|
|
3404
|
+
})
|
|
3405
|
+
: t('lessonForm.originalVideoHint')}
|
|
3406
|
+
</p>
|
|
3407
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
3408
|
+
{t('lessonForm.originalVideoPurpose')}
|
|
3409
|
+
</p>
|
|
3410
|
+
</div>
|
|
3411
|
+
{originalVideoResource && (
|
|
3412
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
3413
|
+
<IconActionTooltip
|
|
3414
|
+
label={t('lessonForm.playVideoAria', {
|
|
3415
|
+
name: originalVideoResource.name,
|
|
3416
|
+
})}
|
|
3417
|
+
asWrapper={isResolvingVideoPreview}
|
|
3418
|
+
>
|
|
3419
|
+
<Button
|
|
3420
|
+
type="button"
|
|
3421
|
+
variant="ghost"
|
|
3422
|
+
size="icon"
|
|
3423
|
+
className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-emerald-600"
|
|
3424
|
+
disabled={isResolvingVideoPreview}
|
|
3425
|
+
onClick={() =>
|
|
3426
|
+
void openVideoPreview(
|
|
3427
|
+
originalVideoResource
|
|
3428
|
+
)
|
|
3429
|
+
}
|
|
3430
|
+
aria-label={t(
|
|
3431
|
+
'lessonForm.playVideoAria',
|
|
3532
3432
|
{
|
|
3533
3433
|
name: originalVideoResource.name,
|
|
3534
3434
|
}
|
|
3535
3435
|
)}
|
|
3536
|
-
asWrapper={isDownloadingOriginalVideo}
|
|
3537
3436
|
>
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
)
|
|
3550
|
-
}
|
|
3551
|
-
aria-label={t(
|
|
3552
|
-
'lessonForm.downloadVideoAria',
|
|
3553
|
-
{
|
|
3554
|
-
name: originalVideoResource.name,
|
|
3555
|
-
}
|
|
3556
|
-
)}
|
|
3557
|
-
>
|
|
3558
|
-
{isDownloadingOriginalVideo ? (
|
|
3559
|
-
<Loader2 className="size-3 animate-spin" />
|
|
3560
|
-
) : (
|
|
3561
|
-
<Download className="size-3" />
|
|
3562
|
-
)}
|
|
3563
|
-
</Button>
|
|
3564
|
-
</IconActionTooltip>
|
|
3565
|
-
<IconActionTooltip
|
|
3566
|
-
label={t('lessonForm.openVideoAria', {
|
|
3437
|
+
{isResolvingVideoPreview ? (
|
|
3438
|
+
<Loader2 className="size-3 animate-spin" />
|
|
3439
|
+
) : (
|
|
3440
|
+
<Play className="size-3" />
|
|
3441
|
+
)}
|
|
3442
|
+
</Button>
|
|
3443
|
+
</IconActionTooltip>
|
|
3444
|
+
<IconActionTooltip
|
|
3445
|
+
label={t(
|
|
3446
|
+
'lessonForm.downloadVideoAria',
|
|
3447
|
+
{
|
|
3567
3448
|
name: originalVideoResource.name,
|
|
3568
|
-
}
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
variant="ghost"
|
|
3573
|
-
size="icon"
|
|
3574
|
-
className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
|
|
3575
|
-
onClick={() =>
|
|
3576
|
-
void openResource(
|
|
3577
|
-
originalVideoResource
|
|
3578
|
-
)
|
|
3579
|
-
}
|
|
3580
|
-
aria-label={t(
|
|
3581
|
-
'lessonForm.openVideoAria',
|
|
3582
|
-
{
|
|
3583
|
-
name: originalVideoResource.name,
|
|
3584
|
-
}
|
|
3585
|
-
)}
|
|
3586
|
-
>
|
|
3587
|
-
<ExternalLink className="size-3" />
|
|
3588
|
-
</Button>
|
|
3589
|
-
</IconActionTooltip>
|
|
3590
|
-
</div>
|
|
3591
|
-
)}
|
|
3592
|
-
</div>
|
|
3593
|
-
{originalVideoResource?.size ? (
|
|
3594
|
-
<div className="inline-flex rounded-full bg-muted px-2 py-0.5 text-[0.65rem] font-medium text-muted-foreground">
|
|
3595
|
-
{originalVideoResource.size}
|
|
3596
|
-
</div>
|
|
3597
|
-
) : null}
|
|
3598
|
-
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
3599
|
-
{originalVideoResource ? (
|
|
3600
|
-
<>
|
|
3449
|
+
}
|
|
3450
|
+
)}
|
|
3451
|
+
asWrapper={isDownloadingOriginalVideo}
|
|
3452
|
+
>
|
|
3601
3453
|
<Button
|
|
3602
3454
|
type="button"
|
|
3603
|
-
variant="
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
}
|
|
3455
|
+
variant="ghost"
|
|
3456
|
+
size="icon"
|
|
3457
|
+
className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-amber-600"
|
|
3458
|
+
disabled={isDownloadingOriginalVideo}
|
|
3608
3459
|
onClick={() =>
|
|
3609
|
-
|
|
3460
|
+
void handleResourceDownload(
|
|
3461
|
+
originalVideoResource
|
|
3462
|
+
)
|
|
3610
3463
|
}
|
|
3464
|
+
aria-label={t(
|
|
3465
|
+
'lessonForm.downloadVideoAria',
|
|
3466
|
+
{
|
|
3467
|
+
name: originalVideoResource.name,
|
|
3468
|
+
}
|
|
3469
|
+
)}
|
|
3611
3470
|
>
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3471
|
+
{isDownloadingOriginalVideo ? (
|
|
3472
|
+
<Loader2 className="size-3 animate-spin" />
|
|
3473
|
+
) : (
|
|
3474
|
+
<Download className="size-3" />
|
|
3615
3475
|
)}
|
|
3616
3476
|
</Button>
|
|
3477
|
+
</IconActionTooltip>
|
|
3478
|
+
<IconActionTooltip
|
|
3479
|
+
label={t('lessonForm.openVideoAria', {
|
|
3480
|
+
name: originalVideoResource.name,
|
|
3481
|
+
})}
|
|
3482
|
+
>
|
|
3617
3483
|
<Button
|
|
3618
3484
|
type="button"
|
|
3619
|
-
variant="
|
|
3620
|
-
|
|
3621
|
-
|
|
3622
|
-
!canRequeueSavedOriginalVideo
|
|
3623
|
-
}
|
|
3485
|
+
variant="ghost"
|
|
3486
|
+
size="icon"
|
|
3487
|
+
className="size-7 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
|
|
3624
3488
|
onClick={() =>
|
|
3625
|
-
void
|
|
3489
|
+
void openResource(
|
|
3490
|
+
originalVideoResource
|
|
3491
|
+
)
|
|
3626
3492
|
}
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
)}
|
|
3633
|
-
{t(
|
|
3634
|
-
'lessonForm.retryConversionWithSavedOriginal'
|
|
3493
|
+
aria-label={t(
|
|
3494
|
+
'lessonForm.openVideoAria',
|
|
3495
|
+
{
|
|
3496
|
+
name: originalVideoResource.name,
|
|
3497
|
+
}
|
|
3635
3498
|
)}
|
|
3499
|
+
>
|
|
3500
|
+
<ExternalLink className="size-3" />
|
|
3636
3501
|
</Button>
|
|
3637
|
-
|
|
3638
|
-
|
|
3502
|
+
</IconActionTooltip>
|
|
3503
|
+
</div>
|
|
3504
|
+
)}
|
|
3505
|
+
</div>
|
|
3506
|
+
{originalVideoResource?.size ? (
|
|
3507
|
+
<div className="inline-flex rounded-full bg-muted px-2 py-0.5 text-[0.65rem] font-medium text-muted-foreground">
|
|
3508
|
+
{originalVideoResource.size}
|
|
3509
|
+
</div>
|
|
3510
|
+
) : null}
|
|
3511
|
+
<div className="flex flex-wrap items-center gap-2 pt-1">
|
|
3512
|
+
{originalVideoResource ? (
|
|
3513
|
+
<>
|
|
3639
3514
|
<Button
|
|
3640
3515
|
type="button"
|
|
3641
3516
|
variant="secondary"
|
|
@@ -3647,50 +3522,83 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
3647
3522
|
>
|
|
3648
3523
|
<UploadCloud className="size-3.5 mr-1" />
|
|
3649
3524
|
{t(
|
|
3650
|
-
'lessonForm.
|
|
3525
|
+
'lessonForm.replaceOriginalForConversion'
|
|
3651
3526
|
)}
|
|
3652
3527
|
</Button>
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3528
|
+
<Button
|
|
3529
|
+
type="button"
|
|
3530
|
+
variant="outline"
|
|
3531
|
+
className="h-8 px-3 text-xs"
|
|
3532
|
+
disabled={!canRequeueSavedOriginalVideo}
|
|
3533
|
+
onClick={() =>
|
|
3534
|
+
void handleRequeueOriginalVideo()
|
|
3535
|
+
}
|
|
3536
|
+
>
|
|
3537
|
+
{isRequeueingOriginalVideo ? (
|
|
3538
|
+
<Loader2 className="size-3.5 mr-1 animate-spin" />
|
|
3539
|
+
) : (
|
|
3540
|
+
<RefreshCw className="size-3.5 mr-1" />
|
|
3541
|
+
)}
|
|
3542
|
+
{t(
|
|
3543
|
+
'lessonForm.retryConversionWithSavedOriginal'
|
|
3544
|
+
)}
|
|
3545
|
+
</Button>
|
|
3546
|
+
</>
|
|
3547
|
+
) : (
|
|
3548
|
+
<Button
|
|
3549
|
+
type="button"
|
|
3550
|
+
variant="secondary"
|
|
3551
|
+
className="h-8 px-3 text-xs"
|
|
3552
|
+
disabled={isOriginalVideoUploadBlocked}
|
|
3553
|
+
onClick={() =>
|
|
3554
|
+
originalVideoInputRef.current?.click()
|
|
3555
|
+
}
|
|
3556
|
+
>
|
|
3557
|
+
<UploadCloud className="size-3.5 mr-1" />
|
|
3558
|
+
{t(
|
|
3559
|
+
'lessonForm.uploadOriginalForConversion'
|
|
3560
|
+
)}
|
|
3561
|
+
</Button>
|
|
3562
|
+
)}
|
|
3563
|
+
<span className="text-[0.65rem] text-muted-foreground">
|
|
3564
|
+
{isConversionJobActive
|
|
3565
|
+
? t(
|
|
3566
|
+
'lessonForm.videoUploadBlockedWhileProcessing'
|
|
3567
|
+
)
|
|
3568
|
+
: isConversionJobStatusResolving
|
|
3569
|
+
? t('lessonForm.videoJobStateLoading')
|
|
3570
|
+
: t('lessonForm.originalVideoHint')}
|
|
3571
|
+
</span>
|
|
3675
3572
|
</div>
|
|
3573
|
+
{originalUploadProgress !== null ? (
|
|
3574
|
+
<div className="space-y-1 pt-1">
|
|
3575
|
+
<Progress
|
|
3576
|
+
value={originalUploadProgress}
|
|
3577
|
+
className="h-1.5"
|
|
3578
|
+
/>
|
|
3579
|
+
<p className="text-[0.65rem] text-muted-foreground">
|
|
3580
|
+
{originalUploadProgress}%
|
|
3581
|
+
</p>
|
|
3582
|
+
</div>
|
|
3583
|
+
) : null}
|
|
3676
3584
|
</div>
|
|
3677
|
-
<input
|
|
3678
|
-
ref={originalVideoInputRef}
|
|
3679
|
-
type="file"
|
|
3680
|
-
accept="video/*"
|
|
3681
|
-
className="hidden"
|
|
3682
|
-
onChange={(event) => {
|
|
3683
|
-
const file = event.target.files?.[0];
|
|
3684
|
-
if (file && !isOriginalVideoUploadBlocked) {
|
|
3685
|
-
void handleOriginalVideoFile(file);
|
|
3686
|
-
}
|
|
3687
|
-
event.target.value = '';
|
|
3688
|
-
}}
|
|
3689
|
-
/>
|
|
3690
3585
|
</div>
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3586
|
+
<input
|
|
3587
|
+
ref={originalVideoInputRef}
|
|
3588
|
+
type="file"
|
|
3589
|
+
accept="video/*"
|
|
3590
|
+
className="hidden"
|
|
3591
|
+
onChange={(event) => {
|
|
3592
|
+
const file = event.target.files?.[0];
|
|
3593
|
+
if (file && !isOriginalVideoUploadBlocked) {
|
|
3594
|
+
void handleOriginalVideoFile(file);
|
|
3595
|
+
}
|
|
3596
|
+
event.target.value = '';
|
|
3597
|
+
}}
|
|
3598
|
+
/>
|
|
3599
|
+
</div>
|
|
3600
|
+
</CardContent>
|
|
3601
|
+
</Card>
|
|
3694
3602
|
|
|
3695
3603
|
{conversionJobId && !shouldHidePipelineCard ? (
|
|
3696
3604
|
<Card className="bg-muted/20 py-2 gap-2 order-4">
|
|
@@ -4047,259 +3955,43 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
4047
3955
|
<Card className="bg-muted/20 py-2 gap-2 order-2">
|
|
4048
3956
|
<CardHeader className="px-3 pt-2 pb-1">
|
|
4049
3957
|
<CardTitle className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
4050
|
-
{t('lessonForm.
|
|
3958
|
+
{t('lessonForm.hlsStatusTitle')}
|
|
4051
3959
|
</CardTitle>
|
|
4052
3960
|
</CardHeader>
|
|
4053
|
-
<CardContent className="px-3 pb-2
|
|
4054
|
-
{
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
<>
|
|
4082
|
-
{isConversionJobActive ? (
|
|
3961
|
+
<CardContent className="px-3 pb-2">
|
|
3962
|
+
{(() => {
|
|
3963
|
+
const hlsResource = localResources.find(
|
|
3964
|
+
(r) => r.type === 'video_hls'
|
|
3965
|
+
);
|
|
3966
|
+
if (hlsResource) {
|
|
3967
|
+
return (
|
|
3968
|
+
<div className="flex items-center gap-2">
|
|
3969
|
+
<div className="size-2 rounded-full bg-emerald-500 shrink-0" />
|
|
3970
|
+
<p className="text-xs text-emerald-700 dark:text-emerald-400">
|
|
3971
|
+
{t('lessonForm.hlsStatusReady')}
|
|
3972
|
+
</p>
|
|
3973
|
+
</div>
|
|
3974
|
+
);
|
|
3975
|
+
}
|
|
3976
|
+
if (isConversionJobActive) {
|
|
3977
|
+
return (
|
|
3978
|
+
<div className="flex items-center gap-2">
|
|
3979
|
+
<Loader2 className="size-3.5 animate-spin text-muted-foreground shrink-0" />
|
|
3980
|
+
<p className="text-xs text-muted-foreground">
|
|
3981
|
+
{t('lessonForm.hlsStatusProcessing')}
|
|
3982
|
+
</p>
|
|
3983
|
+
</div>
|
|
3984
|
+
);
|
|
3985
|
+
}
|
|
3986
|
+
return (
|
|
3987
|
+
<div className="flex items-center gap-2">
|
|
3988
|
+
<div className="size-2 rounded-full bg-muted-foreground/40 shrink-0" />
|
|
4083
3989
|
<p className="text-xs text-muted-foreground">
|
|
4084
|
-
{t(
|
|
4085
|
-
'lessonForm.videoProfilesLockedWhileProcessing'
|
|
4086
|
-
)}
|
|
3990
|
+
{t('lessonForm.hlsStatusPending')}
|
|
4087
3991
|
</p>
|
|
4088
|
-
) : null}
|
|
4089
|
-
<div className="flex flex-col gap-1">
|
|
4090
|
-
{courseVideoProfiles.map((profile) => {
|
|
4091
|
-
const res = profileVideoResources.get(
|
|
4092
|
-
profile.id
|
|
4093
|
-
);
|
|
4094
|
-
const hasVideo = Boolean(res);
|
|
4095
|
-
const isDownloadingResource = res
|
|
4096
|
-
? downloadingResourceKeys.has(
|
|
4097
|
-
String(res.fileId ?? res.id)
|
|
4098
|
-
)
|
|
4099
|
-
: false;
|
|
4100
|
-
const currentUploadProgress =
|
|
4101
|
-
profileUploadProgress[profile.id];
|
|
4102
|
-
const inputId = `lesson-video-profile-${profile.id}`;
|
|
4103
|
-
|
|
4104
|
-
return (
|
|
4105
|
-
<div
|
|
4106
|
-
key={profile.id}
|
|
4107
|
-
className="flex items-center gap-2 rounded-md border bg-background/80 px-2.5 py-2"
|
|
4108
|
-
>
|
|
4109
|
-
<Video
|
|
4110
|
-
className={cn(
|
|
4111
|
-
'size-3.5 shrink-0',
|
|
4112
|
-
hasVideo
|
|
4113
|
-
? 'text-emerald-600'
|
|
4114
|
-
: 'text-blue-600'
|
|
4115
|
-
)}
|
|
4116
|
-
/>
|
|
4117
|
-
<div className="flex-1 min-w-0">
|
|
4118
|
-
<p className="text-xs truncate font-medium">
|
|
4119
|
-
{profile.name}
|
|
4120
|
-
</p>
|
|
4121
|
-
{res ? (
|
|
4122
|
-
(() => {
|
|
4123
|
-
const metadata =
|
|
4124
|
-
resolveResourceMetadata(res);
|
|
4125
|
-
|
|
4126
|
-
return (
|
|
4127
|
-
<p className="text-[0.65rem] text-muted-foreground">
|
|
4128
|
-
{`${metadata.sizeLabel} · ${metadata.uploadedAtLabel}`}
|
|
4129
|
-
</p>
|
|
4130
|
-
);
|
|
4131
|
-
})()
|
|
4132
|
-
) : (
|
|
4133
|
-
<Badge className="mt-1 inline-flex items-center gap-1 border border-blue-200 bg-blue-100 text-blue-700 hover:bg-blue-100 dark:border-blue-800 dark:bg-blue-950/40 dark:text-blue-200 dark:hover:bg-blue-950/50">
|
|
4134
|
-
{isConversionJobActive ? (
|
|
4135
|
-
<Loader2 className="size-3 animate-spin" />
|
|
4136
|
-
) : (
|
|
4137
|
-
<Clock className="size-3" />
|
|
4138
|
-
)}
|
|
4139
|
-
{t('lessonForm.awaitingConversion')}
|
|
4140
|
-
</Badge>
|
|
4141
|
-
)}
|
|
4142
|
-
{currentUploadProgress !== undefined ? (
|
|
4143
|
-
<div className="mt-1 space-y-1">
|
|
4144
|
-
<Progress
|
|
4145
|
-
value={currentUploadProgress}
|
|
4146
|
-
className="h-1.5"
|
|
4147
|
-
/>
|
|
4148
|
-
<p className="text-[0.65rem] text-muted-foreground">
|
|
4149
|
-
{currentUploadProgress}%
|
|
4150
|
-
</p>
|
|
4151
|
-
</div>
|
|
4152
|
-
) : null}
|
|
4153
|
-
</div>
|
|
4154
|
-
<input
|
|
4155
|
-
id={inputId}
|
|
4156
|
-
type="file"
|
|
4157
|
-
accept="video/*"
|
|
4158
|
-
className="hidden"
|
|
4159
|
-
onChange={(event) => {
|
|
4160
|
-
const file = event.target.files?.[0];
|
|
4161
|
-
if (
|
|
4162
|
-
file &&
|
|
4163
|
-
!isProfileVideoUploadBlocked
|
|
4164
|
-
) {
|
|
4165
|
-
void handleVideoProfileFile(
|
|
4166
|
-
profile.id,
|
|
4167
|
-
file
|
|
4168
|
-
);
|
|
4169
|
-
}
|
|
4170
|
-
event.target.value = '';
|
|
4171
|
-
}}
|
|
4172
|
-
/>
|
|
4173
|
-
<Button
|
|
4174
|
-
type="button"
|
|
4175
|
-
variant="outline"
|
|
4176
|
-
size="sm"
|
|
4177
|
-
className="h-7 px-2 text-xs"
|
|
4178
|
-
disabled={
|
|
4179
|
-
currentUploadProgress !== undefined ||
|
|
4180
|
-
isProfileVideoUploadBlocked
|
|
4181
|
-
}
|
|
4182
|
-
onClick={() =>
|
|
4183
|
-
document
|
|
4184
|
-
.getElementById(inputId)
|
|
4185
|
-
?.click()
|
|
4186
|
-
}
|
|
4187
|
-
>
|
|
4188
|
-
<UploadCloud className="size-3 mr-1" />
|
|
4189
|
-
{res
|
|
4190
|
-
? t('lessonForm.replaceVideo')
|
|
4191
|
-
: t('lessonForm.upload')}
|
|
4192
|
-
</Button>
|
|
4193
|
-
{res && (
|
|
4194
|
-
<>
|
|
4195
|
-
<IconActionTooltip
|
|
4196
|
-
label={t(
|
|
4197
|
-
'lessonForm.playVideoAria',
|
|
4198
|
-
{ name: res.name }
|
|
4199
|
-
)}
|
|
4200
|
-
asWrapper={isResolvingVideoPreview}
|
|
4201
|
-
>
|
|
4202
|
-
<Button
|
|
4203
|
-
type="button"
|
|
4204
|
-
variant="ghost"
|
|
4205
|
-
size="icon"
|
|
4206
|
-
className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-emerald-600"
|
|
4207
|
-
disabled={isResolvingVideoPreview}
|
|
4208
|
-
onClick={() =>
|
|
4209
|
-
void openVideoPreview(res)
|
|
4210
|
-
}
|
|
4211
|
-
aria-label={t(
|
|
4212
|
-
'lessonForm.playVideoAria',
|
|
4213
|
-
{ name: res.name }
|
|
4214
|
-
)}
|
|
4215
|
-
>
|
|
4216
|
-
{isResolvingVideoPreview ? (
|
|
4217
|
-
<Loader2 className="size-3 animate-spin" />
|
|
4218
|
-
) : (
|
|
4219
|
-
<Play className="size-3" />
|
|
4220
|
-
)}
|
|
4221
|
-
</Button>
|
|
4222
|
-
</IconActionTooltip>
|
|
4223
|
-
<IconActionTooltip
|
|
4224
|
-
label={t(
|
|
4225
|
-
'lessonForm.openVideoAria',
|
|
4226
|
-
{ name: res.name }
|
|
4227
|
-
)}
|
|
4228
|
-
>
|
|
4229
|
-
<Button
|
|
4230
|
-
type="button"
|
|
4231
|
-
variant="ghost"
|
|
4232
|
-
size="icon"
|
|
4233
|
-
className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-blue-600"
|
|
4234
|
-
onClick={() =>
|
|
4235
|
-
void openResource(res)
|
|
4236
|
-
}
|
|
4237
|
-
aria-label={t(
|
|
4238
|
-
'lessonForm.openVideoAria',
|
|
4239
|
-
{ name: res.name }
|
|
4240
|
-
)}
|
|
4241
|
-
>
|
|
4242
|
-
<ExternalLink className="size-3" />
|
|
4243
|
-
</Button>
|
|
4244
|
-
</IconActionTooltip>
|
|
4245
|
-
<IconActionTooltip
|
|
4246
|
-
label={t(
|
|
4247
|
-
'lessonForm.downloadVideoAria',
|
|
4248
|
-
{ name: res.name }
|
|
4249
|
-
)}
|
|
4250
|
-
asWrapper={isDownloadingResource}
|
|
4251
|
-
>
|
|
4252
|
-
<Button
|
|
4253
|
-
type="button"
|
|
4254
|
-
variant="ghost"
|
|
4255
|
-
size="icon"
|
|
4256
|
-
className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-amber-600"
|
|
4257
|
-
disabled={isDownloadingResource}
|
|
4258
|
-
onClick={() =>
|
|
4259
|
-
void handleResourceDownload(res)
|
|
4260
|
-
}
|
|
4261
|
-
aria-label={t(
|
|
4262
|
-
'lessonForm.downloadVideoAria',
|
|
4263
|
-
{ name: res.name }
|
|
4264
|
-
)}
|
|
4265
|
-
>
|
|
4266
|
-
{isDownloadingResource ? (
|
|
4267
|
-
<Loader2 className="size-3 animate-spin" />
|
|
4268
|
-
) : (
|
|
4269
|
-
<Download className="size-3" />
|
|
4270
|
-
)}
|
|
4271
|
-
</Button>
|
|
4272
|
-
</IconActionTooltip>
|
|
4273
|
-
<IconActionTooltip
|
|
4274
|
-
label={t(
|
|
4275
|
-
'lessonForm.removeVideoAria',
|
|
4276
|
-
{ name: res.name }
|
|
4277
|
-
)}
|
|
4278
|
-
>
|
|
4279
|
-
<Button
|
|
4280
|
-
type="button"
|
|
4281
|
-
variant="ghost"
|
|
4282
|
-
size="icon"
|
|
4283
|
-
className="size-6 shrink-0 text-muted-foreground transition-colors hover:text-destructive"
|
|
4284
|
-
onClick={() =>
|
|
4285
|
-
void removeResource(res.id)
|
|
4286
|
-
}
|
|
4287
|
-
aria-label={t(
|
|
4288
|
-
'lessonForm.removeVideoAria',
|
|
4289
|
-
{ name: res.name }
|
|
4290
|
-
)}
|
|
4291
|
-
>
|
|
4292
|
-
<X className="size-3" />
|
|
4293
|
-
</Button>
|
|
4294
|
-
</IconActionTooltip>
|
|
4295
|
-
</>
|
|
4296
|
-
)}
|
|
4297
|
-
</div>
|
|
4298
|
-
);
|
|
4299
|
-
})}
|
|
4300
3992
|
</div>
|
|
4301
|
-
|
|
4302
|
-
)}
|
|
3993
|
+
);
|
|
3994
|
+
})()}
|
|
4303
3995
|
</CardContent>
|
|
4304
3996
|
</Card>
|
|
4305
3997
|
</>
|
|
@@ -5117,11 +4809,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
5117
4809
|
<DialogTitle className="text-base">
|
|
5118
4810
|
{videoPreviewResource?.name ?? t('lessonForm.tabVideos')}
|
|
5119
4811
|
</DialogTitle>
|
|
5120
|
-
<DialogDescription>
|
|
5121
|
-
{videoPreviewResource
|
|
5122
|
-
? t('lessonForm.fileStorageVideosByResolution')
|
|
5123
|
-
: t('lessonForm.tabVideos')}
|
|
5124
|
-
</DialogDescription>
|
|
4812
|
+
<DialogDescription>{t('lessonForm.tabVideos')}</DialogDescription>
|
|
5125
4813
|
</DialogHeader>
|
|
5126
4814
|
<div className="px-5 pb-5">
|
|
5127
4815
|
{videoPreviewError ? (
|
|
@@ -5140,7 +4828,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
5140
4828
|
) : (
|
|
5141
4829
|
<div className="flex min-h-72 items-center justify-center rounded-lg border border-dashed bg-muted/30 px-6 text-center text-sm text-muted-foreground">
|
|
5142
4830
|
{isResolvingVideoPreview
|
|
5143
|
-
? t('lessonForm.
|
|
4831
|
+
? t('lessonForm.videoJobLoading')
|
|
5144
4832
|
: t('questionEditor.resourceOpenError')}
|
|
5145
4833
|
</div>
|
|
5146
4834
|
)}
|