wondersheets/server/index.js

320 lines
10 KiB
JavaScript

import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { GoogleGenerativeAI } from '@google/generative-ai';
// Import programmatic generators
import {
generateWritingWorksheet,
generateCountingWorksheet,
generateMazeWorksheet
} from './generators/index.js';
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const port = 3001;
app.use(cors());
app.use(express.json());
// Initialize Gemini API
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');
// ============================================
// GALLERY STORAGE - Recent 6 generations
// ============================================
const GALLERY_FILE = path.join(__dirname, 'gallery.json');
const MAX_GALLERY_SIZE = 6;
function loadGallery() {
try {
if (fs.existsSync(GALLERY_FILE)) {
const data = fs.readFileSync(GALLERY_FILE, 'utf8');
return JSON.parse(data);
}
} catch (error) {
console.error('Error loading gallery:', error);
}
return [];
}
function saveGallery(gallery) {
try {
fs.writeFileSync(GALLERY_FILE, JSON.stringify(gallery, null, 2));
} catch (error) {
console.error('Error saving gallery:', error);
}
}
function addToGallery(item) {
const gallery = loadGallery();
gallery.unshift({
...item,
id: Date.now(),
createdAt: new Date().toISOString()
});
while (gallery.length > MAX_GALLERY_SIZE) {
gallery.pop();
}
saveGallery(gallery);
return gallery;
}
// ============================================
// LLM PROMPTS - Now just returns parameters
// ============================================
const parameterPrompts = {
writing: (theme, age) => {
const randomSeed = Math.floor(Math.random() * 1000);
const wordLength = age <= 4 ? '3-4 letters' : age <= 6 ? '4-5 letters' : age <= 8 ? '5-6 letters' : '6-8 letters';
const numberRange = age <= 4 ? '1-5' : age <= 6 ? '1-9' : age <= 8 ? '1-15' : '1-20';
const numberCount = age <= 4 ? '3-4' : age <= 6 ? '4-5' : '5-6';
return `
Create a children's tracing worksheet for theme "${theme}".
Age: ${age} years old
Random seed: ${randomSeed}
Return ONLY a JSON object with ONE of these options:
Option A - Trace a word: {"word": "CAT", "isNumbers": false}
Option B - Trace numbers: {"numbers": [1,2,3,4,5], "isNumbers": true}
Rules:
- For words: Pick a simple ${wordLength} word related to "${theme}" (UPPERCASE). Use seed ${randomSeed} to vary your choice.
- For numbers: Pick ${numberCount} numbers from ${numberRange}
- Randomly choose between words (50%) or numbers (50%)
Return ONLY valid JSON, no explanation.
`;
},
counting: (theme, age) => {
const randomSeed = Math.floor(Math.random() * 1000);
return `
Create a children's counting worksheet for theme "${theme}".
Random seed: ${randomSeed}
AVAILABLE SHAPES: star, heart, circle, apple, fish, flower, butterfly, ball, moon, cloud
CRITICAL: You MUST return JSON with BOTH "shape" AND "quantities":
{"shape": "star", "quantities": [3, 1, 5, 2, 7, 4, 8, 6]}
- "shape": Choose ONE shape that matches "${theme}". Use the random seed ${randomSeed} to vary your choice - don't always pick the same shape for this theme.
- "quantities": Array of exactly 8 different numbers (1-${age <= 4 ? 5 : age <= 6 ? 10 : age <= 8 ? 15 : 20})
Return ONLY valid JSON. No explanation.
`;
},
'find-way': (theme, age) => `
You are helping create a children's maze worksheet.
Theme: ${theme}
Age: ${age} years old
Return ONLY a JSON object:
{ "difficulty": 1 }
Rules:
- difficulty 1 = easy (4x4 grid) for age 3-4
- difficulty 2 = medium (5x5 grid) for age 5-6
- difficulty 3 = hard (6x6 grid) for age 7-8
- difficulty 4 = challenging (7x7 grid) for age 9
- difficulty 5 = expert (8x8 grid) for age 10
Return ONLY valid JSON, no explanation.
`,
coloring: (theme, age) => {
const randomSeed = Math.floor(Math.random() * 1000);
const simpleIllustrations = 'star, sun, flower, ball (circle)';
const complexIllustrations = 'unicorn, butterfly, rocket, house, cat, dog, fish';
const ageGuidance = age <= 4
? `Prefer SIMPLER illustrations (${simpleIllustrations}) for young children.`
: `You can choose from ALL illustrations including complex ones (${complexIllustrations}).`;
return `
You are helping create a children's coloring page.
Theme: ${theme}
Age: ${age} years old
Random seed: ${randomSeed}
Return ONLY a JSON object:
{ "illustration": "cat" }
Available illustrations: cat, dog, butterfly, fish, flower, star, rocket, sun, house, unicorn
${ageGuidance}
Pick the ONE illustration that best matches the theme. Use seed ${randomSeed} to vary your choice.
Return ONLY valid JSON, no explanation.
`;
}
};
// ============================================
// API ENDPOINTS
// ============================================
app.get('/api/gallery', (req, res) => {
try {
const gallery = loadGallery();
res.json(gallery);
} catch (error) {
console.error('Gallery fetch error:', error);
res.status(500).json({ error: 'Failed to fetch gallery' });
}
});
app.post('/api/surprise', async (req, res) => {
try {
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
// Add timestamp to force different responses
const timestamp = Date.now();
// Always randomize theme, age, AND activity type
const prompt = `
Current timestamp: ${timestamp}
Generate ONE creative, fun, specific theme for a children's worksheet (e.g. "Space-Rex Coding", "Underwater Hamster Tea Party").
Pick a RANDOM age between 3 and 10.
Pick a RANDOM activity type with EQUAL PROBABILITY from these 4 options:
1. "coloring" - coloring pages
2. "writing" - tracing letters or numbers
3. "find-way" - maze puzzles
4. "counting" - counting exercises
IMPORTANT: Select EXACTLY one of these values for "section": coloring, writing, find-way, counting
ALL FOUR OPTIONS MUST HAVE EQUAL 25% CHANCE. Do not favor any particular activity type.
USE THE TIMESTAMP ${timestamp} as a random seed to ensure variation.
DO NOT always pick the same age or activity - vary your choices each time.
Return JSON ONLY:
{
"theme": "string",
"age": number,
"section": "string"
}
`;
const result = await model.generateContent(prompt);
const text = result.response.text();
console.log('Surprise LLM response:', text);
const jsonStr = text.replace(/```json/g, '').replace(/```/g, '').trim();
const data = JSON.parse(jsonStr);
console.log('Surprise parsed data:', data);
// Ensure sections is an array
res.json({
theme: data.theme,
age: data.age,
sections: [data.section]
});
} catch (error) {
console.error('Surprise error:', error);
res.status(500).json({ error: 'Failed' });
}
});
app.post('/api/generate', async (req, res) => {
try {
const { theme, age, sections } = req.body;
const activityType = sections[0] || 'writing';
console.log('Generating:', { theme, age, activityType });
if (!process.env.GEMINI_API_KEY) {
return res.status(500).json({ error: 'Missing API Key' });
}
// Step 1: Get parameters from LLM (fast, small response)
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const paramPrompt = parameterPrompts[activityType] || parameterPrompts.writing;
console.log('Getting parameters from LLM...');
const result = await model.generateContent(paramPrompt(theme, age));
const text = result.response.text();
const jsonStr = text.replace(/```json/g, '').replace(/```/g, '').trim();
const params = JSON.parse(jsonStr);
console.log('LLM returned params:', params);
// Step 2: Generate worksheet using programmatic generator
let worksheet;
switch (activityType) {
case 'writing':
worksheet = generateWritingWorksheet({
word: params.word || 'CAT',
numbers: params.numbers || [1, 2, 3, 4, 5],
isNumbers: params.isNumbers || false,
age
});
break;
case 'counting':
worksheet = generateCountingWorksheet({
theme,
shape: params.shape,
quantities: params.quantities || [2, 4, 5, 7, 3, 6, 8, 1],
age
});
break;
case 'find-way':
worksheet = generateMazeWorksheet({
theme,
difficulty: params.difficulty || 1,
age
});
break;
case 'coloring':
// Coloring uses direct LLM SVG generation, not programmatic
console.log('Using LLM for full coloring SVG generation...');
const { getPromptForActivity: getPrompt } = await import('./prompts.js');
const coloringPrompt = getPrompt('coloring', theme, age);
const coloringResult = await model.generateContent(coloringPrompt);
const coloringText = coloringResult.response.text();
const coloringJsonStr = coloringText.replace(/```json/g, '').replace(/```/g, '').trim();
worksheet = JSON.parse(coloringJsonStr);
break;
default:
worksheet = generateWritingWorksheet({
word: 'FUN',
isNumbers: false,
age
});
}
// Add metadata
worksheet.age = age;
worksheet.activityType = activityType;
// Save to gallery
addToGallery(worksheet);
console.log('Generated worksheet:', worksheet.title);
res.json(worksheet);
} catch (error) {
console.error('Generation error:', error);
res.status(500).json({ error: 'Failed to generate content' });
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
console.log('Using hybrid generation: LLM for parameters, code for SVGs');
});