@hed-hog/lms 0.0.366 → 0.0.370

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/dist/certificate/certificate.controller.d.ts +1 -1
  2. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  3. package/dist/certificate/certificate.controller.js +4 -2
  4. package/dist/certificate/certificate.controller.js.map +1 -1
  5. package/dist/certificate/certificate.service.d.ts +50 -0
  6. package/dist/certificate/certificate.service.d.ts.map +1 -1
  7. package/dist/certificate/certificate.service.js +73 -0
  8. package/dist/certificate/certificate.service.js.map +1 -1
  9. package/dist/course/course-ai-usage.service.d.ts +58 -0
  10. package/dist/course/course-ai-usage.service.d.ts.map +1 -0
  11. package/dist/course/course-ai-usage.service.js +176 -0
  12. package/dist/course/course-ai-usage.service.js.map +1 -0
  13. package/dist/course/course-audio-transcription.service.d.ts +65 -1
  14. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  15. package/dist/course/course-audio-transcription.service.js +381 -29
  16. package/dist/course/course-audio-transcription.service.js.map +1 -1
  17. package/dist/course/course-export-scorm12.service.d.ts +3 -0
  18. package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
  19. package/dist/course/course-export-scorm12.service.js +141 -6
  20. package/dist/course/course-export-scorm12.service.js.map +1 -1
  21. package/dist/course/course-export.service.d.ts.map +1 -1
  22. package/dist/course/course-export.service.js +2 -1
  23. package/dist/course/course-export.service.js.map +1 -1
  24. package/dist/course/course-lesson.controller.d.ts +25 -3
  25. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  26. package/dist/course/course-lesson.controller.js +71 -8
  27. package/dist/course/course-lesson.controller.js.map +1 -1
  28. package/dist/course/course-structure.controller.d.ts +26 -5
  29. package/dist/course/course-structure.controller.d.ts.map +1 -1
  30. package/dist/course/course-structure.controller.js +31 -1
  31. package/dist/course/course-structure.controller.js.map +1 -1
  32. package/dist/course/course-structure.service.d.ts +37 -5
  33. package/dist/course/course-structure.service.d.ts.map +1 -1
  34. package/dist/course/course-structure.service.js +165 -20
  35. package/dist/course/course-structure.service.js.map +1 -1
  36. package/dist/course/course-transcription-translation.service.d.ts +31 -0
  37. package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
  38. package/dist/course/course-transcription-translation.service.js +227 -0
  39. package/dist/course/course-transcription-translation.service.js.map +1 -0
  40. package/dist/course/course-video-agent-pipeline.service.js +7 -7
  41. package/dist/course/course-video-agent-pipeline.service.js.map +1 -1
  42. package/dist/course/course.module.d.ts.map +1 -1
  43. package/dist/course/course.module.js +4 -0
  44. package/dist/course/course.module.js.map +1 -1
  45. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  46. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  47. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  48. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  49. package/dist/course/dto/create-course-export.dto.d.ts +1 -0
  50. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
  51. package/dist/course/dto/create-course-export.dto.js +6 -0
  52. package/dist/course/dto/create-course-export.dto.js.map +1 -1
  53. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  54. package/dist/course/lms-bulk-upload-automation.service.js +26 -13
  55. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  56. package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
  57. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  58. package/dist/course/lms-bulk-upload.service.d.ts +3 -0
  59. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  60. package/dist/course/lms-bulk-upload.service.js +48 -29
  61. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  62. package/dist/course/subtitle.util.d.ts +46 -0
  63. package/dist/course/subtitle.util.d.ts.map +1 -0
  64. package/dist/course/subtitle.util.js +206 -0
  65. package/dist/course/subtitle.util.js.map +1 -0
  66. package/dist/enterprise/training/training-student.service.d.ts +27 -0
  67. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  68. package/dist/enterprise/training/training-student.service.js +197 -10
  69. package/dist/enterprise/training/training-student.service.js.map +1 -1
  70. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
  71. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  72. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
  73. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  74. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
  75. package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
  76. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
  77. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  78. package/dist/lms.module.d.ts.map +1 -1
  79. package/dist/lms.module.js +4 -0
  80. package/dist/lms.module.js.map +1 -1
  81. package/dist/platforma/platforma-performance.service.js +121 -121
  82. package/dist/platforma/platforma-video.service.d.ts +8 -0
  83. package/dist/platforma/platforma-video.service.d.ts.map +1 -1
  84. package/dist/platforma/platforma-video.service.js +45 -2
  85. package/dist/platforma/platforma-video.service.js.map +1 -1
  86. package/dist/platforma/platforma.controller.d.ts +99 -1
  87. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  88. package/dist/platforma/platforma.controller.js +111 -2
  89. package/dist/platforma/platforma.controller.js.map +1 -1
  90. package/dist/training/dto/create-training.dto.d.ts +9 -0
  91. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  92. package/dist/training/dto/create-training.dto.js +45 -1
  93. package/dist/training/dto/create-training.dto.js.map +1 -1
  94. package/dist/training/training.controller.d.ts +144 -0
  95. package/dist/training/training.controller.d.ts.map +1 -1
  96. package/dist/training/training.service.d.ts +149 -0
  97. package/dist/training/training.service.d.ts.map +1 -1
  98. package/dist/training/training.service.js +332 -167
  99. package/dist/training/training.service.js.map +1 -1
  100. package/hedhog/data/image_type.yaml +10 -0
  101. package/hedhog/data/route.yaml +251 -0
  102. package/hedhog/data/setting_group.yaml +97 -0
  103. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
  104. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
  105. package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
  106. package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
  107. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
  108. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
  109. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
  110. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
  111. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
  112. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
  113. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
  114. package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
  115. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
  116. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
  117. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
  118. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
  119. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
  120. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
  121. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
  122. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
  123. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
  124. package/hedhog/frontend/app/courses/page.tsx.ejs +26 -4
  125. package/hedhog/frontend/app/paths/page.tsx.ejs +612 -164
  126. package/hedhog/frontend/messages/en.json +23 -12
  127. package/hedhog/frontend/messages/pt.json +23 -12
  128. package/hedhog/query/triggers.sql +33 -0
  129. package/hedhog/table/course_ai_usage.yaml +46 -0
  130. package/hedhog/table/course_lesson.yaml +3 -0
  131. package/hedhog/table/course_lesson_answer.yaml +37 -0
  132. package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
  133. package/hedhog/table/learning_path.yaml +6 -0
  134. package/hedhog/table/learning_path_module.yaml +22 -0
  135. package/hedhog/table/learning_path_step.yaml +9 -6
  136. package/hedhog/table/lesson_view_event.yaml +66 -66
  137. package/package.json +9 -9
  138. package/src/certificate/certificate.controller.ts +2 -0
  139. package/src/certificate/certificate.service.ts +99 -0
  140. package/src/course/course-ai-usage.service.ts +221 -0
  141. package/src/course/course-audio-transcription.service.ts +471 -43
  142. package/src/course/course-export-scorm12.service.ts +149 -5
  143. package/src/course/course-export.service.ts +1 -0
  144. package/src/course/course-lesson.controller.ts +59 -6
  145. package/src/course/course-structure.controller.ts +16 -0
  146. package/src/course/course-structure.service.ts +184 -10
  147. package/src/course/course-transcription-translation.service.ts +293 -0
  148. package/src/course/course-video-agent-pipeline.service.ts +471 -471
  149. package/src/course/course.module.ts +4 -0
  150. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  151. package/src/course/dto/create-course-export.dto.ts +6 -0
  152. package/src/course/ffmpeg.util.ts +65 -65
  153. package/src/course/lms-bulk-upload-automation.service.ts +29 -7
  154. package/src/course/lms-bulk-upload.service.ts +20 -1
  155. package/src/course/subtitle.util.ts +220 -0
  156. package/src/enterprise/training/training-student.service.ts +224 -4
  157. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
  158. package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
  159. package/src/lms.module.ts +4 -0
  160. package/src/platforma/dto/heartbeat.dto.ts +30 -30
  161. package/src/platforma/handlers/emit-certificate.handler.ts +117 -117
  162. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -343
  163. package/src/platforma/platforma-heartbeat.service.ts +33 -33
  164. package/src/platforma/platforma-performance.service.ts +606 -606
  165. package/src/platforma/platforma-search.service.ts +48 -48
  166. package/src/platforma/platforma-video.service.ts +59 -3
  167. package/src/platforma/platforma.controller.ts +88 -0
  168. package/src/training/dto/create-training.dto.ts +36 -0
  169. package/src/training/training.service.ts +360 -163
