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