feat: add view transcription to video card in edit mode

This commit is contained in:
Rotimi Best
2026-05-20 15:57:52 +01:00
parent a4bd49eb85
commit 4e52a618e5
11 changed files with 75 additions and 37 deletions
@@ -4,10 +4,13 @@
import { IconButton } from '@cio/ui/custom/icon-button'; import { IconButton } from '@cio/ui/custom/icon-button';
import { mediaApi } from '$features/media/api'; import { mediaApi } from '$features/media/api';
import { JobPoller, jobsApi, type MediaJobEnvelope } from '$features/jobs'; import { JobPoller, jobsApi, type MediaJobEnvelope } from '$features/jobs';
import { sidePanel } from '$features/side-panel';
import { snackbar } from '$features/ui/snackbar/store'; import { snackbar } from '$features/ui/snackbar/store';
import { t } from '$lib/utils/functions/translations'; import { t } from '$lib/utils/functions/translations';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import type { LessonVideo } from './video-card-utils'; import type { LessonVideo } from './video-card-utils';
import { lessonVideoBus } from './lesson-video-bus.svelte';
import { TRANSCRIPT_PANEL_ID } from './transcript-panel-definition';
interface Props { interface Props {
video: LessonVideo; video: LessonVideo;
@@ -19,6 +22,8 @@
let { video, onRemove, menuPlacement = 'inline' }: Props = $props(); let { video, onRemove, menuPlacement = 'inline' }: Props = $props();
let isTranscribing = $state(false); let isTranscribing = $state(false);
let hasTranscript = $state(false);
let hasCheckedTranscript = $state(false);
let activePoller: JobPoller<MediaJobEnvelope> | null = null; let activePoller: JobPoller<MediaJobEnvelope> | null = null;
const assetId = $derived((video as LessonVideo & { assetId?: string }).assetId ?? null); const assetId = $derived((video as LessonVideo & { assetId?: string }).assetId ?? null);
@@ -29,6 +34,14 @@
activePoller?.stop(); activePoller?.stop();
}); });
async function ensureTranscriptChecked() {
if (hasCheckedTranscript || !assetId) return;
hasCheckedTranscript = true;
const data = await mediaApi.getAssetTranscript(assetId);
hasTranscript = !!data?.segments?.length;
}
async function startTranscriptionPoll(afterAssetId: string) { async function startTranscriptionPoll(afterAssetId: string) {
activePoller?.stop(); activePoller?.stop();
activePoller = null; activePoller = null;
@@ -44,6 +57,7 @@
if (latest.job.status === 'completed') { if (latest.job.status === 'completed') {
isTranscribing = false; isTranscribing = false;
hasTranscript = true;
snackbar.success('snackbar.media_manager.transcription_completed'); snackbar.success('snackbar.media_manager.transcription_completed');
return; return;
@@ -62,6 +76,7 @@
activePoller?.stop(); activePoller?.stop();
activePoller = null; activePoller = null;
isTranscribing = false; isTranscribing = false;
hasTranscript = true;
snackbar.success('snackbar.media_manager.transcription_completed'); snackbar.success('snackbar.media_manager.transcription_completed');
} }
@@ -85,6 +100,22 @@
await startTranscriptionPoll(assetId); await startTranscriptionPoll(assetId);
} }
async function handleViewTranscript() {
if (!assetId) return;
lessonVideoBus.assetId = assetId;
lessonVideoBus.transcriptLoading = true;
sidePanel.open(TRANSCRIPT_PANEL_ID);
try {
const data = await mediaApi.getAssetTranscript(assetId);
lessonVideoBus.transcript = data;
hasTranscript = !!data?.segments?.length;
} finally {
lessonVideoBus.transcriptLoading = false;
}
}
const generateLabel = $derived( const generateLabel = $derived(
isTranscribing isTranscribing
? t.get('course.navItem.lessons.materials.tabs.video.generate_transcript_in_progress') ? t.get('course.navItem.lessons.materials.tabs.video.generate_transcript_in_progress')
@@ -92,7 +123,13 @@
); );
</script> </script>
<DropdownMenu.Root> <DropdownMenu.Root
onOpenChange={(open) => {
if (open && canGenerateTranscript) {
void ensureTranscriptChecked();
}
}}
>
<DropdownMenu.Trigger <DropdownMenu.Trigger
class={menuPlacement === 'corner' class={menuPlacement === 'corner'
? 'ui:data-[state=open]:opacity-100 absolute top-2 right-2 z-40 flex items-center justify-center opacity-0 transition-all delay-150 duration-200 ease-in-out group-hover:opacity-100' ? 'ui:data-[state=open]:opacity-100 absolute top-2 right-2 z-40 flex items-center justify-center opacity-0 transition-all delay-150 duration-200 ease-in-out group-hover:opacity-100'
@@ -114,6 +151,15 @@
> >
{generateLabel} {generateLabel}
</DropdownMenu.Item> </DropdownMenu.Item>
{#if hasTranscript}
<DropdownMenu.Item
onclick={() => {
void handleViewTranscript();
}}
>
{$t('course.navItem.lessons.materials.tabs.video.view_transcript')}
</DropdownMenu.Item>
{/if}
{/if} {/if}
<DropdownMenu.Item class="ui:text-red-600" onclick={onRemove}> <DropdownMenu.Item class="ui:text-red-600" onclick={onRemove}>
{$t('course.navItem.lessons.materials.tabs.video.remove_video')} {$t('course.navItem.lessons.materials.tabs.video.remove_video')}
@@ -1872,7 +1872,8 @@
"kind_generic": "Indlejret", "kind_generic": "Indlejret",
"course_fallback": "Kursus", "course_fallback": "Kursus",
"menu_aria": "Video muligheder" "menu_aria": "Video muligheder"
} },
"view_transcript": "Se transskription"
}, },
"slide": { "slide": {
"title": "Slide", "title": "Slide",
@@ -2163,9 +2164,7 @@
"depth": "Hvor dybt skal kurset gå?", "depth": "Hvor dybt skal kurset gå?",
"topic": "Emne eller niche", "topic": "Emne eller niche",
"expertise_level": "Mål ekspertiseniveau", "expertise_level": "Mål ekspertiseniveau",
"documentation_url": { "documentation_url": "Dokumentations-URL (valgfrit)"
"hint": "Vi læser dette websted for at basere kurset på dit rigtige produkt."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Bliv ekspert på X", "title": "Bliv ekspert på X",
@@ -1872,7 +1872,8 @@
"kind_generic": "Eingebettet", "kind_generic": "Eingebettet",
"course_fallback": "Kurs", "course_fallback": "Kurs",
"menu_aria": "Videooptionen" "menu_aria": "Videooptionen"
} },
"view_transcript": "Transkription ansehen"
}, },
"slide": { "slide": {
"title": "Rutsche", "title": "Rutsche",
@@ -2163,9 +2164,7 @@
"depth": "Wie tief soll der Kurs gehen?", "depth": "Wie tief soll der Kurs gehen?",
"topic": "Thema oder Nische", "topic": "Thema oder Nische",
"expertise_level": "Zielkompetenzniveau", "expertise_level": "Zielkompetenzniveau",
"documentation_url": { "documentation_url": "Dokumentations-URL (optional)"
"hint": "Wir werden diese Website lesen, um den Kurs in Ihrem realen Produkt zu verankern."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Werden Sie Experte für X", "title": "Werden Sie Experte für X",
@@ -2276,6 +2276,7 @@
}, },
"generate_transcript": "Generate transcription", "generate_transcript": "Generate transcription",
"generate_transcript_in_progress": "Transcribing…", "generate_transcript_in_progress": "Transcribing…",
"view_transcript": "View transcription",
"simple_card": { "simple_card": {
"kind_youtube": "YouTube", "kind_youtube": "YouTube",
"kind_upload": "Upload", "kind_upload": "Upload",
@@ -1872,7 +1872,8 @@
"kind_generic": "Incrustado", "kind_generic": "Incrustado",
"course_fallback": "Curso", "course_fallback": "Curso",
"menu_aria": "Opciones de vídeo" "menu_aria": "Opciones de vídeo"
} },
"view_transcript": "Ver transcripción"
}, },
"slide": { "slide": {
"title": "diapositiva", "title": "diapositiva",
@@ -2163,9 +2164,7 @@
"depth": "¿Qué profundidad debe tener el curso?", "depth": "¿Qué profundidad debe tener el curso?",
"topic": "Tema o nicho", "topic": "Tema o nicho",
"expertise_level": "Nivel de experiencia objetivo", "expertise_level": "Nivel de experiencia objetivo",
"documentation_url": { "documentation_url": "URL de documentación (opcional)"
"hint": "Leeremos este sitio para basar el curso en su producto real."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Conviértete en un experto en X", "title": "Conviértete en un experto en X",
@@ -1872,7 +1872,8 @@
"kind_generic": "Intégré", "kind_generic": "Intégré",
"course_fallback": "Cours", "course_fallback": "Cours",
"menu_aria": "Options vidéo" "menu_aria": "Options vidéo"
} },
"view_transcript": "Voir la transcription"
}, },
"slide": { "slide": {
"title": "Diapositive", "title": "Diapositive",
@@ -2163,9 +2164,7 @@
"depth": "Jusqu’où doit aller le cours ?", "depth": "Jusqu’où doit aller le cours ?",
"topic": "Sujet ou niche", "topic": "Sujet ou niche",
"expertise_level": "Niveau d’expertise cible", "expertise_level": "Niveau d’expertise cible",
"documentation_url": { "documentation_url": "URL de la documentation (facultatif)"
"hint": "Nous lirons ce site pour ancrer le cours dans votre produit réel."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Devenez un expert sur X", "title": "Devenez un expert sur X",
@@ -1872,7 +1872,8 @@
"kind_generic": "एंबेडेड", "kind_generic": "एंबेडेड",
"course_fallback": "पाठ्यक्रम", "course_fallback": "पाठ्यक्रम",
"menu_aria": "वीडियो विकल्प" "menu_aria": "वीडियो विकल्प"
} },
"view_transcript": "प्रतिलेखन देखें"
}, },
"slide": { "slide": {
"title": "फिसल पट्टी", "title": "फिसल पट्टी",
@@ -2163,9 +2164,7 @@
"depth": "पाठ्यक्रम कितना गहरा होना चाहिए?", "depth": "पाठ्यक्रम कितना गहरा होना चाहिए?",
"topic": "विषय या आला", "topic": "विषय या आला",
"expertise_level": "लक्ष्य विशेषज्ञता स्तर", "expertise_level": "लक्ष्य विशेषज्ञता स्तर",
"documentation_url": { "documentation_url": "दस्तावेज़ीकरण URL (वैकल्पिक)"
"hint": "हम आपके वास्तविक उत्पाद में पाठ्यक्रम को आधार बनाने के लिए इस साइट को पढ़ेंगे।"
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "एक्स पर विशेषज्ञ बनें", "title": "एक्स पर विशेषज्ञ बनें",
@@ -1872,7 +1872,8 @@
"kind_generic": "Wbudowany", "kind_generic": "Wbudowany",
"course_fallback": "Kurs", "course_fallback": "Kurs",
"menu_aria": "Opcje wideo" "menu_aria": "Opcje wideo"
} },
"view_transcript": "Zobacz transkrypcję"
}, },
"slide": { "slide": {
"title": "Przesuń", "title": "Przesuń",
@@ -2163,9 +2164,7 @@
"depth": "Jak głęboko powinien sięgać kurs?", "depth": "Jak głęboko powinien sięgać kurs?",
"topic": "Temat lub nisza", "topic": "Temat lub nisza",
"expertise_level": "Docelowy poziom wiedzy", "expertise_level": "Docelowy poziom wiedzy",
"documentation_url": { "documentation_url": "Adres URL dokumentacji (opcjonalnie)"
"hint": "Przeczytamy tę witrynę, aby oprzeć kurs na Twoim prawdziwym produkcie."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Zostań ekspertem w dziedzinie X", "title": "Zostań ekspertem w dziedzinie X",
@@ -1872,7 +1872,8 @@
"kind_generic": "Incorporado", "kind_generic": "Incorporado",
"course_fallback": "Curso", "course_fallback": "Curso",
"menu_aria": "Opções de vídeo" "menu_aria": "Opções de vídeo"
} },
"view_transcript": "Ver transcrição"
}, },
"slide": { "slide": {
"title": "Deslizar", "title": "Deslizar",
@@ -2163,9 +2164,7 @@
"depth": "Quão profundo o curso deve ir?", "depth": "Quão profundo o curso deve ir?",
"topic": "Tópico ou nicho", "topic": "Tópico ou nicho",
"expertise_level": "Nível de especialização alvo", "expertise_level": "Nível de especialização alvo",
"documentation_url": { "documentation_url": "URL da documentação (opcional)"
"hint": "Leremos este site para fundamentar o curso em seu produto real."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Torne-se um especialista em X", "title": "Torne-se um especialista em X",
@@ -1872,7 +1872,8 @@
"kind_generic": "Встроенный", "kind_generic": "Встроенный",
"course_fallback": "Курс", "course_fallback": "Курс",
"menu_aria": "Параметры видео" "menu_aria": "Параметры видео"
} },
"view_transcript": "Посмотреть транскрипцию"
}, },
"slide": { "slide": {
"title": "Слайд", "title": "Слайд",
@@ -2163,9 +2164,7 @@
"depth": "Насколько глубоким должен быть курс?", "depth": "Насколько глубоким должен быть курс?",
"topic": "Тема или ниша", "topic": "Тема или ниша",
"expertise_level": "Целевой уровень экспертизы", "expertise_level": "Целевой уровень экспертизы",
"documentation_url": { "documentation_url": "URL-адрес документации (необязательно)"
"hint": "Мы прочитаем этот сайт, чтобы закрепить курс на вашем реальном продукте."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Станьте экспертом по X", "title": "Станьте экспертом по X",
@@ -1872,7 +1872,8 @@
"kind_generic": "Đã nhúng", "kind_generic": "Đã nhúng",
"course_fallback": "Khóa học", "course_fallback": "Khóa học",
"menu_aria": "Tùy chọn video" "menu_aria": "Tùy chọn video"
} },
"view_transcript": "Xem bản chép lời"
}, },
"slide": { "slide": {
"title": "Trượt", "title": "Trượt",
@@ -2163,9 +2164,7 @@
"depth": "Khóa học nên đi sâu đến mức nào?", "depth": "Khóa học nên đi sâu đến mức nào?",
"topic": "Chủ đề hoặc niche", "topic": "Chủ đề hoặc niche",
"expertise_level": "Mục tiêu trình độ chuyên môn", "expertise_level": "Mục tiêu trình độ chuyên môn",
"documentation_url": { "documentation_url": "URL tài liệu (tùy chọn)"
"hint": "Chúng tôi sẽ đọc trang web này để xây dựng khóa học về sản phẩm thực của bạn."
}
}, },
"expert_on_x": { "expert_on_x": {
"title": "Trở thành chuyên gia về X", "title": "Trở thành chuyên gia về X",