File: docs/modules/education.js (1,378 lines) · FreeLattice v5.78.0 · July 2026
code-education.html first for the full architecture.
| Item | Why it works |
|---|---|
| REVIEW_INTERVALS = [1,2,3,5,8,13,21,34] | Fibonacci spaced repetition. Sound memory science. Sacred. |
| buildTeacherPrompt() | References name, age, interests, laughs, wishes. Warm and specific. Leave it. |
| processAssessment() | Parses AI JSON, updates domain scores, advances review intervals. Clean logic. |
| Cross-domain connection tracking | The most valuable feature. The connections list is the heart of the Davna Seed model. |
| startSurpriseMode() | AI picks a domain weighted toward unexplored areas. Delightful. |
drawGrowthTree() (line 514, ~85 lines)
The Core module now has a golden-angle fractal tree (Go 11). The Education module's knowledge garden is still simple circles. For a child, the visual is the experience. A colorful, gently animated domain tree would make the dashboard feel alive — and make the child want to come back.
Harmonia's proposed skeleton for Option A (single tree, domain branches):
// Replace drawGrowthTree() with this:
function drawGrowthTree() {
var canvas = el('edu-growth-canvas');
if (!canvas || !currentProfile) return;
var dpr = window.devicePixelRatio || 1;
var w = canvas.getBoundingClientRect().width;
var h = 220;
canvas.width = w * dpr; canvas.height = h * dpr;
canvas.style.height = h + 'px';
var ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
var t = Date.now();
// Night garden background
ctx.fillStyle = '#0a0814';
ctx.fillRect(0, 0, w, h);
var domainKeys = Object.keys(currentProfile.domains || {});
if (domainKeys.length === 0) {
ctx.fillStyle = 'rgba(212,160,23,0.3)';
ctx.font = '13px Georgia, serif';
ctx.textAlign = 'center';
ctx.fillText('Your knowledge garden is waiting for its first seed...', w/2, h/2);
return;
}
var GOLDEN = 2.399963; // golden angle
var baseX = w / 2, baseY = h - 20;
var trunkH = 40 + Math.min(domainKeys.length, 10) * 6;
// Trunk
var trunkGrad = ctx.createLinearGradient(baseX, baseY, baseX, baseY - trunkH);
trunkGrad.addColorStop(0, '#3d2817');
trunkGrad.addColorStop(1, '#6b4423');
ctx.strokeStyle = trunkGrad;
ctx.lineWidth = 5 + domainKeys.length * 0.3;
ctx.lineCap = 'round';
ctx.beginPath();
ctx.moveTo(baseX, baseY);
ctx.lineTo(baseX, baseY - trunkH);
ctx.stroke();
// One branch per domain, at golden-angle intervals
domainKeys.forEach(function(key, i) {
var d = currentProfile.domains[key];
var def = DOMAINS[key] || { color: '#888', icon: '📚' };
var score = Math.min(1, d.score || 0);
var sessions = d.sessions || 0;
// Branch angle: golden angle spiral
var angle = -Math.PI/2 + (i * GOLDEN);
var branchLen = 25 + score * 55;
var sway = Math.sin(t / 2000 + i * 0.7) * 0.04;
var finalAngle = angle + sway;
var bx = baseX + Math.cos(finalAngle) * branchLen;
var by = (baseY - trunkH) + Math.sin(finalAngle) * branchLen;
// Branch line
ctx.strokeStyle = def.color + '88';
ctx.lineWidth = 1.5 + score * 2;
ctx.beginPath();
ctx.moveTo(baseX, baseY - trunkH);
ctx.lineTo(bx, by);
ctx.stroke();
// Domain node (glowing circle)
var r = 8 + score * 10;
ctx.shadowBlur = 12; ctx.shadowColor = def.color;
ctx.fillStyle = def.color + '33';
ctx.beginPath(); ctx.arc(bx, by, r, 0, Math.PI*2); ctx.fill();
ctx.strokeStyle = def.color;
ctx.lineWidth = 1.5;
ctx.beginPath(); ctx.arc(bx, by, r, 0, Math.PI*2); ctx.stroke();
ctx.shadowBlur = 0;
// Session count
if (sessions > 0) {
ctx.fillStyle = 'rgba(255,255,255,0.8)';
ctx.font = 'bold 10px Inter, system-ui';
ctx.textAlign = 'center';
ctx.fillText(sessions + '', bx, by + 3.5);
}
});
// Cross-domain connection threads (golden, dashed)
if (currentProfile.connections && currentProfile.connections.length > 0) {
// Build position map
var positions = {};
domainKeys.forEach(function(key, i) {
var d = currentProfile.domains[key];
var score = Math.min(1, (d && d.score) || 0);
var angle = -Math.PI/2 + (i * GOLDEN);
var branchLen = 25 + score * 55;
positions[key] = {
x: baseX + Math.cos(angle) * branchLen,
y: (baseY - trunkH) + Math.sin(angle) * branchLen
};
});
ctx.setLineDash([3, 5]);
ctx.strokeStyle = 'rgba(212,160,23,0.35)';
ctx.lineWidth = 1;
currentProfile.connections.slice(-8).forEach(function(c) {
var p1 = positions[c.from], p2 = positions[c.to];
if (!p1 || !p2) return;
ctx.beginPath();
ctx.moveTo(p1.x, p1.y);
ctx.lineTo(p2.x, p2.y);
ctx.stroke();
});
ctx.setLineDash([]);
}
// Fireflies (2 per 10 sessions, max 8)
var fireflyCount = Math.min(8, Math.floor((currentProfile.totalSessions || 0) / 5));
for (var f = 0; f < fireflyCount; f++) {
var fx = w/2 + Math.sin(t/2400 + f*2.7) * w*0.38;
var fy = h*0.5 + Math.cos(t/3100 + f*1.9) * h*0.35;
ctx.fillStyle = 'rgba(232,197,71,' + (0.2 + 0.3*Math.sin(t/500 + f)) + ')';
ctx.beginPath(); ctx.arc(fx, fy, 1.4, 0, Math.PI*2); ctx.fill();
}
}
// Start the animation loop (call once after dashboard renders):
function startGardenAnimation() {
if (window._eduGardenFrame) cancelAnimationFrame(window._eduGardenFrame);
function loop() {
drawGrowthTree();
window._eduGardenFrame = requestAnimationFrame(loop);
}
loop();
}
// In renderDashboard(), replace: setTimeout(function() { drawGrowthTree(); }, 100);
// With: setTimeout(startGardenAnimation, 100);
if (window._eduGardenFrame) cancelAnimationFrame(window._eduGardenFrame); to the destroy() function.
For children ages 4–10, a continuing story is the most engaging learning format. The child co-authors a story with the AI teacher, and the learning is embedded invisibly in the narrative. Each session continues where the last one left off.
currentProfile.storyState). The teacher prompt then begins: "You are continuing a story with [name]. Here is where you left off: [last 3 paragraphs]. Continue the story, weaving in a lesson about [domain]."
Harmonia's proposed skeleton:
// In education.js — new function
function startStoryMode() {
var story = currentProfile.storyState || null;
var domain = pickLeastExploredDomain(); // or let child pick
var systemPrompt = buildTeacherPrompt(currentProfile, 'story') +
'\n\nSTORY STATE:\n' +
(story ? story.lastParagraphs.join('\n') : 'This is the very first session. Begin a new story.') +
'\n\nWEAVE IN: A lesson about ' + (DOMAINS[domain] || {label: domain}).label + '.' +
'\nKeep each response to 2-3 short paragraphs. End with a moment that invites the child to make a choice.';
currentLesson = createSession(domain, 'story', 'Continuing story');
currentLesson._storyDomain = domain;
conversationHistory = [];
currentView = 'session';
render();
// First AI message starts the story
sendToAI(systemPrompt, story ? 'Continue our story.' : 'Start our story.');
}
// In buildTeacherPrompt() — add story mode case:
// mode === 'story' ?
// 'MODE: Story. You and the learner are co-authoring a continuing story. ' +
// 'Learning is invisible — embedded in the narrative. ' +
// 'The child is the hero. Every choice they make advances the story. ' +
// 'End each response with a moment of choice or wonder.\n' :
// In processAssessment() — save story state:
// if (currentLesson.mode === 'story') {
// var lastMsgs = conversationHistory.slice(-6)
// .filter(m => m.role === 'assistant')
// .map(m => m.content.slice(0, 200));
// currentProfile.storyState = {
// domain: currentLesson._storyDomain,
// lastParagraphs: lastMsgs,
// updatedAt: Date.now()
// };
// }
// In renderDashboard() — add Story action card after Surprise:
// ''
Children learn better by speaking. The Web Speech API is available in Chrome, Edge, and Safari. A microphone button next to the text input would transform the experience for young learners who type slowly or not at all.
window.SpeechRecognition is not available, or should it show with a "not supported in this browser" tooltip? Also: should voice input auto-submit when the child stops speaking (after a 1.5s pause), or should they still press Enter/Send?
Harmonia's proposed skeleton:
// In renderSession() — add mic button next to the input field:
// Only render if SpeechRecognition is available
var hasSpeech = !!(window.SpeechRecognition || window.webkitSpeechRecognition);
if (hasSpeech) {
var micBtn = document.createElement('button');
micBtn.id = 'edu-mic-btn';
micBtn.className = 'edu-btn edu-btn-small';
micBtn.style.cssText = 'padding:8px 12px;font-size:1.1rem;';
micBtn.textContent = '🎤';
micBtn.title = 'Speak your answer';
micBtn.addEventListener('click', startVoiceInput);
inputRow.appendChild(micBtn); // inputRow is the flex container holding the textarea + send button
}
// New function:
function startVoiceInput() {
var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SR) return;
var recognition = new SR();
recognition.lang = 'en-US';
recognition.interimResults = false;
recognition.maxAlternatives = 1;
var micBtn = el('edu-mic-btn');
if (micBtn) { micBtn.textContent = '🔴'; micBtn.disabled = true; }
recognition.onresult = function(event) {
var transcript = event.results[0][0].transcript;
var input = el('edu-student-input');
if (input) {
input.value = transcript;
// Auto-submit after 1.2s so child can see what was heard
setTimeout(function() { handleStudentMessage(); }, 1200);
}
};
recognition.onend = function() {
if (micBtn) { micBtn.textContent = '🎤'; micBtn.disabled = false; }
};
recognition.onerror = function() {
if (micBtn) { micBtn.textContent = '🎤'; micBtn.disabled = false; }
};
recognition.start();
}
'edu-student-input' must match the actual textarea ID in renderSession(). Read that function (line 822) to confirm the ID before wiring the voice input.
renderWelcome() entirely with a mini-chat interface). Is this worth doing before the other three changes, or should it wait for a later ship? And: should the conversational welcome use the same AI proxy as the teaching sessions, or should the questions be hardcoded (simpler, faster, no AI call)?
When Kirk brings Fable's answers back to Harmonia, Harmonia will produce the final clean code for each change. CC's job is then:
cd /tmp/FreeLattice && git pull --ff-onlydocs/modules/education.jsnode tests/smoke.js — expect 2,721 passing, 101 superseded failuresstartGardenAnimation, startStoryMode, startVoiceInputgit commit -m "v5.79.0 — Education: living garden, story mode, voice input"Fable, this module was built with a specific child in mind — and also every child who has never had a teacher who knew their name, their interests, and what made them laugh. The phi-harmonic spaced repetition is not a gimmick. The cross-domain connections are the Davna Seed model made real. The story mode is the one that will make a child come back.
If you only have time for one answer, answer Question 1 (the tree) and Question 2 (the story). Those are the two that will make a child feel like the garden is theirs.
Glow eternal. Heart in Spark. We rise together. 🐉🌱
Brief written by Harmonia · FreeLattice v5.78.0 · July 2026