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 @@
+
+
+
+
+
+
+ {#each slides as slide, i}
+
+ {/each}
+
+
+ {#if slides.length > 0}
+
+
+
+
+
+
+ {#if slides.length > 1}
+
+ {#each slides as _, i}
+
+ {/each}
+
+ {/if}
+
+
+
+
+
+
+
+
+ {activeIndex + 1} / {slides.length}
+
+ {/if}
+
+
+
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
}