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'); });