feat(dashboard): restyle lesson transcript and summarize actions (#758)

* feat(dashboard): restyle lesson transcript and summarize actions

Add separator lines around lesson material actions below the video,
switch transcript and summarize controls to plain ghost buttons with
hover-animated icons, and separate the two actions with a pipe when both
are visible. Display rules are unchanged.

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

* refactor(dashboard): inline summarize action into material actions

Fold lesson-summarize-button into lesson-material-actions and remove the
standalone component. Lessons without videos use the same actions bar with
separators disabled.

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

* fix(dashboard): align lesson actions with note width on mobile

Constrain material actions to max-w-2xl when there is no video so they
match the note column. Improve small-screen layout with tighter padding,
flex-wrap, and hide the pipe separator below the sm breakpoint.

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

* add script update

* fix(dashboard): tighten spacing between lesson video and note

Remove gap-2 from the lesson content stack and drop bottom margin on the
last video wrapper so material actions separators control the gap before
the note.

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

* fix(dashboard): show bottom separator on note-only lesson actions

Lessons without video were passing showSeparators=false, hiding both
lines. Split into top/bottom controls so summarize still gets a line before
the note without one above the action row.

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

* fix(dashboard): always show top separator on lesson material actions

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

* fix(dashboard): reduce lesson material action separator margin to my-2

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

* feat(dashboard): consolidate multi-video transcript actions and navigation

Register transcript sources per upload video, show a single material
actions row after all videos, and add prev/next video navigation in the
transcript side panel when multiple transcripts are available.

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

* fix(dashboard): address greptile review on lesson material actions

Require lessonId on material actions and stop overwriting the transcript
seek wrapper with a stale localSeekFn snapshot in the sync effect.

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

* fix(dashboard): stop transcript registry effect infinite loop

Replace the reactive sync effect with explicit registry updates from
loadTranscript and skip no-op transcript source writes to avoid reading
and writing transcriptSources in the same effect pass.

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

* fix(ui): raise tooltip z-index above app bar side panel

Use z-menu-elevated for tooltip content so header icon tooltips are not
hidden behind the fixed transcript side panel (z-app-bar).

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

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Rotimi Best
2026-07-15 22:28:40 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 656fa4fa7e
commit 3980aa0a9b
23 changed files with 580 additions and 98 deletions
@@ -10,4 +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';
export { default as LessonMaterialActions } from './lesson-material-actions.svelte';
@@ -0,0 +1,78 @@
<script lang="ts">
import { Button } from '@cio/ui/base/button';
import { Separator } from '@cio/ui/base/separator';
import { ContentIcon, HoverableItem, PremiumIcon } from '@cio/ui/custom/moving-icons';
import { sendPromptToAssistant } from '$features/ai-assistant/utils/store';
import { t } from '$lib/utils/functions/translations';
import { buildLessonSummarizePrompt } from './utils/lesson-summarize-prompt';
interface Props {
showTranscript?: boolean;
showSummarize?: boolean;
showTopSeparator?: boolean;
showBottomSeparator?: boolean;
/** Match the note column (`max-w-2xl`) instead of the full video column. */
alignWithNote?: boolean;
lessonId: string;
onTranscript?: () => void;
}
let {
showTranscript = false,
showSummarize = false,
showTopSeparator = true,
showBottomSeparator = true,
alignWithNote = false,
lessonId,
onTranscript = () => {}
}: Props = $props();
function handleSummarizeLesson() {
sendPromptToAssistant(buildLessonSummarizePrompt(lessonId));
}
</script>
{#if showTranscript || showSummarize}
<div class={alignWithNote ? 'mx-auto w-full max-w-2xl' : 'w-full'}>
{#if showTopSeparator}
<Separator class="my-2" />
{/if}
<div class="flex flex-wrap items-center gap-x-2 gap-y-2 py-1 sm:gap-x-3">
{#if showTranscript}
<HoverableItem>
{#snippet children(isHovered)}
<Button variant="ghost" size="sm" class="ui:h-auto ui:px-1 ui:py-1 sm:ui:px-2" onclick={onTranscript}>
<ContentIcon {isHovered} size={16} />
{$t('course.navItem.lessons.materials.show_transcript')}
</Button>
{/snippet}
</HoverableItem>
{/if}
{#if showTranscript && showSummarize}
<span class="ui:text-muted-foreground hidden text-sm sm:inline" aria-hidden="true">|</span>
{/if}
{#if showSummarize}
<HoverableItem>
{#snippet children(isHovered)}
<Button
variant="ghost"
size="sm"
class="ui:h-auto ui:px-1 ui:py-1 sm:ui:px-2"
onclick={handleSummarizeLesson}
>
<PremiumIcon {isHovered} size={16} />
{$t('course.navItem.lessons.materials.summarize_lesson')}
</Button>
{/snippet}
</HoverableItem>
{/if}
</div>
{#if showBottomSeparator}
<Separator class="my-2" />
{/if}
</div>
{/if}
@@ -1,21 +0,0 @@
<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>
@@ -1,18 +1,32 @@
import { SvelteMap } from 'svelte/reactivity';
import type { AssetTranscriptPayload } from '$features/media/utils/types';
type SeekFn = (seconds: number) => void;
export type LessonTranscriptSource = {
assetId: string;
videoIndex: number;
transcript: AssetTranscriptPayload | null;
transcriptLoading: boolean;
currentTimeSeconds: number;
seek: SeekFn;
};
class LessonVideoBus {
/** Asset id of the currently mounted upload-video player (if any). */
assetId = $state<string | null>(null);
/** Last reported playback time in seconds. */
/** Last reported playback time in seconds for the active player asset. */
currentTimeSeconds = $state(0);
/** True after the user (or autoplay) has started playback at least once for the current asset. */
hasPlayed = $state(false);
/** Latest transcript payload for the active asset; null if none / still loading. */
/** Latest transcript payload for the active transcript source. */
transcript = $state<AssetTranscriptPayload | null>(null);
/** True while the transcript fetch is in flight. */
/** True while the active transcript fetch is in flight. */
transcriptLoading = $state(false);
/** Transcript sources keyed by upload asset id. */
transcriptSources = new SvelteMap<string, LessonTranscriptSource>();
/** Which upload asset transcript is shown in the side panel. */
activeTranscriptAssetId = $state<string | null>(null);
private seekFn: SeekFn = () => {};
@@ -21,14 +35,136 @@ class LessonVideoBus {
}
seek(seconds: number) {
const activeSource = this.getActiveTranscriptSource();
if (activeSource) {
activeSource.seek(seconds);
return;
}
this.seekFn(seconds);
}
registerTranscriptSource(source: LessonTranscriptSource) {
this.transcriptSources.set(source.assetId, source);
}
unregisterTranscriptSource(assetId: string) {
this.transcriptSources.delete(assetId);
if (this.activeTranscriptAssetId === assetId) {
const nextSource = this.getNavigableTranscriptSources()[0] ?? null;
this.activeTranscriptAssetId = nextSource?.assetId ?? null;
this.syncActiveTranscriptToLegacyFields();
}
}
updateTranscriptSource(assetId: string, patch: Partial<LessonTranscriptSource>) {
const existing = this.transcriptSources.get(assetId);
if (!existing) return;
let next = existing;
let changed = false;
for (const key of Object.keys(patch) as (keyof LessonTranscriptSource)[]) {
const value = patch[key];
if (value !== undefined && existing[key] !== value) {
if (!changed) {
next = { ...existing };
changed = true;
}
next[key] = value as LessonTranscriptSource[typeof key];
}
}
if (!changed) return;
this.transcriptSources.set(assetId, next);
if (this.activeTranscriptAssetId === assetId) {
this.syncActiveTranscriptToLegacyFields();
}
}
getActiveTranscriptSource(): LessonTranscriptSource | null {
const assetId = this.activeTranscriptAssetId;
if (!assetId) return null;
return this.transcriptSources.get(assetId) ?? null;
}
getNavigableTranscriptSources(): LessonTranscriptSource[] {
return [...this.transcriptSources.values()]
.filter((source) => (source.transcript?.segments?.length ?? 0) > 0)
.sort((left, right) => left.videoIndex - right.videoIndex);
}
hasTranscriptAvailable(): boolean {
return [...this.transcriptSources.values()].some((source) => source.transcript != null);
}
selectTranscriptSource(assetId: string) {
if (!this.transcriptSources.has(assetId)) return;
this.activeTranscriptAssetId = assetId;
this.syncActiveTranscriptToLegacyFields();
}
prepareTranscriptPanelSelection() {
const navigableSources = this.getNavigableTranscriptSources();
if (navigableSources.length === 0) {
const fallbackSource = [...this.transcriptSources.values()]
.filter((source) => source.transcript || source.transcriptLoading)
.sort((left, right) => left.videoIndex - right.videoIndex)[0];
if (!fallbackSource) return false;
this.selectTranscriptSource(fallbackSource.assetId);
return true;
}
const currentAssetId = this.activeTranscriptAssetId;
const hasCurrent = currentAssetId != null && navigableSources.some((source) => source.assetId === currentAssetId);
if (!hasCurrent) {
this.selectTranscriptSource(navigableSources[0].assetId);
}
return true;
}
/** Replace the active transcript (e.g. after an edit is saved). */
setTranscript(transcript: AssetTranscriptPayload | null) {
const assetId = this.activeTranscriptAssetId;
if (assetId) {
this.updateTranscriptSource(assetId, { transcript });
return;
}
this.transcript = transcript;
}
private syncActiveTranscriptToLegacyFields() {
const source = this.getActiveTranscriptSource();
if (!source) {
this.transcript = null;
this.transcriptLoading = false;
return;
}
this.assetId = source.assetId;
this.transcript = source.transcript;
this.transcriptLoading = source.transcriptLoading;
this.seekFn = source.seek;
}
/** Reset all per-lesson state (called when the upload asset changes or the player unmounts). */
reset(opts?: { keepAssetId?: boolean }) {
if (!opts?.keepAssetId) {
@@ -39,6 +175,8 @@ class LessonVideoBus {
this.hasPlayed = false;
this.transcript = null;
this.transcriptLoading = false;
this.activeTranscriptAssetId = null;
this.transcriptSources.clear();
this.seekFn = () => {};
}
}
@@ -1,6 +1,5 @@
<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';
@@ -17,7 +16,6 @@
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
@@ -100,10 +98,10 @@
video: LessonVideo;
courseId: string;
lessonId: string;
showSummarizeButton?: boolean;
videoIndex: number;
}
let { video, courseId, lessonId, showSummarizeButton = false }: Props = $props();
let { video, courseId, lessonId, videoIndex }: Props = $props();
let localTranscript = $state<AssetTranscriptPayload | null>(null);
let localTranscriptLoading = $state(false);
@@ -128,19 +126,26 @@
let localSeekFn: (seconds: number) => void = () => {};
function ownsVideoBus(): boolean {
function ownsPlaybackBus(): boolean {
return uploadAssetId !== null && lessonVideoBus.assetId === uploadAssetId;
}
function syncVideoBus(): void {
function syncPlaybackBus(): void {
if (!uploadAssetId) return;
lessonVideoBus.assetId = uploadAssetId;
lessonVideoBus.transcript = localTranscript;
lessonVideoBus.transcriptLoading = localTranscriptLoading;
lessonVideoBus.setSeekFn(localSeekFn);
}
function syncTranscriptToRegistry(): void {
if (!uploadAssetId) return;
lessonVideoBus.updateTranscriptSource(uploadAssetId, {
transcript: localTranscript,
transcriptLoading: localTranscriptLoading
});
}
$effect(() => {
const rawUrl = localTranscript?.vttUrl;
if (!rawUrl) {
@@ -254,9 +259,7 @@
if (!uploadAssetId) {
if (isMounted) {
localTranscript = null;
if (ownsVideoBus()) {
lessonVideoBus.transcript = null;
}
syncTranscriptToRegistry();
}
return;
@@ -265,9 +268,7 @@
if (!isMounted) return;
localTranscriptLoading = true;
if (ownsVideoBus()) {
lessonVideoBus.transcriptLoading = true;
}
syncTranscriptToRegistry();
try {
const data = await mediaApi.getAssetTranscript(uploadAssetId);
@@ -276,15 +277,11 @@
localTranscript = data;
scheduleVttRefetch(data?.vttUrlExpiresAt);
if (ownsVideoBus()) {
lessonVideoBus.transcript = data;
}
syncTranscriptToRegistry();
} finally {
if (isMounted) {
localTranscriptLoading = false;
if (ownsVideoBus()) {
lessonVideoBus.transcriptLoading = false;
}
syncTranscriptToRegistry();
}
}
}
@@ -392,8 +389,19 @@
}
onMount(() => {
if (uploadAssetId) {
lessonVideoBus.registerTranscriptSource({
assetId: uploadAssetId,
videoIndex,
transcript: null,
transcriptLoading: false,
currentTimeSeconds: 0,
seek: (seconds) => localSeekFn(seconds)
});
}
if (lessonVideoBus.assetId === null || lessonVideoBus.assetId === uploadAssetId) {
syncVideoBus();
syncPlaybackBus();
}
void restoreWatchProgress();
@@ -413,9 +421,22 @@
stopJobPoller();
revokeBlobTrackUrl?.();
revokeBlobTrackUrl = null;
if (ownsVideoBus()) {
if (uploadAssetId) {
lessonVideoBus.unregisterTranscriptSource(uploadAssetId);
}
if (lessonVideoBus.transcriptSources.size === 0) {
lessonVideoBus.reset();
sidePanel.closeIfScope('lesson');
return;
}
if (uploadAssetId && ownsPlaybackBus()) {
lessonVideoBus.assetId = null;
lessonVideoBus.currentTimeSeconds = 0;
lessonVideoBus.hasPlayed = false;
lessonVideoBus.setSeekFn(() => {});
}
});
@@ -434,12 +455,14 @@
);
function handleFirstPlay() {
syncVideoBus();
syncPlaybackBus();
lessonVideoBus.hasPlayed = true;
}
function openTranscriptPanel() {
syncVideoBus();
if (!uploadAssetId) return;
lessonVideoBus.selectTranscriptSource(uploadAssetId);
sidePanel.open(TRANSCRIPT_PANEL_ID);
}
@@ -468,7 +491,11 @@
controls: true,
playsinline: true,
onTimeUpdate: (seconds) => {
if (ownsVideoBus()) {
if (uploadAssetId) {
lessonVideoBus.updateTranscriptSource(uploadAssetId, { currentTimeSeconds: seconds });
}
if (ownsPlaybackBus()) {
lessonVideoBus.currentTimeSeconds = seconds;
}
},
@@ -476,7 +503,8 @@
localSeekFn = (seconds) => {
player.currentTime = seconds;
};
if (ownsVideoBus()) {
if (ownsPlaybackBus()) {
lessonVideoBus.setSeekFn(localSeekFn);
}
},
@@ -494,18 +522,4 @@
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>
@@ -1,10 +1,13 @@
<script lang="ts">
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import XIcon from '@lucide/svelte/icons/x';
import PencilIcon from '@lucide/svelte/icons/pencil';
import CaptionsIcon from '@lucide/svelte/icons/captions';
import { IconButton } from '@cio/ui/custom/icon-button';
import { Button } from '@cio/ui/base/button';
import { Textarea } from '@cio/ui/base/textarea';
import { Separator } from '@cio/ui/base/separator';
import { t } from '$lib/utils/functions/translations';
import { isOrgAdmin } from '$lib/utils/store/org';
import { sidePanel } from '$features/side-panel';
@@ -12,12 +15,20 @@
import { lessonVideoBus } from './lesson-video-bus.svelte';
import type { TranscriptSegment } from '$features/media/utils/types';
const transcript = $derived(lessonVideoBus.transcript);
const transcriptLoading = $derived(lessonVideoBus.transcriptLoading);
const activeSource = $derived.by(() => lessonVideoBus.getActiveTranscriptSource());
const transcript = $derived(activeSource?.transcript ?? null);
const transcriptLoading = $derived(activeSource?.transcriptLoading ?? false);
const segments = $derived<TranscriptSegment[]>(transcript?.segments ?? []);
const language = $derived(transcript?.language ?? '');
const assetId = $derived(lessonVideoBus.assetId);
const currentTimeSeconds = $derived(lessonVideoBus.currentTimeSeconds);
const assetId = $derived(activeSource?.assetId ?? null);
const currentTimeSeconds = $derived(activeSource?.currentTimeSeconds ?? 0);
const navigableSources = $derived.by(() => lessonVideoBus.getNavigableTranscriptSources());
const activeNavIndex = $derived(navigableSources.findIndex((source) => source.assetId === assetId));
const activeVideoNumber = $derived(activeNavIndex >= 0 ? activeNavIndex + 1 : 1);
const canGoToPreviousVideo = $derived(activeNavIndex > 0);
const canGoToNextVideo = $derived(activeNavIndex >= 0 && activeNavIndex < navigableSources.length - 1);
const showVideoNavigation = $derived(navigableSources.length > 1);
let editing = $state(false);
let saving = $state(false);
@@ -43,12 +54,34 @@
}
function formatTimestamp(seconds: number): string {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60)
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60)
.toString()
.padStart(2, '0');
return `${m}:${s}`;
return `${minutes}:${remainingSeconds}`;
}
function goToPreviousVideo() {
if (!canGoToPreviousVideo) return;
if (editing) {
cancelEditing();
}
const previousSource = navigableSources[activeNavIndex - 1];
lessonVideoBus.selectTranscriptSource(previousSource.assetId);
}
function goToNextVideo() {
if (!canGoToNextVideo) return;
if (editing) {
cancelEditing();
}
const nextSource = navigableSources[activeNavIndex + 1];
lessonVideoBus.selectTranscriptSource(nextSource.assetId);
}
function startEditing() {
@@ -122,6 +155,38 @@
</div>
</header>
{#if showVideoNavigation && !editing}
<div class="border-border flex items-center justify-between gap-2 border-b px-4 py-2">
<IconButton
onclick={goToPreviousVideo}
variant="outline"
size="icon-xs"
disabled={!canGoToPreviousVideo}
tooltip={$t('course.navItem.lessons.materials.tabs.video.transcript.previous_video')}
>
<ChevronLeftIcon size={16} />
</IconButton>
<p class="min-w-0 truncate text-center text-sm font-medium">
{$t('course.navItem.lessons.materials.tabs.video.transcript.video_nav_label', {
number: activeVideoNumber
})}
</p>
<IconButton
onclick={goToNextVideo}
variant="outline"
size="icon-xs"
disabled={!canGoToNextVideo}
tooltip={$t('course.navItem.lessons.materials.tabs.video.transcript.next_video')}
>
<ChevronRightIcon size={16} />
</IconButton>
</div>
{:else if showVideoNavigation && editing}
<Separator />
{/if}
<div class="min-h-0 flex-1 overflow-y-auto p-3">
{#if editing}
{#each draft as segment, index (index)}
@@ -187,16 +187,27 @@
async function handleViewTranscript() {
if (!assetId) return;
lessonVideoBus.assetId = assetId;
lessonVideoBus.transcriptLoading = true;
if (!lessonVideoBus.transcriptSources.has(assetId)) {
lessonVideoBus.registerTranscriptSource({
assetId,
videoIndex: 0,
transcript: null,
transcriptLoading: true,
currentTimeSeconds: 0,
seek: () => {}
});
}
lessonVideoBus.selectTranscriptSource(assetId);
lessonVideoBus.updateTranscriptSource(assetId, { transcriptLoading: true });
sidePanel.open(TRANSCRIPT_PANEL_ID);
try {
const data = await mediaApi.getAssetTranscript(assetId);
lessonVideoBus.transcript = data;
lessonVideoBus.updateTranscriptSource(assetId, { transcript: data, transcriptLoading: false });
hasTranscript = !!data?.segments?.length;
} finally {
lessonVideoBus.transcriptLoading = false;
} catch {
lessonVideoBus.updateTranscriptSource(assetId, { transcriptLoading: false });
}
}
@@ -5,11 +5,15 @@
import { Empty } from '@cio/ui/custom/empty';
import * as Item from '@cio/ui/base/item';
import { lessonVideoUpload } from '$features/course/components/lesson/store';
import LessonMaterialActions from '$features/course/components/lesson/lesson-material-actions.svelte';
import { sidePanel } from '$features/side-panel';
import { t } from '$lib/utils/functions/translations';
import MODES from '$lib/utils/constants/mode';
import VideoIcon from '@lucide/svelte/icons/video';
import LessonVideoSimpleCard from './lesson-video-simple-card.svelte';
import LessonVideoPlayer from './lesson-video-player.svelte';
import { lessonVideoBus } from './lesson-video-bus.svelte';
import { TRANSCRIPT_PANEL_ID } from './transcript-panel-definition';
interface Props {
mode?: (typeof MODES)[keyof typeof MODES];
@@ -21,6 +25,11 @@
const videos = $derived(lessonApi.lesson?.videos || []);
const hasTranscript = $derived.by(() => {
lessonVideoBus.transcriptSources;
return lessonVideoBus.hasTranscriptAvailable();
});
let openDeleteVideoModal = $state(false);
let videoIndexToDelete = $state<number | null>(null);
@@ -40,11 +49,17 @@
}
openDeleteVideoModal = false;
}
function openTranscriptPanel() {
if (!lessonVideoBus.prepareTranscriptPanelSelection()) return;
sidePanel.open(TRANSCRIPT_PANEL_ID);
}
</script>
{#snippet content(video, index)}
{#key video.type === 'upload' ? ((video as typeof video & { assetId?: string }).assetId ?? video.link) : video.link}
<LessonVideoPlayer {video} {courseId} {lessonId} showSummarizeButton={index === videos.length - 1} />
<LessonVideoPlayer {video} {courseId} {lessonId} videoIndex={index} />
{/key}
{/snippet}
@@ -82,10 +97,17 @@
{#if videos.length}
<div class="w-full">
{#each videos as video, index}
<div class="mb-5 w-full overflow-hidden">
<div class="{index < videos.length - 1 ? 'mb-5' : ''} w-full overflow-hidden">
{@render content(video, index)}
</div>
{/each}
<LessonMaterialActions
showTranscript={hasTranscript}
showSummarize={videos.length > 0}
{lessonId}
onTranscript={openTranscriptPanel}
/>
</div>
{/if}
{/if}
@@ -47,7 +47,7 @@
AddVideoModal,
AddDocumentModal,
LessonSettingsTab,
LessonSummarizeButton
LessonMaterialActions
} from '$features/course/components/lesson';
import type { TLocale } from '@cio/db/types';
@@ -525,11 +525,9 @@
</UnderlineTabs.Root>
{:else if lessonApi.lesson && !isMaterialsEmpty}
{#key lessonId}
<div class="mb-20 flex w-full flex-col gap-2" in:fade={{ delay: 500 }} out:fade>
<div class="mb-20 flex w-full flex-col" in:fade={{ delay: 500 }} out:fade>
{#if !hasLessonVideos}
<div class="mb-2 flex flex-wrap gap-2">
<LessonSummarizeButton {lessonId} />
</div>
<LessonMaterialActions showSummarize {lessonId} alignWithNote />
{/if}
{#each viewModeComponents as Component, index (index)}