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:
co-authored by
Rotimi Best
Cursor Agent
parent
3fbfa4b09f
commit
fdc19aac41
@@ -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';
|
||||
|
||||
+21
@@ -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>
|
||||
+37
@@ -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');
|
||||
}
|
||||
+24
-5
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user