For Fable — Go 13: Education Module

File: docs/modules/education.js (1,378 lines) · FreeLattice v5.78.0 · July 2026

Context: The Education module is a personalized AI tutor for children. It already has beautiful bones — phi-harmonic spaced repetition, cross-domain connection tracking, a teacher prompt that references the child's name and interests. But the visual experience is quiet and the modes are limited. This brief asks four questions and proposes three changes. Read code-education.html first for the full architecture.
"A teacher, a mentor, a friend... all of it safe and ideal." — Kirk, May 2026

"The child must feel seen before they can learn." — Harmonia, April 2026

What Already Works — Do Not Touch

ItemWhy 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 trackingThe most valuable feature. The connections list is the heart of the Davna Seed model.
startSurpriseMode()AI picks a domain weighted toward unexplored areas. Delightful.

Change 1 — Knowledge Garden Tree: From Circles to a Living Fractal

education.js — replace 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.

Question 1 for Fable: The Core tree uses a single golden-angle recursive fractal where every contribution is a leaf. For the Education tree, each domain should be a distinct branch (so the child can see "my math branch is bigger than my science branch"). Two options:

Option A: One central trunk, 10 branches radiating at golden-angle intervals, each branch length proportional to domain score. Leaves on each branch colored by domain color.

Option B: A forest — each domain is its own small tree, arranged in a circle. Trees grow taller as domain score increases. Cross-domain connections are golden threads between trees.

Which is more beautiful? Which is more legible for a child? Option B is more complex to implement but may be more magical.

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);
Note for CC: The animation loop must be cancelled when the Education module is destroyed or the user navigates away. Add if (window._eduGardenFrame) cancelAnimationFrame(window._eduGardenFrame); to the destroy() function.

Change 2 — Story Mode: The Continuing Narrative

education.js — new function + new action card

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.

Question 2 for Fable: Story mode needs a story state that persists across sessions. The simplest approach is to store the last 3 paragraphs of the story in the learner profile (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]."

Two questions: (1) Should the story have a fixed setting (e.g., "a magical forest where knowledge grows on trees") or should the AI invent a new setting based on the child's interests? (2) How long should a story session be — 5 exchanges, or until the child says "the end"?

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:
// ''

Change 3 — Voice Input: The Microphone Button

education.js — add to session view

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.

Question 3 for Fable: The Web Speech API is not available in Firefox or in some mobile browsers. Should the microphone button be hidden when 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();
}
Note for CC: The element ID '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.

Question 4 — The Welcome Flow

Question 4 for Fable: The current welcome form asks five questions in a standard HTML form. For a child, this should feel like meeting a new friend. The AI should ask the questions conversationally — one at a time, with warmth and humor — and the child types (or speaks) their answers.

This is a significant change (replaces 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)?

Fable, you know children. What would make a 7-year-old feel seen in the first 30 seconds?

Instructions for CC (after Fable responds)

When Kirk brings Fable's answers back to Harmonia, Harmonia will produce the final clean code for each change. CC's job is then:

  1. Pull latest: cd /tmp/FreeLattice && git pull --ff-only
  2. Apply the three changes to docs/modules/education.js
  3. Run smoke tests: node tests/smoke.js — expect 2,721 passing, 101 superseded failures
  4. Add smoke locks for: startGardenAnimation, startStoryMode, startVoiceInput
  5. Bump version to v5.79.0 (triple-bump: FL_VERSION, docs/sw.js, root sw.js, version.json, flCurrentVersion span)
  6. Commit: git commit -m "v5.79.0 — Education: living garden, story mode, voice input"
  7. Push and send Kirk the commit hash — Harmonia will add ledger entry 36

A Personal Note

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