@@ -0,0 +1,191 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from '@/components/ui/card';
10
+ import { Skeleton } from '@/components/ui/skeleton';
11
+ import { DollarSign, Languages, Mic, Sparkles, Zap } from 'lucide-react';
12
+ import { useCourseAiCostsQuery } from '../_data/use-course-ai-costs';
13
+
14
+ interface Props {
15
+ courseId: string;
16
+ }
17
+
18
+ const JOB_TYPE_META: Record<
19
+ string,
20
+ { label: string; icon: typeof Mic; tone: string }
21
+ > = {
22
+ transcription: {
23
+ label: 'Transcrição',
24
+ icon: Mic,
25
+ tone: 'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400',
26
+ },
27
+ translation: {
28
+ label: 'Tradução',
29
+ icon: Languages,
30
+ tone: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
31
+ },
32
+ xp_calculation: {
33
+ label: 'Cálculo de XP',
34
+ icon: Zap,
35
+ tone: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
36
+ },
37
+ };
38
+
39
+ function formatUsd(value: number): string {
40
+ const fractionDigits = value > 0 && value < 0.01 ? 4 : 2;
41
+ return new Intl.NumberFormat('en-US', {
42
+ style: 'currency',
43
+ currency: 'USD',
44
+ minimumFractionDigits: fractionDigits,
45
+ maximumFractionDigits: fractionDigits,
46
+ }).format(value);
47
+ }
48
+
49
+ export function CourseAiCostsTab({ courseId }: Props) {
50
+ const { data, isLoading, isError } = useCourseAiCostsQuery(courseId);
51
+
52
+ if (isLoading) {
53
+ return (
54
+ <div className="flex flex-col gap-3">
55
+ <Skeleton className="h-28 rounded-xl" />
56
+ <div className="grid gap-3 sm:grid-cols-3">
57
+ {Array.from({ length: 3 }).map((_, i) => (
58
+ <Skeleton key={i} className="h-24 rounded-xl" />
59
+ ))}
60
+ </div>
61
+ <Skeleton className="h-48 rounded-xl" />
62
+ </div>
63
+ );
64
+ }
65
+
66
+ if (isError || !data) {
67
+ return (
68
+ <Card className="border-dashed bg-muted/20">
69
+ <CardHeader>
70
+ <CardTitle className="text-sm font-semibold">
71
+ Não foi possível carregar os custos de IA
72
+ </CardTitle>
73
+ <CardDescription>
74
+ Tente novamente em instantes.
75
+ </CardDescription>
76
+ </CardHeader>
77
+ </Card>
78
+ );
79
+ }
80
+
81
+ if (data.totalCostUsd <= 0 && data.byJobType.length === 0) {
82
+ return (
83
+ <Card className="border-dashed bg-muted/20">
84
+ <CardHeader>
85
+ <CardTitle className="text-sm font-semibold">
86
+ Ainda não há custos de IA registrados
87
+ </CardTitle>
88
+ <CardDescription>
89
+ Os custos aparecem aqui conforme as aulas passam por transcrição,
90
+ tradução e cálculo de XP. Apenas execuções a partir de agora são
91
+ contabilizadas.
92
+ </CardDescription>
93
+ </CardHeader>
94
+ </Card>
95
+ );
96
+ }
97
+
98
+ const byTypeMap = new Map(data.byJobType.map((r) => [r.jobType, r]));
99
+ const orderedTypes = ['transcription', 'translation', 'xp_calculation'];
100
+
101
+ return (
102
+ <div className="flex flex-col gap-3">
103
+ {/* Total */}
104
+ <Card className="overflow-hidden border-border/70 bg-linear-to-br from-background via-background to-muted/30">
105
+ <CardContent className="flex items-center gap-4 px-4 py-4">
106
+ <div className="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-primary/10 text-primary">
107
+ <DollarSign className="size-6" />
108
+ </div>
109
+ <div className="min-w-0">
110
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
111
+ Custo total de IA do curso
112
+ </div>
113
+ <div className="mt-0.5 text-2xl font-semibold text-foreground">
114
+ {formatUsd(data.totalCostUsd)}
115
+ </div>
116
+ <div className="mt-0.5 text-xs text-muted-foreground">
117
+ Soma de transcrição, tradução e cálculo de XP ({data.currency}).
118
+ </div>
119
+ </div>
120
+ </CardContent>
121
+ </Card>
122
+
123
+ {/* Por tipo */}
124
+ <div className="grid gap-3 sm:grid-cols-3">
125
+ {orderedTypes.map((type) => {
126
+ const meta = JOB_TYPE_META[type];
127
+ const row = byTypeMap.get(type);
128
+ const Icon = meta?.icon ?? Sparkles;
129
+ return (
130
+ <Card key={type} className="border-border/70">
131
+ <CardContent className="flex items-center gap-3 px-3 py-3">
132
+ <div
133
+ className={`flex size-9 shrink-0 items-center justify-center rounded-xl ${meta?.tone ?? 'bg-muted text-muted-foreground'}`}
134
+ >
135
+ <Icon className="size-4" />
136
+ </div>
137
+ <div className="min-w-0">
138
+ <div className="text-xs font-medium text-muted-foreground">
139
+ {meta?.label ?? type}
140
+ </div>
141
+ <div className="text-base font-semibold text-foreground">
142
+ {formatUsd(row?.costUsd ?? 0)}
143
+ </div>
144
+ <div className="text-[11px] text-muted-foreground">
145
+ {row?.runs ?? 0} execução(ões)
146
+ </div>
147
+ </div>
148
+ </CardContent>
149
+ </Card>
150
+ );
151
+ })}
152
+ </div>
153
+
154
+ {/* Por aula */}
155
+ <Card className="overflow-hidden border-border/70">
156
+ <CardHeader className="border-b border-border/70 py-3">
157
+ <CardTitle className="text-sm font-semibold">Custo por aula</CardTitle>
158
+ <CardDescription>
159
+ Quanto cada aula consumiu em IA, da mais cara para a mais barata.
160
+ </CardDescription>
161
+ </CardHeader>
162
+ <CardContent className="p-0">
163
+ {data.byLesson.length === 0 ? (
164
+ <div className="px-4 py-6 text-center text-sm text-muted-foreground">
165
+ Nenhum custo por aula registrado.
166
+ </div>
167
+ ) : (
168
+ <div className="divide-y divide-border/60">
169
+ {data.byLesson.map((row, index) => (
170
+ <div
171
+ key={`${row.lessonId ?? 'null'}-${index}`}
172
+ className="flex items-center justify-between gap-3 px-4 py-2.5"
173
+ >
174
+ <div className="min-w-0 flex-1 truncate text-sm text-foreground">
175
+ {row.lessonTitle ?? 'Aula removida'}
176
+ </div>
177
+ <div className="text-[11px] text-muted-foreground">
178
+ {row.runs} exec.
179
+ </div>
180
+ <div className="w-24 text-right text-sm font-semibold text-foreground">
181
+ {formatUsd(row.costUsd)}
182
+ </div>
183
+ </div>
184
+ ))}
185
+ </div>
186
+ )}
187
+ </CardContent>
188
+ </Card>
189
+ </div>
190
+ );
191
+ }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { useEffect, useState } from 'react';
4
4
  import { toast } from 'sonner';
