diff --git a/server/gallery.json b/server/gallery.json
index 4108b5b7..16a7d3c1 100644
--- a/server/gallery.json
+++ b/server/gallery.json
@@ -1,77 +1,92 @@
[
{
- "title": "Rocket to the Planet",
- "subtitle": "Help the rocket find its way through the asteroids to the planet!",
- "sections": [
- {
- "id": "find-the-way-main",
- "type": "find-the-way",
- "content_svg": ""
- }
- ],
- "age": 7,
- "activityType": "find-way",
- "id": 1770334258054,
- "createdAt": "2026-02-05T23:30:58.054Z"
- },
- {
- "title": "Unicorn Adventure Maze",
- "subtitle": "Help the little unicorn find the magical star!",
- "sections": [
- {
- "id": "find-the-way-main",
- "type": "find-the-way",
- "content_svg": ""
- }
- ],
- "age": 5,
- "activityType": "find-way",
- "id": 1770334088798,
- "createdAt": "2026-02-05T23:28:08.798Z"
- },
- {
- "title": "Space Number Blast Off!",
- "subtitle": "Color the number and see how many space objects match!",
- "sections": [
- {
- "id": "numbers-main",
- "type": "numbers",
- "content_svg": ""
- }
- ],
- "age": 5,
- "activityType": "numbers",
- "id": 1770333755198,
- "createdAt": "2026-02-05T23:22:35.198Z"
- },
- {
- "title": "Space Word Writing",
- "subtitle": "Trace the letters to write the word SUN.",
- "sections": [
- {
- "id": "writing-main",
- "type": "writing",
- "content_svg": ""
- }
- ],
- "age": 5,
- "activityType": "writing",
- "id": 1770333626839,
- "createdAt": "2026-02-05T23:20:26.839Z"
- },
- {
- "title": "Octopus's Garden",
- "subtitle": "Color this friendly octopus and its underwater friends!",
+ "title": "Pirate Parrot's Treasure!",
+ "subtitle": "Color the parrot and his hidden treasure!",
"sections": [
{
"id": "coloring-main",
"type": "coloring",
- "content_svg": ""
+ "content_svg": ""
}
],
"age": 5,
"activityType": "coloring",
- "id": 1770333549787,
- "createdAt": "2026-02-05T23:19:09.787Z"
+ "id": 1770458040593,
+ "createdAt": "2026-02-07T09:54:00.593Z"
+ },
+ {
+ "title": "Parrot's Treasure!",
+ "subtitle": "Color the parrot and his treasure hunt!",
+ "sections": [
+ {
+ "id": "coloring-main",
+ "type": "coloring",
+ "content_svg": ""
+ }
+ ],
+ "age": 5,
+ "activityType": "coloring",
+ "id": 1770458028752,
+ "createdAt": "2026-02-07T09:53:48.752Z"
+ },
+ {
+ "title": "Pirate Parrot's Treasure!",
+ "subtitle": "Color the parrot and his treasure hunt!",
+ "sections": [
+ {
+ "id": "coloring-main",
+ "type": "coloring",
+ "content_svg": ""
+ }
+ ],
+ "age": 5,
+ "activityType": "coloring",
+ "id": 1770457963325,
+ "createdAt": "2026-02-07T09:52:43.325Z"
+ },
+ {
+ "title": "Count the Stars!",
+ "subtitle": "Count each group and write the number",
+ "sections": [
+ {
+ "id": "counting-main",
+ "type": "counting",
+ "content_svg": ""
+ }
+ ],
+ "age": 5,
+ "activityType": "counting",
+ "id": 1770457944132,
+ "createdAt": "2026-02-07T09:52:24.132Z"
+ },
+ {
+ "title": "Count the Fishs!",
+ "subtitle": "Count each group and write the number",
+ "sections": [
+ {
+ "id": "counting-main",
+ "type": "counting",
+ "content_svg": ""
+ }
+ ],
+ "age": 5,
+ "activityType": "counting",
+ "id": 1770457941396,
+ "createdAt": "2026-02-07T09:52:21.396Z"
+ },
+ {
+ "title": "Pirate Unicorn Treasure Hunt Maze Adventure",
+ "subtitle": "Find the path from start to finish!",
+ "sections": [
+ {
+ "id": "find-the-way-main",
+ "type": "find-the-way",
+ "content_svg": ""
+ }
+ ],
+ "age": 5,
+ "activityType": "find-way",
+ "id": 1770457938911,
+ "createdAt": "2026-02-07T09:52:18.911Z"
}
]
\ No newline at end of file
diff --git a/server/generators/coloring.js b/server/generators/coloring.js
new file mode 100644
index 00000000..f09c0729
--- /dev/null
+++ b/server/generators/coloring.js
@@ -0,0 +1,216 @@
+/**
+ * Coloring Page Generator
+ * Creates themed coloring pages with pre-defined SVG shapes
+ */
+
+// Library of coloring illustrations (outlines only)
+const illustrations = {
+ // Animals
+ cat: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ dog: `
+
+
+
+
+
+
+
+
+
+ `,
+
+ butterfly: `
+
+
+
+
+
+
+
+
+
+ `,
+
+ fish: `
+
+
+
+
+
+
+
+ `,
+
+ flower: `
+
+
+
+
+
+
+
+
+
+ `,
+
+ star: `
+
+
+
+
+ `,
+
+ rocket: `
+
+
+
+
+
+
+ `,
+
+ sun: `
+
+
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ house: `
+
+
+
+
+
+
+
+
+
+
+ `,
+
+ unicorn: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `
+};
+
+const illustrationNames = Object.keys(illustrations);
+
+/**
+ * Pick an illustration based on theme keywords with randomization
+ */
+function pickIllustration(theme, llmSuggestion, age) {
+ // Try LLM suggestion first if valid
+ if (llmSuggestion && illustrations[llmSuggestion]) {
+ return llmSuggestion;
+ }
+
+ if (!theme) {
+ return illustrationNames[Math.floor(Math.random() * illustrationNames.length)];
+ }
+
+ const themeLower = theme.toLowerCase();
+
+ // Direct matches
+ for (const name of illustrationNames) {
+ if (themeLower.includes(name)) {
+ return name;
+ }
+ }
+
+ // Keyword associations
+ const associations = {
+ cat: ['kitten', 'kitty', 'meow', 'feline', 'pet', 'animal'],
+ dog: ['puppy', 'doggy', 'woof', 'canine', 'pet', 'animal'],
+ butterfly: ['insect', 'bug', 'garden', 'spring', 'animal'],
+ fish: ['ocean', 'sea', 'water', 'aquarium', 'swim', 'animal'],
+ flower: ['garden', 'plant', 'nature', 'spring', 'rose'],
+ star: ['night', 'sky', 'space', 'twinkle', 'magic'],
+ rocket: ['space', 'astronaut', 'moon', 'planet', 'fly'],
+ sun: ['summer', 'sunny', 'bright', 'happy', 'day'],
+ house: ['home', 'building', 'family', 'cozy'],
+ unicorn: ['magic', 'fairy', 'princess', 'rainbow', 'fantasy', 'horse', 'animal']
+ };
+
+ // Collect ALL matching illustrations
+ const matchingIllustrations = [];
+ for (const [illustration, keywords] of Object.entries(associations)) {
+ if (keywords.some(keyword => themeLower.includes(keyword))) {
+ matchingIllustrations.push(illustration);
+ }
+ }
+
+ // If we found matches, pick one randomly for variety
+ if (matchingIllustrations.length > 0) {
+ return matchingIllustrations[Math.floor(Math.random() * matchingIllustrations.length)];
+ }
+
+ // Random fallback
+ return illustrationNames[Math.floor(Math.random() * illustrationNames.length)];
+}
+
+/**
+ * Generate complete coloring page
+ * @param {Object} params
+ * @param {string} params.theme - Theme for the coloring page
+ * @param {string} params.illustration - LLM-suggested illustration (optional)
+ * @param {number} params.age - Child's age (optional)
+ */
+export function generateColoringPage({ theme, illustration, age }) {
+ const selectedIllustration = pickIllustration(theme, illustration, age);
+ const illustrationSvg = illustrations[selectedIllustration] || illustrations.cat;
+
+ const displayName = selectedIllustration.charAt(0).toUpperCase() + selectedIllustration.slice(1);
+
+ const svg = ``;
+
+ return {
+ title: `${displayName} Coloring Page`,
+ subtitle: theme ? `${theme} themed coloring fun!` : 'Color and have fun!',
+ sections: [{
+ id: 'coloring-main',
+ type: 'coloring',
+ content_svg: svg
+ }]
+ };
+}
+
+export default { generateColoringPage };
diff --git a/server/generators/counting.js b/server/generators/counting.js
new file mode 100644
index 00000000..ab5d7bab
--- /dev/null
+++ b/server/generators/counting.js
@@ -0,0 +1,204 @@
+/**
+ * Counting Worksheet Generator
+ * Generates randomized counting exercises with themed objects
+ */
+
+// Simple themed object SVG shapes
+const themedObjects = {
+ star: ``,
+ heart: ``,
+ circle: ``,
+ apple: ``,
+ fish: ``,
+ flower: ``,
+ butterfly: ``,
+ ball: ``,
+ moon: ``,
+ cloud: ``
+};
+
+const objectNames = Object.keys(themedObjects);
+
+/**
+ * Generate random quantities that are all different
+ */
+function generateRandomQuantities(count, maxValue) {
+ const quantities = [];
+ const used = new Set();
+
+ while (quantities.length < count) {
+ const num = Math.floor(Math.random() * maxValue) + 1;
+ if (!used.has(num)) {
+ used.add(num);
+ quantities.push(num);
+ }
+ }
+
+ // Shuffle the array
+ return quantities.sort(() => Math.random() - 0.5);
+}
+
+/**
+ * Pick object type based on theme keywords with randomization
+ */
+function pickObjectForTheme(theme, llmSuggestion) {
+ // Try to use LLM suggestion first if valid
+ if (llmSuggestion && themedObjects[llmSuggestion]) {
+ return llmSuggestion;
+ }
+
+ if (!theme) {
+ return objectNames[Math.floor(Math.random() * objectNames.length)];
+ }
+
+ const themeLower = theme.toLowerCase();
+
+ // Direct matches
+ for (const name of objectNames) {
+ if (themeLower.includes(name)) {
+ return name;
+ }
+ }
+
+ // Build list of ALL matching shapes, then pick randomly
+ const associations = {
+ star: ['space', 'night', 'sky', 'twinkle', 'magic', 'galaxy', 'astronaut', 'king', 'crown', 'royal'],
+ heart: ['love', 'valentine', 'romance', 'caring', 'friendship', 'kind', 'help'],
+ circle: ['round', 'bubble', 'dot'],
+ apple: ['fruit', 'food', 'tree', 'garden', 'healthy', 'snack', 'farm', 'autumn', 'fall'],
+ fish: ['ocean', 'sea', 'water', 'aquarium', 'swim', 'underwater', 'beach', 'marine', 'animal'],
+ flower: ['garden', 'plant', 'nature', 'spring', 'rose', 'bloom', 'petal', 'beautiful'],
+ butterfly: ['insect', 'bug', 'garden', 'spring', 'colorful', 'wings', 'animal', 'fly'],
+ ball: ['sport', 'play', 'game', 'soccer', 'football', 'basketball', 'tennis', 'cricket', 'kick'],
+ moon: ['night', 'sleep', 'bedtime', 'dream', 'space', 'lunar', 'dark'],
+ cloud: ['sky', 'weather', 'rain', 'sunny', 'day', 'fluffy', 'soft']
+ };
+
+ // Collect ALL matching shapes
+ const matchingShapes = [];
+ for (const [objectType, keywords] of Object.entries(associations)) {
+ if (keywords.some(keyword => themeLower.includes(keyword))) {
+ matchingShapes.push(objectType);
+ }
+ }
+
+ // If we found matches, pick one randomly for variety
+ if (matchingShapes.length > 0) {
+ return matchingShapes[Math.floor(Math.random() * matchingShapes.length)];
+ }
+
+ // Random fallback
+ return objectNames[Math.floor(Math.random() * objectNames.length)];
+}
+
+/**
+ * Generate a group of objects at a position
+ */
+function generateObjectGroup(objectType, count, x, y, groupWidth) {
+ const objectSvg = themedObjects[objectType] || themedObjects.star;
+ const objectSize = 22;
+ const maxPerRow = 5;
+
+ let svg = '';
+
+ if (count <= maxPerRow) {
+ // Single row
+ const spacing = Math.min(28, (groupWidth - count * objectSize) / (count + 1));
+ let startX = x + spacing;
+
+ for (let i = 0; i < count; i++) {
+ svg += `${objectSvg}`;
+ }
+ } else {
+ // Two rows for larger counts
+ const topRowCount = Math.ceil(count / 2);
+ const bottomRowCount = count - topRowCount;
+ const spacing = Math.min(26, (groupWidth - topRowCount * objectSize) / (topRowCount + 1));
+
+ // Top row
+ let startX = x + spacing;
+ for (let i = 0; i < topRowCount; i++) {
+ svg += `${objectSvg}`;
+ }
+
+ // Bottom row
+ startX = x + spacing + (topRowCount - bottomRowCount) * (objectSize + spacing) / 2;
+ for (let i = 0; i < bottomRowCount; i++) {
+ svg += `${objectSvg}`;
+ }
+ }
+
+ return svg;
+}
+
+/**
+ * Generate an empty answer box
+ */
+function generateAnswerBox(x, y, size = 40) {
+ return ``;
+}
+
+/**
+ * Generate complete counting worksheet
+ * @param {Object} params
+ * @param {string} params.theme - Theme name (optional)
+ * @param {string} params.shape - Shape chosen by LLM (e.g. 'fish', 'star')
+ * @param {number[]} params.quantities - Array of quantities from LLM
+ * @param {number} params.age - Child's age
+ */
+export function generateCountingWorksheet({ theme, shape, quantities, age }) {
+ // Use hybrid approach: LLM suggestion + programmatic randomization from matching shapes
+ const objectType = pickObjectForTheme(theme, shape);
+
+ // ALWAYS generate random quantities for variety (LLM is deterministic)
+ const maxValue = age <= 4 ? 5 : 10;
+ const rowCount = 8;
+ const finalQuantities = generateRandomQuantities(rowCount, maxValue);
+
+ const rowHeight = 62; // 500 / 8 = 62.5, fill entire height
+ const startY = 0; // Start at absolute top
+ const objectAreaWidth = 320;
+ const answerBoxX = 440;
+ const answerBoxSize = 42;
+
+ let objectRows = '';
+
+ for (let i = 0; i < rowCount; i++) {
+ const rowY = startY + i * rowHeight;
+ const quantity = finalQuantities[i];
+
+ // Dotted separator line between rows (except first row)
+ if (i > 0) {
+ objectRows += ``;
+ }
+
+ // Row number indicator - vertically centered in row
+ objectRows += `${i + 1}.`;
+
+ // Objects to count - positioned in middle of row
+ objectRows += generateObjectGroup(objectType, quantity, 50, rowY + 20, objectAreaWidth);
+
+ // Equals sign
+ objectRows += `=`;
+
+ // Answer box
+ objectRows += generateAnswerBox(answerBoxX, rowY + 10, answerBoxSize);
+ }
+
+ const svg = ``;
+
+ return {
+ title: `Count the ${objectType.charAt(0).toUpperCase() + objectType.slice(1)}s!`,
+ subtitle: 'Count each group and write the number',
+ sections: [{
+ id: 'counting-main',
+ type: 'counting',
+ content_svg: svg
+ }]
+ };
+}
+
+export default { generateCountingWorksheet };
diff --git a/server/generators/index.js b/server/generators/index.js
new file mode 100644
index 00000000..25402d7d
--- /dev/null
+++ b/server/generators/index.js
@@ -0,0 +1,9 @@
+/**
+ * Worksheet Generators Index
+ * Exports all programmatic SVG generators
+ */
+
+export { generateWritingWorksheet } from './writing.js';
+export { generateCountingWorksheet } from './counting.js';
+export { generateMazeWorksheet } from './maze.js';
+export { generateColoringPage } from './coloring.js';
diff --git a/server/generators/maze.js b/server/generators/maze.js
new file mode 100644
index 00000000..1da3ffa7
--- /dev/null
+++ b/server/generators/maze.js
@@ -0,0 +1,241 @@
+/**
+ * Maze Generator
+ * Creates algorithmically generated, guaranteed-solvable mazes
+ */
+
+/**
+ * Generate a maze using recursive backtracking algorithm
+ * Returns a 2D grid where true = wall, false = path
+ */
+function generateMazeGrid(rows, cols, startEdge, endEdge) {
+ // Initialize grid with all walls
+ const grid = Array(rows * 2 + 1).fill(null).map(() =>
+ Array(cols * 2 + 1).fill(true)
+ );
+
+ // Recursive backtracking to carve paths
+ const visited = Array(rows).fill(null).map(() => Array(cols).fill(false));
+
+ function carve(row, col) {
+ visited[row][col] = true;
+ grid[row * 2 + 1][col * 2 + 1] = false; // Carve cell
+
+ // Get neighbors in random order
+ const directions = [
+ [-1, 0], [1, 0], [0, -1], [0, 1]
+ ].sort(() => Math.random() - 0.5);
+
+ for (const [dr, dc] of directions) {
+ const newRow = row + dr;
+ const newCol = col + dc;
+
+ if (newRow >= 0 && newRow < rows && newCol >= 0 && newCol < cols && !visited[newRow][newCol]) {
+ // Carve wall between cells
+ grid[row * 2 + 1 + dr][col * 2 + 1 + dc] = false;
+ carve(newRow, newCol);
+ }
+ }
+ }
+
+ // Determine starting cell for carving based on entrance position
+ let startRow = 0, startCol = 0;
+ switch (startEdge) {
+ case 'top':
+ startRow = 0; startCol = 0;
+ break;
+ case 'bottom':
+ startRow = rows - 1; startCol = rows - 1;
+ break;
+ case 'left':
+ startRow = 0; startCol = 0;
+ break;
+ case 'right':
+ startRow = 0; startCol = cols - 1;
+ break;
+ }
+
+ // Start carving from position near entrance
+ carve(startRow, startCol);
+
+ // Track entrance and exit positions for marker placement
+ let entranceGridPos = { row: 0, col: 0 };
+ let exitGridPos = { row: 0, col: 0 };
+
+ // Create entrance based on startEdge - opening in the outer wall
+ // Place in first third of edge for variety
+ switch (startEdge) {
+ case 'top':
+ const topEntranceCol = 1 + Math.floor(Math.random() * Math.max(1, cols / 3)) * 2;
+ grid[0][topEntranceCol] = false;
+ entranceGridPos = { row: 0, col: topEntranceCol };
+ break;
+ case 'bottom':
+ const bottomEntranceCol = 1 + Math.floor(Math.random() * Math.max(1, cols / 3)) * 2;
+ grid[rows * 2][bottomEntranceCol] = false;
+ entranceGridPos = { row: rows * 2, col: bottomEntranceCol };
+ break;
+ case 'left':
+ const leftEntranceRow = 1 + Math.floor(Math.random() * Math.max(1, rows / 3)) * 2;
+ grid[leftEntranceRow][0] = false;
+ entranceGridPos = { row: leftEntranceRow, col: 0 };
+ break;
+ case 'right':
+ const rightEntranceRow = 1 + Math.floor(Math.random() * Math.max(1, rows / 3)) * 2;
+ grid[rightEntranceRow][cols * 2] = false;
+ entranceGridPos = { row: rightEntranceRow, col: cols * 2 };
+ break;
+ }
+
+ // Create exit based on endEdge - opening in the outer wall
+ // Place in last third of edge to be far from entrance
+ switch (endEdge) {
+ case 'top':
+ const topExitCol = cols * 2 - 1 - Math.floor(Math.random() * Math.max(1, cols / 3)) * 2;
+ grid[0][topExitCol] = false;
+ exitGridPos = { row: 0, col: topExitCol };
+ break;
+ case 'bottom':
+ const bottomExitCol = cols * 2 - 1 - Math.floor(Math.random() * Math.max(1, cols / 3)) * 2;
+ grid[rows * 2][bottomExitCol] = false;
+ exitGridPos = { row: rows * 2, col: bottomExitCol };
+ break;
+ case 'left':
+ const leftExitRow = rows * 2 - 1 - Math.floor(Math.random() * Math.max(1, rows / 3)) * 2;
+ grid[leftExitRow][0] = false;
+ exitGridPos = { row: leftExitRow, col: 0 };
+ break;
+ case 'right':
+ const rightExitRow = rows * 2 - 1 - Math.floor(Math.random() * Math.max(1, rows / 3)) * 2;
+ grid[rightExitRow][cols * 2] = false;
+ exitGridPos = { row: rightExitRow, col: cols * 2 };
+ break;
+ }
+
+ return { grid, entranceGridPos, exitGridPos };
+}
+
+/**
+ * Convert maze grid to SVG
+ */
+function mazeGridToSVG(grid, startX, startY, cellSize) {
+ let svg = '';
+ const wallColor = '#333333';
+
+ for (let row = 0; row < grid.length; row++) {
+ for (let col = 0; col < grid[row].length; col++) {
+ if (grid[row][col]) {
+ const x = startX + col * cellSize;
+ const y = startY + row * cellSize;
+ svg += ``;
+ }
+ }
+ }
+
+ return svg;
+}
+
+/**
+ * Generate START and GOAL markers
+ */
+function generateMarkers(startX, startY, endX, endY, theme, cellSize) {
+ // Smaller icon size to fit in path cells
+ const iconSize = Math.min(cellSize * 0.6, 20);
+ const radius = iconSize / 2;
+
+ return `
+
+
+ START
+
+
+
+ GOAL
+ `;
+}
+
+/**
+ * Generate complete maze worksheet
+ * @param {Object} params
+ * @param {string} params.theme - Theme for decorations
+ * @param {number} params.difficulty - 1-3 (affects grid size)
+ * @param {number} params.age - Child's age
+ */
+export function generateMazeWorksheet({ theme, difficulty = 1, age }) {
+ // Grid size based on difficulty and age
+ let rows, cols;
+ if (age <= 4 || difficulty === 1) {
+ rows = 4;
+ cols = 4;
+ } else if (age <= 5 || difficulty === 2) {
+ rows = 5;
+ cols = 5;
+ } else {
+ rows = 6;
+ cols = 6;
+ }
+
+ // Randomly select start and end edges - favor opposite edges for better gameplay
+ const edges = ['top', 'bottom', 'left', 'right'];
+ const startEdge = edges[Math.floor(Math.random() * edges.length)];
+
+ // 80% chance to pick opposite edge, 20% chance for any different edge
+ let endEdge;
+ const oppositeEdges = {
+ 'top': 'bottom',
+ 'bottom': 'top',
+ 'left': 'right',
+ 'right': 'left'
+ };
+
+ if (Math.random() < 0.8) {
+ // Pick opposite edge
+ endEdge = oppositeEdges[startEdge];
+ } else {
+ // Pick any different edge
+ const remainingEdges = edges.filter(e => e !== startEdge);
+ endEdge = remainingEdges[Math.floor(Math.random() * remainingEdges.length)];
+ }
+
+ console.log(`Maze edges: START=${startEdge}, END=${endEdge}`);
+
+ const { grid, entranceGridPos, exitGridPos } = generateMazeGrid(rows, cols, startEdge, endEdge);
+
+ // Calculate positioning to fill the space
+ const gridWidth = cols * 2 + 1;
+ const gridHeight = rows * 2 + 1;
+ const cellSize = Math.min(480 / gridWidth, 480 / gridHeight);
+ const mazeWidth = gridWidth * cellSize;
+ const mazeHeight = gridHeight * cellSize;
+ const startX = (500 - mazeWidth) / 2;
+ const startY = (500 - mazeHeight) / 2;
+
+ // Calculate marker positions based on actual entrance/exit grid positions
+ // Add half cellSize to center in the path cell (not on the edge)
+ const mazeStartX = startX + entranceGridPos.col * cellSize + cellSize / 2;
+ const mazeStartY = startY + entranceGridPos.row * cellSize + cellSize / 2;
+ const mazeEndX = startX + exitGridPos.col * cellSize + cellSize / 2;
+ const mazeEndY = startY + exitGridPos.row * cellSize + cellSize / 2;
+
+ const svg = ``;
+
+ return {
+ title: theme ? `${theme} Maze Adventure` : 'Maze Adventure',
+ subtitle: 'Find the path from start to finish!',
+ sections: [{
+ id: 'find-the-way-main',
+ type: 'find-the-way',
+ content_svg: svg
+ }]
+ };
+}
+
+export default { generateMazeWorksheet };
diff --git a/server/generators/writing.js b/server/generators/writing.js
new file mode 100644
index 00000000..36251a5c
--- /dev/null
+++ b/server/generators/writing.js
@@ -0,0 +1,142 @@
+/**
+ * Writing/Tracing Worksheet Generator
+ * Generates consistent dotted letters and numbers for tracing practice
+ */
+
+// Letter path definitions (school-style uppercase)
+const letterPaths = {
+ 'A': 'M 0 80 L 25 0 L 50 80 M 12 50 L 38 50',
+ 'B': 'M 0 0 L 0 80 L 35 80 Q 50 80 50 60 Q 50 40 35 40 L 0 40 M 0 0 L 35 0 Q 50 0 50 20 Q 50 40 35 40',
+ 'C': 'M 50 15 Q 50 0 25 0 Q 0 0 0 40 Q 0 80 25 80 Q 50 80 50 65',
+ 'D': 'M 0 0 L 0 80 L 25 80 Q 50 80 50 40 Q 50 0 25 0 L 0 0',
+ 'E': 'M 50 0 L 0 0 L 0 80 L 50 80 M 0 40 L 35 40',
+ 'F': 'M 50 0 L 0 0 L 0 80 M 0 40 L 35 40',
+ 'G': 'M 50 15 Q 50 0 25 0 Q 0 0 0 40 Q 0 80 25 80 Q 50 80 50 50 L 30 50',
+ 'H': 'M 0 0 L 0 80 M 50 0 L 50 80 M 0 40 L 50 40',
+ 'I': 'M 10 0 L 40 0 M 25 0 L 25 80 M 10 80 L 40 80',
+ 'J': 'M 10 0 L 40 0 M 30 0 L 30 60 Q 30 80 15 80 Q 0 80 0 65',
+ 'K': 'M 0 0 L 0 80 M 50 0 L 0 45 L 50 80',
+ 'L': 'M 0 0 L 0 80 L 50 80',
+ 'M': 'M 0 80 L 0 0 L 25 40 L 50 0 L 50 80',
+ 'N': 'M 0 80 L 0 0 L 50 80 L 50 0',
+ 'O': 'M 25 0 Q 0 0 0 40 Q 0 80 25 80 Q 50 80 50 40 Q 50 0 25 0',
+ 'P': 'M 0 80 L 0 0 L 35 0 Q 50 0 50 20 Q 50 40 35 40 L 0 40',
+ 'Q': 'M 25 0 Q 0 0 0 40 Q 0 80 25 80 Q 50 80 50 40 Q 50 0 25 0 M 35 60 L 55 85',
+ 'R': 'M 0 80 L 0 0 L 35 0 Q 50 0 50 20 Q 50 40 35 40 L 0 40 M 30 40 L 50 80',
+ 'S': 'M 50 15 Q 50 0 25 0 Q 0 0 0 20 Q 0 40 25 40 Q 50 40 50 60 Q 50 80 25 80 Q 0 80 0 65',
+ 'T': 'M 0 0 L 50 0 M 25 0 L 25 80',
+ 'U': 'M 0 0 L 0 60 Q 0 80 25 80 Q 50 80 50 60 L 50 0',
+ 'V': 'M 0 0 L 25 80 L 50 0',
+ 'W': 'M 0 0 L 12 80 L 25 30 L 38 80 L 50 0',
+ 'X': 'M 0 0 L 50 80 M 50 0 L 0 80',
+ 'Y': 'M 0 0 L 25 40 L 50 0 M 25 40 L 25 80',
+ 'Z': 'M 0 0 L 50 0 L 0 80 L 50 80'
+};
+
+// Number path definitions
+const numberPaths = {
+ '1': 'M 15 15 L 25 0 L 25 80 M 10 80 L 40 80',
+ '2': 'M 0 20 Q 0 0 25 0 Q 50 0 50 20 Q 50 40 0 80 L 50 80',
+ '3': 'M 0 10 Q 0 0 25 0 Q 50 0 50 20 Q 50 40 25 40 Q 50 40 50 60 Q 50 80 25 80 Q 0 80 0 70',
+ '4': 'M 40 80 L 40 0 L 0 55 L 50 55',
+ '5': 'M 50 0 L 0 0 L 0 35 Q 0 35 25 35 Q 50 35 50 57 Q 50 80 25 80 Q 0 80 0 65',
+ '6': 'M 45 10 Q 45 0 25 0 Q 0 0 0 40 L 0 55 Q 0 80 25 80 Q 50 80 50 57 Q 50 35 25 35 Q 0 35 0 55',
+ '7': 'M 0 0 L 50 0 L 20 80',
+ '8': 'M 25 40 Q 0 40 0 20 Q 0 0 25 0 Q 50 0 50 20 Q 50 40 25 40 Q 0 40 0 60 Q 0 80 25 80 Q 50 80 50 60 Q 50 40 25 40',
+ '9': 'M 5 70 Q 5 80 25 80 Q 50 80 50 40 L 50 25 Q 50 0 25 0 Q 0 0 0 23 Q 0 45 25 45 Q 50 45 50 25'
+};
+
+/**
+ * Generate a dotted path for a single character
+ */
+function generateDottedChar(char, x, y, scale = 1, color = '#000000') {
+ const path = letterPaths[char.toUpperCase()] || numberPaths[char];
+ if (!path) return '';
+
+ // Transform the path to the correct position and scale
+ const transformedPath = path.replace(/(\d+)/g, (match) => {
+ return match; // Keep original values, we'll use transform
+ });
+
+ return ``;
+}
+
+/**
+ * Generate a row of dotted characters centered on the page with dynamic scaling
+ */
+function generateCharacterRow(text, y, baseScale = 1, color = '#000000') {
+ const baseCharWidth = 50;
+ const baseSpacing = 20;
+ const maxWidth = 460; // Leave 20px margin on each side
+
+ // Calculate required width with base scale
+ const requiredWidth = text.length * (baseCharWidth + baseSpacing) * baseScale - baseSpacing * baseScale;
+
+ // Scale down if content is too wide
+ let finalScale = baseScale;
+ if (requiredWidth > maxWidth) {
+ finalScale = maxWidth / (text.length * (baseCharWidth + baseSpacing) - baseSpacing);
+ }
+
+ const charWidth = baseCharWidth * finalScale;
+ const spacing = baseSpacing * finalScale;
+ const totalWidth = text.length * charWidth + (text.length - 1) * spacing;
+ let startX = (500 - totalWidth) / 2;
+
+ let svg = '';
+ for (let i = 0; i < text.length; i++) {
+ const char = text[i];
+ svg += generateDottedChar(char, startX + i * (charWidth + spacing), y, finalScale, color);
+ }
+ return svg;
+}
+
+/**
+ * Generate baseline guide
+ */
+function generateBaseline(y, width = 400) {
+ const startX = (500 - width) / 2;
+ return ``;
+}
+
+/**
+ * Generate complete writing worksheet
+ * @param {Object} params
+ * @param {string} params.word - Word to trace (if tracing letters)
+ * @param {number[]} params.numbers - Numbers to trace (if tracing numbers)
+ * @param {boolean} params.isNumbers - Whether to trace numbers
+ * @param {number} params.age - Child's age (affects complexity)
+ */
+export function generateWritingWorksheet({ word, numbers, isNumbers, age }) {
+ const content = isNumbers ? numbers.join('') : word.toUpperCase();
+ const title = isNumbers ? 'Trace the Numbers' : `Trace: ${word.toUpperCase()}`;
+
+ const svg = ``;
+
+ return {
+ title: title,
+ subtitle: isNumbers ? 'Practice writing your numbers!' : 'Practice writing letters!',
+ sections: [{
+ id: 'writing-main',
+ type: 'writing',
+ content_svg: svg
+ }]
+ };
+}
+
+export default { generateWritingWorksheet };
diff --git a/server/index.js b/server/index.js
index f9f1837c..40314526 100644
--- a/server/index.js
+++ b/server/index.js
@@ -5,7 +5,14 @@ import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { GoogleGenerativeAI } from '@google/generative-ai';
-import { getPromptForActivity } from './prompts.js';
+
+// Import programmatic generators
+import {
+ generateWritingWorksheet,
+ generateCountingWorksheet,
+ generateMazeWorksheet,
+ generateColoringPage
+} from './generators/index.js';
dotenv.config();
@@ -27,7 +34,6 @@ const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || '');
const GALLERY_FILE = path.join(__dirname, 'gallery.json');
const MAX_GALLERY_SIZE = 6;
-// Load gallery from file
function loadGallery() {
try {
if (fs.existsSync(GALLERY_FILE)) {
@@ -40,7 +46,6 @@ function loadGallery() {
return [];
}
-// Save gallery to file
function saveGallery(gallery) {
try {
fs.writeFileSync(GALLERY_FILE, JSON.stringify(gallery, null, 2));
@@ -49,31 +54,113 @@ function saveGallery(gallery) {
}
}
-// Add item to gallery (keeps only recent 6)
function addToGallery(item) {
const gallery = loadGallery();
-
- // Add new item at the beginning
gallery.unshift({
...item,
id: Date.now(),
createdAt: new Date().toISOString()
});
-
- // Keep only the most recent 6
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
// ============================================
-// Get recent gallery items
app.get('/api/gallery', (req, res) => {
try {
const gallery = loadGallery();
@@ -86,14 +173,29 @@ app.get('/api/gallery', (req, res) => {
app.post('/api/surprise', async (req, res) => {
try {
- if (!process.env.GEMINI_API_KEY) {
- return res.status(500).json({ error: 'Missing API Key' });
- }
- const model = genAI.getGenerativeModel({ model: "gemini-2.5-pro" });
+ 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 from: coloring, numbers, writing, find-way, counting.
+ 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:
{
@@ -105,8 +207,18 @@ app.post('/api/surprise', async (req, res) => {
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();
- res.json(JSON.parse(jsonStr));
+ 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' });
@@ -116,43 +228,84 @@ app.post('/api/surprise', async (req, res) => {
app.post('/api/generate', async (req, res) => {
try {
const { theme, age, sections } = req.body;
- console.log('Received generation request:', { theme, age, sections });
+ const activityType = sections[0] || 'writing';
+
+ console.log('Generating:', { theme, age, activityType });
if (!process.env.GEMINI_API_KEY) {
- console.warn('Missing GEMINI_API_KEY');
- return res.status(500).json({ error: 'Server configuration error: Missing API Key' });
+ return res.status(500).json({ error: 'Missing API Key' });
}
- const model = genAI.getGenerativeModel({ model: "gemini-2.5-pro" });
+ // Step 1: Get parameters from LLM (fast, small response)
+ const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
+ const paramPrompt = parameterPrompts[activityType] || parameterPrompts.writing;
- // Get the activity-specific prompt for the first section
- const activityType = sections[0] || 'coloring';
- const prompt = getPromptForActivity(activityType, theme, age);
-
- console.log(`Using specialized prompt for activity: ${activityType}`);
-
- const result = await model.generateContent(prompt);
- const response = await result.response;
- const text = response.text();
-
- // Cleanup markdown code blocks if present
+ 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 data = JSON.parse(jsonStr);
+ const params = JSON.parse(jsonStr);
- if (!data.title || !data.sections || !Array.isArray(data.sections)) {
- throw new Error('Invalid generation format');
+ 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
- const galleryItem = {
- ...data,
- age,
- activityType
- };
- addToGallery(galleryItem);
- console.log('Added to gallery, current size:', loadGallery().length);
+ addToGallery(worksheet);
+ console.log('Generated worksheet:', worksheet.title);
- res.json(data);
+ res.json(worksheet);
} catch (error) {
console.error('Generation error:', error);
res.status(500).json({ error: 'Failed to generate content' });
@@ -161,4 +314,5 @@ app.post('/api/generate', async (req, res) => {
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
+ console.log('Using hybrid generation: LLM for parameters, code for SVGs');
});
diff --git a/server/prompts.js b/server/prompts.js
index 3ed7b8c0..4c81d146 100644
--- a/server/prompts.js
+++ b/server/prompts.js
@@ -43,37 +43,103 @@ CRITICAL: content_svg MUST be a string with single quotes in SVG attributes. Pat
export const activityPrompts = {
coloring: (theme, age) => `
-You are a professional illustrator of high-quality children's coloring books. Create a BEAUTIFUL, ENGAGING coloring page.
+You are a professional SVG illustrator creating REALISTIC, RECOGNIZABLE children's coloring pages.
Theme: ${theme}
Age: ${age} years old
-YOUR MISSION: Create ONE stunning coloring page featuring the theme in a way that makes children excited to color it.
+🎯 YOUR GOAL: Create a coloring page with ONE clear subject that a 3-year-old can immediately identify as "${theme}".
-⚠️ STRICT ACTIVITY RESTRICTION — THIS IS A COLORING PAGE ONLY:
-- DO NOT include any numbers, digits, or numerals of any kind
-- DO NOT include any letters, words, text, or alphabet characters
-- DO NOT include any counting exercises or number recognition
-- DO NOT include any mazes, paths to follow, or puzzles
-- DO NOT include any tracing lines, dotted lines, or writing practice
-- DO NOT include any educational exercises — ONLY pure illustration for coloring
-- The ONLY activity is coloring inside the outlined shapes
+🚨 ABSOLUTE SVG REQUIREMENTS (CRITICAL - NON-NEGOTIABLE):
+- EVERY single path MUST have fill="none"
+- EVERY single path MUST have stroke="black" or stroke="#000000"
+- Use stroke-width="2" or "3" for all outlines
+- NO filled shapes allowed - children need empty white areas to color
+- Example:
-COLORING PAGE SPECIFIC RULES:
-- Create ONE main cute subject (or 2-3 closely related elements) as the centerpiece
-- Design 8-20 large, clearly defined CLOSED areas perfect for coloring
-- Each colorable region should be satisfyingly large — kids love filling big spaces
-- Add cute details that enhance the theme: accessories, background elements, decorations
-- Characters should have friendly expressions: big round eyes, gentle smiles
-- Include a simple ground line or setting context (grass, clouds, water) if appropriate
-- Balance white space — not too crowded, not too empty
-- Make outlines bold and confident so young hands can color inside easily
+📐 STEP-BY-STEP SVG CONSTRUCTION METHOD:
-COMPOSITION TIPS:
-- Center the main subject or use rule-of-thirds for dynamic layouts
-- Vary the sizes of colorable regions for visual interest
-- Add 2-4 small decorative elements (stars, hearts, flowers) around the main subject
-- Ensure all shapes are CLOSED and ready to be filled with color
+For ANIMALS (cat, dog, panda, unicorn, etc.):
+1. HEAD: Draw a large circle or oval for head (100-150px diameter), centered around x=250, y=200
+2. EARS: Add 2 rounded ears on top of head (circles, triangles, or ovals)
+3. EYES: 2 circles (30-40px diameter) positioned symmetrically
+4. NOSE: Small circle or triangle in center below eyes
+5. MOUTH: Simple curved line (smile) below nose
+6. BODY: Large oval or rounded rectangle below head (150-200px wide, 180-220px tall)
+7. LEGS: 4 rounded rectangles or ovals extending down from body
+8. TAIL: Curved path on one side
+9. DETAILS: Add characteristic features (whiskers, spots, horn, etc.)
+
+For OBJECTS (car, rocket, house, etc.):
+1. MAIN BODY: Start with basic shape (rectangle for car/house, elongated triangle for rocket)
+2. MAJOR COMPONENTS: Add 2-4 large recognizable parts (wheels, windows, door, roof)
+3. DETAILS: Add smaller features that make it identifiable
+
+For PLANTS (flower, tree, etc.):
+1. CENTER/TRUNK: Main central element
+2. PETALS/LEAVES: 5-8 symmetrical rounded shapes around center
+3. STEM: Vertical line or path
+4. BASE: Ground line or pot
+
+🎨 SPECIFIC THEME EXAMPLES:
+
+UNICORN:
+- Circle head (cx="250" cy="180", r="70")
+- 2 triangle ears
+- HORN: Triangle pointing up from forehead (30-40px tall)
+- 2 circle eyes
+- Small nose circle
+- Curved smile
+- Oval body below head
+- 4 rounded rectangle legs
+- Flowing curved tail on right side
+- Mane: 3-4 curved strokes on neck
+
+PANDA:
+- Large circle head (r="80")
+- 2 small circle ears on top
+- 2 LARGE circle eye patches (r="25", OUTLINED not filled)
+- 2 smaller circle eyes inside patches
+- Small circle nose
+- Smile below nose
+- Large oval body
+- 4 thick rounded legs
+- Optional: bamboo stick to the side (2 parallel lines)
+
+CAT:
+- Circle head (r="60")
+- 2 triangle ears pointing up
+- 2 circle eyes
+- Small triangle nose
+- Whiskers (3 lines each side)
+- Curved smile
+- Oval body
+- 4 legs
+- Curved S-shaped tail
+
+FLOWER:
+- Center circle (r="40")
+- 6-8 rounded petal paths around center (use ellipses or bezier curves)
+- Straight stem line down from center
+- 2 leaf shapes on stem (ellipses at angle)
+- Simple ground line
+
+🔍 QUALITY CHECKLIST - YOUR SVG MUST:
+✓ Have 8-15 large closed shapes ready for coloring
+✓ Be IMMEDIATELY recognizable as "${theme}"
+✓ Use ONLY stroke outlines (fill="none" on ALL paths)
+✓ Have bold stroke-width="2" or "3"
+✓ Be 500x500 viewBox with subject centered
+✓ Include characteristic features (horn for unicorn, eye patches for panda, etc.)
+✓ Have simple, rounded, child-friendly shapes
+✓ Look like it came from a store-bought coloring book
+
+❌ AVOID:
+- Abstract or geometric art
+- Filled/colored areas
+- Tiny details (too hard for kids)
+- Complex overlapping shapes
+- Unrecognizable forms
${baseRules}
${ageRules(age)}
diff --git a/src/components/Hero.jsx b/src/components/Hero.jsx
index 5ff85504..c5aa1b31 100644
--- a/src/components/Hero.jsx
+++ b/src/components/Hero.jsx
@@ -15,7 +15,6 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
// Sync state when a sheet is restored
React.useEffect(() => {
if (generatedSheet) {
- setTheme(generatedSheet.title || '');
if (generatedSheet.age) setAge(generatedSheet.age);
// If sections exist, restore the first one's type as selected
if (generatedSheet.sections && generatedSheet.sections.length > 0) {
@@ -68,24 +67,32 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
// since we don't have a separate loading state, we can reuse isGenerating logic or add one.
// For now, we'll just set the values when they arrive.
- const res = await fetch('/api/surprise', { method: 'POST' });
+ const res = await fetch('/api/surprise', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' }
+ });
if (!res.ok) throw new Error('Surprise failed');
const idea = await res.json();
+ console.log('Surprise response from backend:', idea);
+
// Validate section ID exists in our map, else fallback
- const validSection = sections.find(s => s.id === idea.section) ? idea.section : 'coloring';
+ const validSections = ['coloring', 'writing', 'find-way', 'counting'];
+ const validSection = validSections.includes(idea.sections[0]) ? idea.sections[0] : 'coloring';
+
+ console.log('Section from backend:', idea.sections[0], '-> Valid section:', validSection);
setTheme(idea.theme);
setAge(idea.age);
setSelectedSection(validSection);
- // Auto-generate for surprise
+ // Auto-generate for surprise - use validSection directly, not state
if (onGenerate) {
onGenerate({
theme: idea.theme,
age: idea.age,
- sections: [validSection]
+ sections: [validSection] // Use validSection, not selectedSection from state
});
}
} catch (err) {
@@ -263,7 +270,7 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
-
+
{isGenerating && (
@@ -280,12 +287,12 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
className="h-full bg-gradient-to-r from-violet-500 via-primary-500 to-indigo-500 rounded-full"
initial={{ width: "0%" }}
animate={{ width: "95%" }}
- transition={{ duration: 30, ease: "easeOut" }}
+ transition={{ duration: selectedSection === 'coloring' ? 15 : 5, ease: "easeOut" }}
/>
Generating...
- ~30 seconds
+ ~{selectedSection === 'coloring' ? '15' : '5'} seconds
@@ -308,7 +315,7 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
key="msg1"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 1, 0] }}
- transition={{ duration: 7, times: [0, 0.1, 0.9, 1], delay: 0 }}
+ transition={{ duration: 2, times: [0, 0.1, 0.9, 1], delay: 0 }}
className="text-sm text-slate-500 absolute left-0 right-0 text-center"
>
✨ Adding some magic...
@@ -317,7 +324,7 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
key="msg2"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 1, 0] }}
- transition={{ duration: 7, times: [0, 0.1, 0.9, 1], delay: 7 }}
+ transition={{ duration: 2, times: [0, 0.1, 0.9, 1], delay: 2 }}
className="text-sm text-slate-500 absolute left-0 right-0 text-center"
>
🎨 Drawing cute illustrations...
@@ -326,7 +333,7 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
key="msg3"
initial={{ opacity: 0 }}
animate={{ opacity: [0, 1, 1, 0] }}
- transition={{ duration: 7, times: [0, 0.1, 0.9, 1], delay: 14 }}
+ transition={{ duration: 2, times: [0, 0.1, 0.9, 1], delay: 4 }}
className="text-sm text-slate-500 absolute left-0 right-0 text-center"
>
📝 Almost ready...
@@ -335,7 +342,7 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
key="msg4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
- transition={{ delay: 21 }}
+ transition={{ delay: 4 }}
className="text-sm text-slate-500 absolute left-0 right-0 text-center"
>
🚀 Finishing up...
@@ -347,7 +354,7 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
{previewTheme}
-
{previewSubtitle}
+
{previewSubtitle}
{generatedSheet ? (