feat(dashboard): add Show transcript and Summarize lesson buttons in lesson view (#748)

* feat(dashboard): add lesson transcript and summarize actions in view mode

Add Show transcript and Summarize lesson outline buttons beneath lesson
videos in view mode. Show transcript opens the existing transcript side
panel when an uploaded video has a transcript. Summarize lesson appears
on every lesson and sends a contextual prompt to the AI assistant.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): localize summarize lesson AI prompts

Move hardcoded summarize prompt strings into translation keys so the
prompt sent to the AI assistant follows the user's app language.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-13 11:18:28 +01:00
committed by GitHub
co-authored by Rotimi Best Cursor Agent
parent 3fbfa4b09f
commit fdc19aac41
16 changed files with 166 additions and 10 deletions
@@ -10,3 +10,4 @@ export { default as Document } from './document/document.svelte';
export { default as AddVideoModal } from './video/add-video-modal.svelte';
export { default as AddDocumentModal } from './document/add-document-modal.svelte';
export { default as LessonSettingsTab } from './lesson-settings-tab.svelte';
export { default as LessonSummarizeButton } from './lesson-summarize-button.svelte';
@@ -0,0 +1,21 @@
<script lang="ts">
import { Button } from '@cio/ui/base/button';
import { sendPromptToAssistant } from '$features/ai-assistant/utils/store';
import { t } from '$lib/utils/functions/translations';
import { buildLessonSummarizePrompt } from './utils/lesson-summarize-prompt';
interface Props {
lessonId: string;
class?: string;
}
let { lessonId, class: className = '' }: Props = $props();
function handleSummarizeLesson() {
sendPromptToAssistant(buildLessonSummarizePrompt(lessonId));
}
</script>
<Button variant="outline" size="sm" class={className} onclick={handleSummarizeLesson}>
{$t('course.navItem.lessons.materials.summarize_lesson')}
</Button>
@@ -0,0 +1,37 @@
import { lessonApi } from '$features/course/api';
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
import { t } from '$lib/utils/functions/translations';
import { isEnforceableLessonVideo, type LessonVideo } from '../video/video-card-utils';
export function hasUploadedLessonVideo(videos: LessonVideo[] | null | undefined): boolean {
return (videos ?? []).some((video) => isEnforceableLessonVideo(video));
}
export function hasLessonNoteContentForLesson(lessonId: string): boolean {
const rawNote = lessonApi.lesson?.note;
const hasLegacyNote = typeof rawNote === 'string' && rawNote.trim().length > 0;
if (hasLegacyNote) {
return true;
}
const translations = Object.values(lessonApi.translations[lessonId] || {});
return translations.some((content) => typeof content === 'string' && !isHtmlValueEmpty(content));
}
export function buildLessonSummarizePrompt(lessonId: string): string {
const videos = lessonApi.lesson?.videos ?? [];
const hasUploadedVideo = hasUploadedLessonVideo(videos);
const hasLessonNotes = hasLessonNoteContentForLesson(lessonId);
if (!hasUploadedVideo) {
return t.get('course.navItem.lessons.materials.summarize_lesson_prompt.lesson_only');
}
if (hasLessonNotes) {
return t.get('course.navItem.lessons.materials.summarize_lesson_prompt.transcript_and_notes');
}
return t.get('course.navItem.lessons.materials.summarize_lesson_prompt.transcript_only');
}
@@ -1,5 +1,6 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import { Button } from '@cio/ui/base/button';
import { MediaPlayer } from '@cio/ui/custom/media-player';
import { presignApi } from '$features/course/api/presign.svelte';
import { mediaApi } from '$features/media/api';
@@ -16,6 +17,7 @@
import { resolveWatchEnforcedAssetIds, type LessonVideo } from './video-card-utils';
import { lessonVideoBus } from './lesson-video-bus.svelte';
import { TRANSCRIPT_PANEL_ID } from './transcript-panel-definition';
import LessonSummarizeButton from '../lesson-summarize-button.svelte';
/**
* HLS playback flag — set by the upload flow when the asset was encoded
@@ -98,9 +100,10 @@
video: LessonVideo;
courseId: string;
lessonId: string;
showSummarizeButton?: boolean;
}
let { video, courseId, lessonId }: Props = $props();
let { video, courseId, lessonId, showSummarizeButton = false }: Props = $props();
let localTranscript = $state<AssetTranscriptPayload | null>(null);
let localTranscriptLoading = $state(false);
@@ -435,14 +438,16 @@
lessonVideoBus.hasPlayed = true;
}
function openTranscriptPanel() {
syncVideoBus();
sidePanel.open(TRANSCRIPT_PANEL_ID);
}
const transcriptPanelControl = $derived(
localTranscript
? {
label: t.get('course.navItem.lessons.materials.tabs.video.transcript.open_side_panel'),
onClick: () => {
syncVideoBus();
sidePanel.open(TRANSCRIPT_PANEL_ID);
}
onClick: openTranscriptPanel
}
: undefined
);
@@ -489,4 +494,18 @@
seekPolicy
}}
/>
{#if localTranscript || showSummarizeButton}
<div class="mt-3 flex flex-wrap gap-2">
{#if localTranscript}
<Button variant="outline" size="sm" onclick={openTranscriptPanel}>
{$t('course.navItem.lessons.materials.show_transcript')}
</Button>
{/if}
{#if showSummarizeButton}
<LessonSummarizeButton {lessonId} />
{/if}
</div>
{/if}
</div>
@@ -42,9 +42,9 @@
}
</script>
{#snippet content(video)}
{#snippet content(video, index)}
{#key video.type === 'upload' ? ((video as typeof video & { assetId?: string }).assetId ?? video.link) : video.link}
<LessonVideoPlayer {video} {courseId} {lessonId} />
<LessonVideoPlayer {video} {courseId} {lessonId} showSummarizeButton={index === videos.length - 1} />
{/key}
{/snippet}
@@ -81,9 +81,9 @@
<!-- View Mode -->
{#if videos.length}
<div class="w-full">
{#each videos as video}
{#each videos as video, index}
<div class="mb-5 w-full overflow-hidden">
{@render content(video)}
{@render content(video, index)}
</div>
{/each}
</div>
@@ -46,7 +46,8 @@
Document,
AddVideoModal,
AddDocumentModal,
LessonSettingsTab
LessonSettingsTab,
LessonSummarizeButton
} from '$features/course/components/lesson';
import type { TLocale } from '@cio/db/types';
@@ -179,6 +180,7 @@
const viewModeComponents = $derived(getViewModeComponents(tabs));
const isMaterialsEmpty = $derived(tabs.every((tab) => tab.badgeValue === 0));
const hasLessonVideos = $derived((lessonApi.lesson?.videos?.length ?? 0) > 0);
let timeoutId: NodeJS.Timeout | undefined;
@@ -524,6 +526,12 @@
{:else if lessonApi.lesson && !isMaterialsEmpty}
{#key lessonId}
<div class="mb-20 flex w-full flex-col gap-2" in:fade={{ delay: 500 }} out:fade>
{#if !hasLessonVideos}
<div class="mb-2 flex flex-wrap gap-2">
<LessonSummarizeButton {lessonId} />
</div>
{/if}
{#each viewModeComponents as Component, index (index)}
<Component {mode} {lessonId} {courseId} />
{/each}
@@ -1889,6 +1889,13 @@
"no_content": "Intet indhold",
"no_translation": "Ingen oversættelse for det valgte sprog",
"button": "knappen",
"show_transcript": "Vis transskript",
"summarize_lesson": "Opsummer lektion",
"summarize_lesson_prompt": {
"lesson_only": "Opsummer, hvad der er blevet undervist i denne lektion.",
"transcript_only": "Se transskriptet af den uploadede video, og opsummer, hvad der nævnes i transskriptet.",
"transcript_and_notes": "Se transskriptet af den uploadede video, og opsummer, hvad der nævnes i transskriptet. Opsummer også, hvad der dækkes i lektionsnoterne."
},
"button_done": "Færdig",
"tabs": {
"settings": {
@@ -1889,6 +1889,13 @@
"no_content": "Kein Inhalt",
"no_translation": "Keine Übersetzung für die ausgewählte Sprache",
"button": "Taste",
"show_transcript": "Transkript anzeigen",
"summarize_lesson": "Lektion zusammenfassen",
"summarize_lesson_prompt": {
"lesson_only": "Fassen Sie zusammen, was in dieser Lektion vermittelt wurde.",
"transcript_only": "Sehen Sie sich das Transkript des hochgeladenen Videos an und fassen Sie zusammen, was im Transkript erwähnt wird.",
"transcript_and_notes": "Sehen Sie sich das Transkript des hochgeladenen Videos an und fassen Sie zusammen, was im Transkript erwähnt wird. Fassen Sie auch zusammen, was in den Lektionsnotizen behandelt wird."
},
"button_done": "Fertig",
"tabs": {
"settings": {
@@ -2265,6 +2265,13 @@
"no_content": "No Content",
"no_translation": "No translation for the selected language",
"button": "button",
"show_transcript": "Show transcript",
"summarize_lesson": "Summarize lesson",
"summarize_lesson_prompt": {
"lesson_only": "Summarize what has been taught in this lesson.",
"transcript_only": "Look at the transcript of the uploaded video and summarize what is mentioned in the transcript.",
"transcript_and_notes": "Look at the transcript of the uploaded video and summarize what is mentioned in the transcript. Also summarize what is covered in the lesson notes."
},
"tabs": {
"note": {
"title": "Note",
@@ -1889,6 +1889,13 @@
"no_content": "Sin contenido",
"no_translation": "No hay traducción para el idioma seleccionado.",
"button": "botón",
"show_transcript": "Mostrar transcripción",
"summarize_lesson": "Resumir lección",
"summarize_lesson_prompt": {
"lesson_only": "Resume lo que se ha enseñado en esta lección.",
"transcript_only": "Consulta la transcripción del video subido y resume lo que se menciona en la transcripción.",
"transcript_and_notes": "Consulta la transcripción del video subido y resume lo que se menciona en la transcripción. También resume lo que se cubre en las notas de la lección."
},
"button_done": "hecho",
"tabs": {
"settings": {
@@ -1889,6 +1889,13 @@
"no_content": "Aucun contenu",
"no_translation": "Aucune traduction pour la langue sélectionnée",
"button": "bouton",
"show_transcript": "Afficher la transcription",
"summarize_lesson": "Résumer la leçon",
"summarize_lesson_prompt": {
"lesson_only": "Résumez ce qui a été enseigné dans cette leçon.",
"transcript_only": "Consultez la transcription de la vidéo téléchargée et résumez ce qui y est mentionné.",
"transcript_and_notes": "Consultez la transcription de la vidéo téléchargée et résumez ce qui y est mentionné. Résumez également ce qui est couvert dans les notes de cours."
},
"button_done": "Terminé",
"tabs": {
"settings": {
@@ -1889,6 +1889,13 @@
"no_content": "कोई सामग्री नहीं",
"no_translation": "चयनित भाषा के लिए कोई अनुवाद नहीं",
"button": "बटन",
"show_transcript": "प्रतिलेख दिखाएं",
"summarize_lesson": "पाठ का सारांश",
"summarize_lesson_prompt": {
"lesson_only": "इस पाठ में क्या सिखाया गया है, उसका सारांश दें।",
"transcript_only": "अपलोड किए गए वीडियो का प्रतिलेख देखें और प्रतिलेख में जो कुछ उल्लेखित है उसका सारांश दें।",
"transcript_and_notes": "अपलोड किए गए वीडियो का प्रतिलेख देखें और प्रतिलेख में जो कुछ उल्लेखित है उसका सारांश दें। साथ ही पाठ नोट्स में जो कवर किया गया है उसका भी सारांश दें।"
},
"button_done": "हो गया",
"tabs": {
"settings": {
@@ -1889,6 +1889,13 @@
"no_content": "Brak treści",
"no_translation": "Brak tłumaczenia dla wybranego języka",
"button": "przycisk",
"show_transcript": "Pokaż transkrypt",
"summarize_lesson": "Podsumuj lekcję",
"summarize_lesson_prompt": {
"lesson_only": "Podsumuj, czego nauczono się w tej lekcji.",
"transcript_only": "Przejrzyj transkrypcję przesłanego wideo i podsumuj, co zostało w niej wspomniane.",
"transcript_and_notes": "Przejrzyj transkrypcję przesłanego wideo i podsumuj, co zostało w niej wspomniane. Podsumuj także to, co zostało omówione w notatkach do lekcji."
},
"button_done": "Gotowe",
"tabs": {
"settings": {
@@ -1889,6 +1889,13 @@
"no_content": "Sem conteúdo",
"no_translation": "Nenhuma tradução para o idioma selecionado",
"button": "botão",
"show_transcript": "Mostrar transcrição",
"summarize_lesson": "Resumir lição",
"summarize_lesson_prompt": {
"lesson_only": "Resuma o que foi ensinado nesta lição.",
"transcript_only": "Consulte a transcrição do vídeo enviado e resuma o que é mencionado na transcrição.",
"transcript_and_notes": "Consulte a transcrição do vídeo enviado e resuma o que é mencionado na transcrição. Resuma também o que é abordado nas notas da lição."
},
"button_done": "Concluído",
"tabs": {
"settings": {
@@ -1889,6 +1889,13 @@
"no_content": "Нет контента",
"no_translation": "Нет перевода на выбранный язык",
"button": "кнопка",
"show_transcript": "Показать транскрипт",
"summarize_lesson": "Суммировать урок",
"summarize_lesson_prompt": {
"lesson_only": "Кратко изложите, чему учили в этом уроке.",
"transcript_only": "Просмотрите транскрипт загруженного видео и кратко изложите, что в нём упоминается.",
"transcript_and_notes": "Просмотрите транскрипт загруженного видео и кратко изложите, что в нём упоминается. Также кратко изложите, что рассматривается в заметках к уроку."
},
"button_done": "Готово",
"tabs": {
"settings": {
@@ -1889,6 +1889,13 @@
"no_content": "Không có nội dung",
"no_translation": "Không có bản dịch cho ngôn ngữ đã chọn",
"button": "nút",
"show_transcript": "Hiển thị bản ghi",
"summarize_lesson": "Tóm tắt bài học",
"summarize_lesson_prompt": {
"lesson_only": "Tóm tắt những gì đã được dạy trong bài học này.",
"transcript_only": "Xem bản ghi của video đã tải lên và tóm tắt những gì được đề cập trong bản ghi.",
"transcript_and_notes": "Xem bản ghi của video đã tải lên và tóm tắt những gì được đề cập trong bản ghi. Đồng thời tóm tắt nội dung trong ghi chú bài học."
},
"button_done": "Xong",
"tabs": {
"settings": {