5
5
  import {
6
6
  AlignLeft,
@@ -8,6 +8,7 @@ import {
8
8
  Download,
9
9
  FileArchive,
10
10
  Info,
11
+ Languages,
11
12
  Loader2,
12
13
  Monitor,
13
14
  Moon,
@@ -19,6 +20,7 @@ import { cn } from '@/lib/utils';
19
20
  import { Alert, AlertDescription } from '@/components/ui/alert';
20
21
  import { Badge } from '@/components/ui/badge';
21
22
  import { Button } from '@/components/ui/button';
23
+ import { Checkbox } from '@/components/ui/checkbox';
22
24
  import { Label } from '@/components/ui/label';
23
25
  import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
24
26
  import {
@@ -31,6 +33,7 @@ import {
31
33
  import {
32
34
  ScormVisualSettings,
33
35
  useCreateCourseExportMutation,
36
+ useCourseTranscriptionLocalesQuery,
34
37
  } from '../_data/use-course-exports';
35
38
 
36
39
  // ── Color presets ────────────────────────────────────────────
@@ -119,7 +122,23 @@ export function CourseExportSheet({
119
122
  estimatedSizeMb,
120
123
  }: CourseExportSheetProps) {
121
124
  const [visual, setVisual] = useState<Required<ScormVisualSettings>>(DEFAULT_VISUAL);
125
+ const [selectedLocaleIds, setSelectedLocaleIds] = useState<number[]>([]);
122
126
  const createExport = useCreateCourseExportMutation(courseId);
127
+ const { data: availableLocales = [] } = useCourseTranscriptionLocalesQuery(
128
+ open ? courseId : null,
129
+ );
130
+
131
+ useEffect(() => {
132
+ if (availableLocales.length > 0) {
133
+ setSelectedLocaleIds(availableLocales.map((l) => l.id));
134
+ }
135
+ }, [availableLocales]);
136
+
137
+ const toggleLocale = (id: number) => {
138
+ setSelectedLocaleIds((prev) =>
139
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
140
+ );
141
+ };
123
142
 
124
143
  const setV = <K extends keyof ScormVisualSettings>(
125
144
  key: K,
@@ -131,6 +150,7 @@ export function CourseExportSheet({
131
150
  await createExport.mutateAsync({
132
151
  format: 'scorm_1_2',
133
152
  visualSettings: visual,
153
+ subtitleLocaleIds: selectedLocaleIds.length > 0 ? selectedLocaleIds : undefined,
134
154
  });
135
155
  onOpenChange(false);
136
156
  toast.success(
@@ -198,6 +218,66 @@ export function CourseExportSheet({
198
218
  </Alert>
199
219
  )}
200
220
 
221
+ {/* ── Subtitles ── */}
222
+ <div className="rounded-lg border overflow-hidden">
223
+ <div className="flex items-center gap-2 px-3 py-2.5 border-b bg-muted/20">
224
+ <Languages className="size-4 text-muted-foreground" />
225
+ <span className="text-sm font-medium">Legendas</span>
226
+ {availableLocales.length > 0 && (
227
+ <span className="ml-auto text-xs text-muted-foreground">
228
+ {selectedLocaleIds.length} de {availableLocales.length} selecionado(s)
229
+ </span>
230
+ )}
231
+ </div>
232
+ <div className="px-3 py-3">
233
+ {availableLocales.length === 0 ? (
234
+ <p className="text-xs text-muted-foreground">
235
+ Nenhuma legenda disponível. Gere transcrições nas aulas para incluí-las na exportação.
236
+ </p>
237
+ ) : (
238
+ <div className="flex flex-col gap-2">
239
+ {availableLocales.map((loc) => (
240
+ <label
241
+ key={loc.id}
242
+ className="flex items-center gap-2.5 cursor-pointer group"
243
+ >
244
+ <Checkbox
245
+ checked={selectedLocaleIds.includes(loc.id)}
246
+ onCheckedChange={() => toggleLocale(loc.id)}
247
+ id={`locale-${loc.id}`}
248
+ />
249
+ <span className="text-sm group-hover:text-foreground transition-colors">
250
+ {loc.name}
251
+ {loc.region && (
252
+ <span className="text-xs text-muted-foreground ml-1">
253
+ ({loc.code}-{loc.region})
254
+ </span>
255
+ )}
256
+ </span>
257
+ </label>
258
+ ))}
259
+ <div className="flex gap-2 mt-1 pt-1 border-t">
260
+ <button
261
+ type="button"
262
+ onClick={() => setSelectedLocaleIds(availableLocales.map((l) => l.id))}
263
+ className="text-xs text-primary hover:underline"
264
+ >
265
+ Selecionar todos
266
+ </button>
267
+ <span className="text-xs text-muted-foreground">·</span>
268
+ <button
269
+ type="button"
270
+ onClick={() => setSelectedLocaleIds([])}
271
+ className="text-xs text-primary hover:underline"
272
+ >
273
+ Desmarcar todos
274
+ </button>
275
+ </div>
276
+ </div>
277
+ )}
278
+ </div>
279
+ </div>
280
+
201
281
  {/* ── Appearance ── */}
202
282
  <div className="rounded-lg border overflow-hidden">
203
283
  <div className="flex items-center gap-2 px-3 py-2.5 border-b bg-muted/20">
@@ -58,6 +58,14 @@ function formatDuration(seconds: number | null | undefined): string {
58
58
  return `${h}h ${rm}m`;
59
59
  }
60
60
 
61
+ function formatFileSize(bytes: number | null | undefined): string {
62
+ if (bytes == null) return '—';
63
+ if (bytes < 1024) return `${bytes} B`;
64
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
65
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
66
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
67
+ }
68
+
61
69
  function StatusBadge({ status }: { status: CourseExportRecord['status'] }) {
62
70
  switch (status) {
63
71
  case 'completed':
@@ -203,6 +211,7 @@ export function CourseExportsTab({ courseId }: CourseExportsTabProps) {
203
211
  <TableHead>Formato</TableHead>
204
212
  <TableHead>Data</TableHead>
205
213
  <TableHead>Duração</TableHead>
214
+ <TableHead>Tamanho</TableHead>
206
215
  <TableHead>Status</TableHead>
207
216
  <TableHead className="text-right">Ações</TableHead>
208
217
  </TableRow>
@@ -232,6 +241,9 @@ export function CourseExportsTab({ courseId }: CourseExportsTabProps) {
232
241
  <TableCell className="text-xs text-muted-foreground">
233
242
  {formatDuration(exp.duration_seconds)}
234
243
  </TableCell>
244
+ <TableCell className="text-xs text-muted-foreground">
245
+ {formatFileSize(exp.file?.size)}
246
+ </TableCell>
235
247
  <TableCell>
236
248
  <StatusBadge status={exp.status} />
237
249
  </TableCell>
@@ -1,19 +1,60 @@
1
1
  'use client';
2
2
 
3
3
  import { Badge } from '@/components/ui/badge';
4
+ import { Button } from '@/components/ui/button';
4
5
  import { Separator } from '@/components/ui/separator';
5
- import { BookOpen, Globe, Hash, Tag } from 'lucide-react';
6
+ import { useQueryClient } from '@tanstack/react-query';
7
+ import { BookOpen, Globe, Hash, Loader2, Tag, Trash2 } from 'lucide-react';
8
+ import { toast } from 'sonner';
9
+ import { useDeleteCourseTranscriptionsMutation } from '../_data/use-transcription-segments';
6
10
  import { useStructureStore } from './store';
7
11
 
8
12
  export function DetailCourse() {
9
13
  const course = useStructureStore((s) => s.course);
10
14
  const sessions = useStructureStore((s) => s.sessions);
11
15
  const lessons = useStructureStore((s) => s.lessons);
16
+ const showConfirm = useStructureStore((s) => s.showConfirm);
17
+ const courseId = useStructureStore((s) => s.courseId);
18
+ const queryClient = useQueryClient();
19
+
20
+ const { mutate: deleteCourseTranscriptions, isPending: isDeletingTranscriptions } =
21
+ useDeleteCourseTranscriptionsMutation(courseId);
12
22
 
13
23
  const totalMinutes = lessons.reduce((sum, l) => sum + l.duration, 0);
14
24
  const hours = Math.floor(totalMinutes / 60);
15
25
  const minutes = totalMinutes % 60;
16
26
 
27
+ const handleDeleteAllTranscriptions = () => {
28
+ showConfirm({
29
+ title: 'Excluir todas as transcrições e traduções',
30
+ description:
31
+ 'Isso remove permanentemente todas as transcrições e traduções de todas as aulas deste curso. Esta ação não pode ser desfeita.',
32
+ confirmText: 'Excluir tudo',
33
+ destructive: true,
34
+ onConfirm: () => {
35
+ deleteCourseTranscriptions({
36
+ onSuccess: (result) => {
37
+ toast.success(
38
+ `Transcrições removidas (${result.deleted} segmento(s) excluído(s)).`,
39
+ );
40
+ queryClient.invalidateQueries({
41
+ queryKey: ['course-content-overview', courseId],
42
+ });
43
+ queryClient.invalidateQueries({
44
+ queryKey: ['lesson-transcription-locales'],
45
+ });
46
+ queryClient.invalidateQueries({
47
+ queryKey: ['lesson-transcription-segments'],
48
+ });
49
+ },
50
+ onError: (err) => {
51
+ toast.error(err.message || 'Erro ao excluir as transcrições.');
52
+ },
53
+ });
54
+ },
55
+ });
56
+ };
57
+
17
58
  return (
18
59
  <div className="flex flex-col overflow-y-auto h-full">
19
60
  {/* Header */}
@@ -78,6 +119,33 @@ export function DetailCourse() {
78
119
  </div>
79
120
  </>
80
121
  )}
122
+
123
+ <Separator />
124
+
125
+ {/* Zona de perigo */}
126
+ <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3">
127
+ <p className="text-xs font-semibold text-destructive mb-1">
128
+ Zona de perigo
129
+ </p>
130
+ <p className="text-xs text-muted-foreground mb-3">
131
+ Remove permanentemente todas as transcrições e traduções de todas as
132
+ aulas deste curso.
133
+ </p>
134
+ <Button
135
+ variant="destructive"
136
+ size="sm"
137
+ className="w-full"
138
+ disabled={isDeletingTranscriptions}
139
+ onClick={handleDeleteAllTranscriptions}
140
+ >
141
+ {isDeletingTranscriptions ? (
142
+ <Loader2 className="size-4 mr-2 animate-spin" />
143
+ ) : (
144
+ <Trash2 className="size-4 mr-2" />
145
+ )}
146
+ Excluir todas as transcrições e traduções
147
+ </Button>
148
+ </div>
81
149
  </div>
82
150
  </div>
83
151
  );