Generate interactive HTML slide-deck explainers for math concepts (grades 6-12). Use this skill whenever you need to create a multi-slide concept walkthrough with animated visuals, interactive elements, quizzes, and real-world examples. Trigger on any request involving "explainer", "slide deck", "concept walkthrough", "interactive lesson", or "generate explainer for" a concept/interest/grade. Claude Code generates complete HTML bundles directly (you ARE the LLM) and writes via TutorDataServiceWriter.
You generate interactive HTML slide-deck explainers for math concepts. Each explainer is a self-contained HTML file with multiple slides that students navigate through — featuring animated visuals, interactive elements, practice quizzes, and real-world applications themed to the student's interest.
This is the most engaging artifact type because it combines interactivity, pacing control, visual explanations, and quizzes in one package. Students click through at their own speed.
You ARE the LLM — generate HTML bundles directly. Do NOT call any external generation APIs.
import sys
from pathlib import Path
sys.path.insert(0, str(Path("src")))
from math_content_engine.integration.tutor_writer import TutorDataServiceWriter
from math_content_engine.personalization.theme_mapper import interest_to_theme, PIPELINE_INTERESTS
writer = TutorDataServiceWriter()
:root {
--bg: #0f0e17; /* deep dark background */
--card: #1a1830; /* card background */
--card2: #211f35; /* nested card background */
--accent: #ff6b6b; /* red accent */
--yellow: #ffd166; /* gold/yellow — highlights, answers */
--teal: #06d6a0; /* green — success, common factors */
--purple: #a78bfa; /* purple — secondary numbers */
--blue: #60a5fa; /* blue — primary numbers */
--white: #fffffe; /* text */
--muted: #a7a9be; /* secondary text */
--border: rgba(255,255,255,0.08); /* subtle borders */
}
Override --accent with the interest theme color:
| Interest | --accent override |
|---|---|
| basketball | #f97316 (orange) |
| soccer | #22c55e (green) |
| football | #3b82f6 (blue) |
| gaming | #8b5cf6 (purple) |
| pokemon | #eab308 (yellow) |
| space | #6366f1 (indigo) |
| animals | #14b8a6 (teal) |
| music | #ec4899 (pink) |
| cooking | #f59e0b (amber) |
| art | #a855f7 (violet) |
| robots | #0ea5e9 (sky) |
Two fonts via Google Fonts CDN:
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;700;800;900&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet">
Reasoning and equation text should be the SAME size (~1rem / 16px). Do NOT make equations oversized.
h1: clamp(2rem, 5vw, 3.2rem), weight 900h2: clamp(1.4rem, 3.5vw, 2rem), weight 8001.05rem, line-height 1.7, color #ccc1rem, weight 8000.92rem, color var(--muted)1rem, Space Mono — NOT larger than body text0.7rem, uppercase, letter-spacing 0.15emEvery explainer follows this structure:
--yellow highlight.show class)<nav class="nav">
<span class="nav-title">CONCEPT NAME</span>
<div class="progress-dots" id="dots"></div>
<div class="nav-btns">
<button class="nav-btn" id="btn-prev" onclick="prevSlide()">← Back</button>
<button class="nav-btn" id="btn-next" onclick="nextSlide()">Next →</button>
</div>
</nav>
Auto-play bar at bottom (with speed control):
<div class="autoplay-bar">
<button class="play-btn" onclick="togglePlay()">▶</button>
<div class="progress-bar-wrap"><div class="progress-bar-fill" id="pb-fill"></div></div>
<button id="speed-btn" onclick="cycleSpeed()" style="...monospace...min-width:48px" title="Playback speed">1x</button>
<span class="slide-counter" id="slide-counter">1 / 9</span>
</div>
const SPEEDS = [0.75, 1, 1.25, 1.5];
let speedIdx = 1; // default 1x
function cycleSpeed() {
speedIdx = (speedIdx + 1) % SPEEDS.length;
const spd = SPEEDS[speedIdx];
document.getElementById('speed-btn').textContent = spd + 'x';
document.getElementById('speed-btn').style.color = spd === 1 ? 'var(--muted)' : 'var(--accent)';
document.getElementById('speed-btn').style.borderColor = spd === 1 ? 'var(--border)' : 'var(--accent)';
if (audioEl) audioEl.playbackRate = spd;
}
// Apply speed when starting audio:
function speakSlide(n) {
// ...
audioEl.playbackRate = SPEEDS[speedIdx];
audioEl.play();
}
The speed button cycles 0.75x → 1x → 1.25x → 1.5x. Highlights in accent color when not at default. Applies playbackRate to both current and new audio.
let current = 0;
const total = document.querySelectorAll('.slide').length;
let autoInterval = null;
function goToSlide(n) {
document.querySelectorAll('.slide').forEach((s, i) => {
s.classList.toggle('active', i === n);
});
current = n;
updateDots();
updateButtons();
onSlideEnter(n); // trigger animations for this slide
}
function nextSlide() { if (current < total - 1) goToSlide(current + 1); }
function prevSlide() { if (current > 0) goToSlide(current - 1); }
function toggleAutoPlay() {
if (autoInterval) { clearInterval(autoInterval); autoInterval = null; }
else { autoInterval = setInterval(() => { if (current < total-1) nextSlide(); else clearInterval(autoInterval); }, 5000); }
}
function animateChips(containerId, numbers, colorClass, delay) {
const container = document.getElementById(containerId);
numbers.forEach((n, i) => {
const chip = document.createElement('div');
chip.className = 'chip ' + colorClass;
chip.textContent = n;
container.appendChild(chip);
setTimeout(() => chip.classList.add('show'), delay + i * 120);
});
}
function animateSteps(slideId, baseDelay) {
const steps = document.querySelectorAll('#' + slideId + ' .step');
steps.forEach((s, i) => {
setTimeout(() => s.classList.add('show'), baseDelay + i * 600);
});
}
function revealAfter(elementId, delay) {
setTimeout(() => {
const el = document.getElementById(elementId);
el.style.display = 'block';
}, delay);
}
Students click numbers 1-12 to test which are factors of a given number:
function buildFactorTest(target, containerId) {
const container = document.getElementById(containerId);
for (let i = 1; i <= 12; i++) {
const btn = document.createElement('button');
btn.className = 'quiz-btn';
btn.style.width = '60px'; btn.style.height = '50px';
btn.textContent = i;
btn.onclick = () => {
btn.classList.add(target % i === 0 ? 'correct' : 'wrong');
btn.disabled = true;
};
container.appendChild(btn);
}
}
Each quiz question needs a why field explaining the answer:
const QUIZ_CONCEPT = 'algebra.graphing.slope'; // concept_id for reporting
const quizData = [
{ q: '30 blocks in 6 min. Slope?', opts: ['3','5','6'], ans: '5',
why: 'Slope = rise/run = 30/6 = 5 blocks per minute.' },
// ...
];
let quizIdx = 0, quizScore = 0, quizMissed = [], quizAnswers = [];
On answer: track score, show explanation, highlight correct/wrong:
function answerQuiz(chosen, q) {
if (chosen === q.ans) quizScore++;
else quizMissed.push({ q: q.q, ans: q.ans, why: q.why });
quizAnswers.push({ question: q.q, chosen, correct_answer: q.ans, correct: chosen === q.ans });
// Show: "✅ Correct! Slope = rise/run = 30/6 = 5" or "❌ Not quite. 5 — Slope = rise/run..."
}
On quiz complete: show score, review missed questions, Try Again button, and report to parent:
// Post results to parent (tutor chat page)
window.parent.postMessage({
type: 'slide_deck_quiz',
concept: QUIZ_CONCEPT,
score: quizScore,
total: quizData.length,
percent: Math.round(quizScore / quizData.length * 100),
missed: quizMissed.map(m => ({ question: m.q, correct_answer: m.ans })),
answers: quizAnswers
}, '*');
The parent page (math_chat_frontend.html) listens for this message, shows a chat notification, and sends a [Quiz Result] message to the tutor agent. The agent responds with encouragement based on score.
Draw as SVG with circles (nodes) and lines (edges). Primes in accent color, composites in muted:
function drawTree(svgId, tree) {
// tree = [{val, children: [{val, children: [...]}]}]
// Recursive layout: each level is 45px lower
// Circles: r=16, fill depends on isPrime
// Lines: stroke #555, connect parent center to child center
}
SVG with tick marks, labeled points, and highlighted segments for factors.
Simple SVG circles with colored fills and centered text for quantities.
When generating a slide deck, you MUST use the student's interest theme for:
--accent)basketball — accent: #f97316
soccer — accent: #22c55e
football — accent: #3b82f6
gaming — accent: #8b5cf6
pokemon — accent: #eab308
space — accent: #6366f1
animals — accent: #14b8a6
music — accent: #ec4899
cooking — accent: #f59e0b
art — accent: #a855f7
harry_potter — accent: #9b59b6 (wizarding purple)
robots — accent: #0ea5e9
Each slide deck is a dict passed to writer.write_personalized_content(animations=[...]):
{
"asset_id": "ch7_pipeline_deck_7_1_basketball_grade_8_01",
"type": "slide_deck",
"title": "GCF Explorer: Greatest Common Factor",
"html_bundle": "<!DOCTYPE html><html>...</html>",
"duration_seconds": 45.0, # estimated auto-play duration (9 slides x 5s)
"display_order": 0,
}
ID convention: {source}_deck_{section}_{interest}_{grade}_{seq:02d}
Each slide deck includes voice narration using pre-generated Edge TTS MP3 files.
Use the EdgeTTSProvider to generate one MP3 per slide:
import asyncio
import edge_tts
from pathlib import Path
VOICE = "en-US-AndrewNeural" # Warm, confident teacher — works for all grades
RATE = "-20%" # 20% slower than normal — gives students time to absorb
# Write narration as a TEACHER TALKING, not reading the screen.
# Include humor, pauses (...), tips, encouragement, basketball metaphors.
# See "Narration Text Rules" below for the full style guide.
NARRATION = [
"Okay, pop quiz. What do you do when x shows up on BOTH sides? "
"Freak out? Nope. Cry? ...Maybe a little. "
"But actually, it's not that bad. I promise...",
# ... one per slide, teacher-style
]
async def generate_narration(output_dir: Path):
output_dir.mkdir(parents=True, exist_ok=True)
for i, text in enumerate(NARRATION):
out = output_dir / f"slide_{i}.mp3"
comm = edge_tts.Communicate(text=text, voice=VOICE, rate=RATE)
await comm.save(str(out))
asyncio.run(generate_narration(Path("audio")))
The slide deck narration must sound like the SAME person as the agentic_math_tutor chat agent. The agent's persona is defined in claude_math_teacher/prompts.py:
"You are a hype, supportive math tutor who vibes with students and genuinely wants them to succeed."
Persona traits that MUST be consistent across slides AND chat:
What NOT to sound like:
The narration is the agent's voice in audio form. When a student chats with the agent and then plays a slide deck, it should feel like the same teacher switched from texting to talking.
The narration should sound like a real teacher talking to a student, NOT like reading the text on screen. The voice adds what the visuals can't: context, reasoning, encouragement, and tips.
Before showing math — build intuition:
Explain WHY, not just WHAT:
Anticipate confusion:
Connect to prior knowledge:
Pause for thinking:
... in the text to create natural pausesReact to answers:
Give tips that aren't on screen:
Encourage:
Sound like a real person, not TTS:
... pauses)Humor (age-appropriate for grades 6-12):
Technical:
en-US-AndrewNeural voice (warm, confident teacher) as default... for natural pauses (the TTS engine adds a brief pause at ellipses)Default: en-US-AndrewNeural — warm, confident male teacher voice. Works across all grades 6-12.
The script matters MORE than the voice engine. A great teacher-style script with AndrewNeural sounds better than a flat screen-reading script with any premium voice.
| Voice | Style | Best For | SSML Styles |
|---|---|---|---|
en-US-AndrewNeural | Warm, confident teacher | All grades (default) | No express-as, but responds well to prosody/breaks |
en-US-AriaNeural | Expressive, versatile | Grades 6-10 | chat, excited, friendly, empathetic — best for style mixing |
en-US-JennyNeural | Cheerful, upbeat | Grades 6-8 | cheerful, chat, friendly |
en-US-AvaNeural | Caring, gentle | Grades 6-7 | No express-as |
en-US-BrianNeural | Casual, approachable | Grades 9-12 | No express-as |
For more expressive delivery, use AriaNeural with <mstts:express-as> SSML to switch emotions mid-slide:
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis"