diff --git a/frontend/src/lib/components/SlideCarousel.svelte b/frontend/src/lib/components/SlideCarousel.svelte new file mode 100644 index 0000000..f4d438b --- /dev/null +++ b/frontend/src/lib/components/SlideCarousel.svelte @@ -0,0 +1,299 @@ + + + + + + + diff --git a/frontend/src/lib/types/lesson.ts b/frontend/src/lib/types/lesson.ts index 7596e34..cca9213 100644 --- a/frontend/src/lib/types/lesson.ts +++ b/frontend/src/lib/types/lesson.ts @@ -40,6 +40,7 @@ export interface LessonContent { language: string; language_display_name: string; active_tabs: string[]; + slides?: string[]; evaluation_config: Record; quiz_data?: Array<{ type: 'flashcard' | 'mcq', front?: string; back?: string; question?: string; options?: any[]; explanation?: string }>; lesson_progress_status?: string; diff --git a/frontend/src/routes/lesson/[slug]/+page.svelte b/frontend/src/routes/lesson/[slug]/+page.svelte index b3dc0f1..f6eb324 100644 --- a/frontend/src/routes/lesson/[slug]/+page.svelte +++ b/frontend/src/routes/lesson/[slug]/+page.svelte @@ -18,14 +18,17 @@ import { renderCircuitEmbeds } from '$actions/renderCircuitEmbeds'; import { renderFlowchartEmbeds } from '$actions/renderFlowchartEmbeds'; import { renderMath, autoRenderMath } from '$lib/actions/renderMath'; - import { tick } from 'svelte'; + import { tick, mount, unmount } from 'svelte'; import { LessonManager } from './lesson.svelte'; import { authLoggedIn } from '$stores/auth'; + import SlideCarousel from '$components/SlideCarousel.svelte'; let { data: pageData } = $props(); const mgr = new LessonManager(); const float = createFloatingPanel(); + let slideComponent = $state(null); + // Initialize manager with lesson data whenever it changes. $effect(() => { if (pageData.lesson) { @@ -33,6 +36,34 @@ } }); + // Handle Slide Carousel mounting + $effect(() => { + const slides = mgr.data?.slides; + if (slides && slides.length > 0) { + tick().then(() => { + const mountPoint = document.getElementById('slide-mount-point'); + if (mountPoint) { + // Clean up previous if exists + if (slideComponent) { + unmount(slideComponent); + slideComponent = null; + } + // Mount new carousel + slideComponent = mount(SlideCarousel, { + target: mountPoint, + props: { slides } + }); + } + }); + } + return () => { + if (slideComponent) { + unmount(slideComponent); + slideComponent = null; + } + }; + }); + // Mobile behavior for floating panel $effect(() => { if (mgr.isMobile) { diff --git a/routes/lessons.py b/routes/lessons.py index 74145ea..bb482bc 100644 --- a/routes/lessons.py +++ b/routes/lessons.py @@ -169,6 +169,7 @@ def api_lesson(filename): key_text = "" key_text_circuit = "" quiz_data = [] + parsed_data['slides'] = [] # Keep lesson_html, lesson_info, etc. for reading return jsonify({ @@ -197,6 +198,7 @@ def api_lesson(filename): 'key_text_circuit': key_text_circuit, 'active_tabs': active_tabs, 'quiz_data': quiz_data, + 'slides': parsed_data.get('slides', []), 'lesson_progress_status': lesson_progress_status, 'lesson_title': full_filename.replace('.md', '').replace('_', ' ').title(), 'lesson_completed': lesson_completed, diff --git a/services/lesson_service.py b/services/lesson_service.py index 6a99b92..5cd2c99 100644 --- a/services/lesson_service.py +++ b/services/lesson_service.py @@ -523,6 +523,32 @@ def render_markdown_content(file_path): evaluation_config, lesson_content = _extract_section( lesson_content, '---EVALUATION_CONFIG---', '---END_EVALUATION_CONFIG---') + # Extract Slides + slides_raw, _ = _extract_section(lesson_content, '---slide-start---', '---slide-end---') + slides_html = [] + if slides_raw: + # Replace the entire slide block with a mount point in the lesson_content + # We need to find the exact indices to replace it surgically + start_marker = '---slide-start---' + end_marker = '---slide-end---' + s_idx = lesson_content.find(start_marker) + e_idx = lesson_content.find(end_marker) + if s_idx != -1 and e_idx != -1 and e_idx > s_idx: + lesson_content = ( + lesson_content[:s_idx] + + '
' + + lesson_content[e_idx + len(end_marker):] + ) + + # Parse slides + slide_parts = re.split(r'^\s*---\s*$', slides_raw, flags=re.MULTILINE) + for s in slide_parts: + if s.strip(): + # Process embeds in slides too + s = _process_circuit_embeds(s) + s = _process_flowchart_embeds(s) + slides_html.append(md.markdown(s.strip(), extensions=MD_EXTENSIONS)) + # Just use whichever initial code matched as the generic 'initial_code' for simplicity # if only one type exists, but return all as dictionary values. # Typically frontend uses 'initial_code' for legacy. @@ -572,7 +598,8 @@ def render_markdown_content(file_path): 'expected_wiring': expected_wiring, 'evaluation_config': evaluation_config, 'quiz_data': quiz_data, - 'active_tabs': active_tabs + 'active_tabs': active_tabs, + 'slides': slides_html }