Everyday Mandarin – Lesson Planner
/* Custom styles */
body {
font-family: ‘Inter’, sans-serif;
}
/* Simple loading spinner */
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: #ffffff;
border-radius: 50%;
width: 24px;
height: 24px;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
Everyday Mandarin
AI Daily Lesson Planner
Your Daily Class Schedule
// — DOM Elements —
const form = document.getElementById(‘lesson-form’);
const reviewInput = document.getElementById(‘review-content’);
const teachInput = document.getElementById(‘teach-content’);
const generateBtn = document.getElementById(‘generate-btn’);
const btnText = document.getElementById(‘btn-text’);
const loadingSpinner = document.getElementById(‘loading-spinner’);
const errorMessage = document.getElementById(‘error-message’);
const formContainer = document.getElementById(‘form-container’);
const scheduleContainer = document.getElementById(‘schedule-container’);
const scheduleGrid = document.getElementById(‘schedule-grid’);
const resetBtn = document.getElementById(‘reset-btn’);
// — Gemini API Configuration —
const apiKey = “”; // Leave as-is, will be populated by the environment
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`;
// — System Prompt & JSON Schema —
// This system prompt instructs the AI on its role, the template, and the JSON output format.
const systemPrompt = `You are an expert curriculum designer for ‘Everyday Mandarin,’ creating engaging 45-60 minute after-school lesson plans for elementary students (TK-5). A teacher will provide ‘Content to Review’ and ‘Content to Teach’. Your task is to generate a detailed 6-part daily schedule based on this template:
1. **Part 1 (0-5 min): Set the Mood.** Activity: Mandarin Music/Video. Suggest a specific song or video type related to the ‘Content to Teach’.
2. **Part 2 (6-11 min): Review.** Activity: Review [Content to Review]. Suggest a fun, quick review game or activity suitable for TK-5.
3. **Part 3 (12-22 min): Instruction.** Activity: Instruction [Content to Teach]. Suggest a ‘Direct Instruction’ or ‘I Do, We Do’ method for introducing the new content.
4. **Part 4 (23-33 min): Practice and Reinforce.** Activity: Group Practice. Suggest a specific, interactive learning group activity or practice game related to the new content.
5. **Part 5 (34-5 min before end): Test and Practice.** Activity: Independent Practice/Quiz. Suggest a simple worksheet idea or a fun quiz game (like ‘Around the World’ or ‘Fly Swatter’) to check understanding.
6. **Part 6 (Last 5 min): Wrap Up.** Activity: Rewards and Clean Up. This part is standard: ‘Teacher awards 3-2-1 reward points, students pack folders, clean up, and wait for dismissal.’ Provide this as the suggestion.
You must return the schedule as a JSON object following the provided schema. Fill in the ‘title’, ‘time’, ‘activity’ (using the teacher’s input where specified), and ‘suggestion’ (your creative, age-appropriate idea) for all 6 parts.`;
// This schema forces the AI to return a JSON object in the exact structure we need.
const responseSchema = {
“type”: “OBJECT”,
“properties”: {
“part1”: {
“type”: “OBJECT”,
“properties”: {
“title”: { “type”: “STRING” },
“time”: { “type”: “STRING” },
“activity”: { “type”: “STRING” },
“suggestion”: { “type”: “STRING” }
}
},
“part2”: {
“type”: “OBJECT”,
“properties”: {
“title”: { “type”: “STRING” },
“time”: { “type”: “STRING” },
“activity”: { “type”: “STRING” },
“suggestion”: { “type”: “STRING” }
}
},
“part3”: {
“type”: “OBJECT”,
“properties”: {
“title”: { “type”: “STRING” },
“time”: { “type”: “STRING” },
“activity”: { “type”: “STRING” },
“suggestion”: { “type”: “STRING” }
}
},
“part4”: {
“type”: “OBJECT”,
“properties”: {
“title”: { “type”: “STRING” },
“time”: { “type”: “STRING” },
“activity”: { “type”: “STRING” },
“suggestion”: { “type”: “STRING” }
}
},
“part5”: {
“type”: “OBJECT”,
“properties”: {
“title”: { “type”: “STRING” },
“time”: { “type”: “STRING” },
“activity”: { “type”: “STRING” },
“suggestion”: { “type”: “STRING” }
}
},
“part6”: {
“type”: “OBJECT”,
“properties”: {
“title”: { “type”: “STRING” },
“time”: { “type”: “STRING” },
“activity”: { “type”: “STRING” },
“suggestion”: { “type”: “STRING” }
}
}
}
};
// — Event Listeners —
/**
* Handles the form submission
*/
form.addEventListener(‘submit’, async (e) => {
e.preventDefault();
const reviewText = reviewInput.value.trim();
const teachText = teachInput.value.trim();
if (!reviewText || !teachText) {
showError(“Please fill out both fields.”);
return;
}
setLoading(true);
try {
const scheduleData = await generateSchedule(reviewText, teachText);
if (scheduleData) {
displaySchedule(scheduleData);
// Toggle views
formContainer.classList.add(‘hidden’);
scheduleContainer.classList.remove(‘hidden’);
}
} catch (error) {
console.error(“Error generating schedule:”, error);
showError(“Could not generate schedule. Please try again.”);
} finally {
setLoading(false);
}
});
/**
* Handles the “Start Over” button click
*/
resetBtn.addEventListener(‘click’, () => {
scheduleContainer.classList.add(‘hidden’);
formContainer.classList.remove(‘hidden’);
reviewInput.value = ”;
teachInput.value = ”;
scheduleGrid.innerHTML = ”;
showError(”); // Clear any old errors
});
// — Core Functions —
/**
* Calls the Gemini API to generate the schedule
* @param {string} reviewContent – The content to review
* @param {string} teachContent – The content to teach
* @returns {object | null} – The parsed schedule object or null on error
*/
async function generateSchedule(reviewContent, teachContent) {
const userQuery = `Please generate a daily class schedule. Content to Review: “${reviewContent}”. Content to Teach: “${teachContent}”.`;
const payload = {
contents: [{ parts: [{ text: userQuery }] }],
systemInstruction: {
parts: [{ text: systemPrompt }]
},
generationConfig: {
responseMimeType: “application/json”,
responseSchema: responseSchema,
}
};
try {
const response = await fetchWithRetry(apiUrl, {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’ },
body: JSON.stringify(payload)
});
if (!response.ok) {
// Added more detailed logging for debugging the 401 error
console.error(“API Response Error:”, response.status, response.statusText);
await response.text().then(text => console.error(“API Error Body:”, text));
throw new Error(`API request failed with status ${response.status}`);
}
const result = await response.json();
const candidate = result.candidates?.[0];
if (candidate && candidate.content?.parts?.[0]?.text) {
const jsonText = candidate.content.parts[0].text;
return JSON.parse(jsonText);
} else {
console.error(“Invalid response structure:”, result);
throw new Error(“Invalid response from AI.”);
}
} catch (error) {
console.error(“Error in generateSchedule:”, error);
showError(“Failed to parse AI response. Please try again.”);
return null;
}
}
/**
* Displays the generated schedule on the page
* @param {object} scheduleData – The schedule object from the AI
*/
function displaySchedule(scheduleData) {
scheduleGrid.innerHTML = ”; // Clear previous results
// Loop through the 6 parts of the schedule
for (const [key, part] of Object.entries(scheduleData)) {
const card = document.createElement(‘div’);
card.className = “flex flex-col rounded-lg shadow-lg overflow-hidden bg-white border border-gray-200”;
// Determine card header color
let headerColor = “bg-gray-500”;
if (key === ‘part2’) headerColor = “bg-blue-600”;
if (key === ‘part3’) headerColor = “bg-green-600”;
if (key === ‘part4’ || key === ‘part5’) headerColor = “bg-yellow-500”;
if (key === ‘part6’) headerColor = “bg-red-500”;
card.innerHTML = `
Activity
${part.activity}
AI Suggestion
${part.suggestion}
`;
scheduleGrid.appendChild(card);
}
}
// — Utility Functions —
/**
* Sets the loading state of the generate button
* @param {boolean} isLoading – Whether to show the loading state
*/
function setLoading(isLoading) {
if (isLoading) {
generateBtn.disabled = true;
btnText.classList.add(‘hidden’);
loadingSpinner.classList.remove(‘hidden’);
showError(”); // Clear error on new request
} else {
generateBtn.disabled = false;
btnText.classList.remove(‘hidden’);
loadingSpinner.classList.add(‘hidden’);
}
}
/**
* Displays an error message to the user
* @param {string} message – The error message to display
*/
function showError(message) {
errorMessage.textContent = message;
}
/**
* Implements exponential backoff for API retries
*/
async function fetchWithRetry(url, options, retries = 3, delay = 1000) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);
// 429 is "Too Many Requests" – a retryable error
if (response.status === 429 && i setTimeout(resolve, delay));
delay *= 2; // Exponential backoff
continue;
}
return response; // Success or non-retryable error
} catch (error) {
if (i setTimeout(resolve, delay));
delay *= 2;
continue;
}
throw error; // Rethrow last error
}
}
}