319 lines
10 KiB
JavaScript
319 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,
|
|
generateColoringPage
|
|
} 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} letters`;
|
|
const numberRange = age <= 4 ? '1-5' : '1-9';
|
|
const numberCount = age <= 4 ? '3-4' : '4-5';
|
|
|
|
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 : 10})
|
|
|
|
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+
|
|
|
|
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 7.
|
|
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');
|
|
});
|