feat: Extend worksheet generation age range to 10 years, refine generator parameters, and add PDF watermarks.

This commit is contained in:
francy 2026-02-07 15:36:17 +01:00
parent ed9c118fef
commit 5630c6cee7
9 changed files with 172 additions and 160 deletions

File diff suppressed because one or more lines are too long

View File

@ -120,7 +120,7 @@ const illustrations = {
<circle cx='15' cy='30' r='2' fill='none' stroke='#000' stroke-width='1'/> <circle cx='15' cy='30' r='2' fill='none' stroke='#000' stroke-width='1'/>
<path d='M10,40 Q15,45 20,42' fill='none' stroke='#000' stroke-width='1'/> <path d='M10,40 Q15,45 20,42' fill='none' stroke='#000' stroke-width='1'/>
<path d='M25,75 L25,95' stroke='#000' stroke-width='3'/> <path d='M25,75 L25,95' stroke='#000' stroke-width='3'/>
<path d='M40' y1='75' L40,95' stroke='#000' stroke-width='3'/> <path d='M40,75 L40,95' stroke='#000' stroke-width='3'/>
<path d='M60,75 L60,95' stroke='#000' stroke-width='3'/> <path d='M60,75 L60,95' stroke='#000' stroke-width='3'/>
<path d='M75,75 L75,95' stroke='#000' stroke-width='3'/> <path d='M75,75 L75,95' stroke='#000' stroke-width='3'/>
<path d='M85,50 Q100,45 95,55 Q105,60 90,65' fill='none' stroke='#000' stroke-width='2'/> <path d='M85,50 Q100,45 95,55 Q105,60 90,65' fill='none' stroke='#000' stroke-width='2'/>

View File

@ -19,6 +19,19 @@ const themedObjects = {
const objectNames = Object.keys(themedObjects); const objectNames = Object.keys(themedObjects);
/**
* Pluralize object names correctly
*/
function pluralize(word) {
const irregulars = {
'fish': 'Fish',
'butterfly': 'Butterflies',
'moon': 'Moons'
};
if (irregulars[word]) return irregulars[word];
return word.charAt(0).toUpperCase() + word.slice(1) + 's';
}
/** /**
* Generate random quantities that are all different * Generate random quantities that are all different
*/ */
@ -151,7 +164,16 @@ export function generateCountingWorksheet({ theme, shape, quantities, age }) {
const objectType = pickObjectForTheme(theme, shape); const objectType = pickObjectForTheme(theme, shape);
// ALWAYS generate random quantities for variety (LLM is deterministic) // ALWAYS generate random quantities for variety (LLM is deterministic)
const maxValue = age <= 4 ? 5 : 10; let maxValue;
if (age <= 4) {
maxValue = 5;
} else if (age <= 6) {
maxValue = 10;
} else if (age <= 8) {
maxValue = 15;
} else {
maxValue = 20;
}
const rowCount = 8; const rowCount = 8;
const finalQuantities = generateRandomQuantities(rowCount, maxValue); const finalQuantities = generateRandomQuantities(rowCount, maxValue);
@ -169,7 +191,7 @@ export function generateCountingWorksheet({ theme, shape, quantities, age }) {
// Dotted separator line between rows (except first row) // Dotted separator line between rows (except first row)
if (i > 0) { if (i > 0) {
objectRows += `<line x1='15' y1='${rowY}' x2='485' y2='${rowY}' stroke='#CCCCCC' stroke-width='1' stroke-dasharray='5,5'/>`; objectRows += `<line x1='15' y1='${rowY}' x2='485' y2='${rowY}' stroke='#AAAAAA' stroke-width='1' stroke-dasharray='5,5'/>`;
} }
// Row number indicator - vertically centered in row // Row number indicator - vertically centered in row
@ -191,7 +213,7 @@ export function generateCountingWorksheet({ theme, shape, quantities, age }) {
</svg>`; </svg>`;
return { return {
title: `Count the ${objectType.charAt(0).toUpperCase() + objectType.slice(1)}s!`, title: `Count the ${pluralize(objectType)}!`,
subtitle: 'Count each group and write the number', subtitle: 'Count each group and write the number',
sections: [{ sections: [{
id: 'counting-main', id: 'counting-main',

View File

@ -44,7 +44,7 @@ function generateMazeGrid(rows, cols, startEdge, endEdge) {
startRow = 0; startCol = 0; startRow = 0; startCol = 0;
break; break;
case 'bottom': case 'bottom':
startRow = rows - 1; startCol = rows - 1; startRow = rows - 1; startCol = cols - 1;
break; break;
case 'left': case 'left':
startRow = 0; startCol = 0; startRow = 0; startCol = 0;
@ -169,9 +169,15 @@ export function generateMazeWorksheet({ theme, difficulty = 1, age }) {
} else if (age <= 5 || difficulty === 2) { } else if (age <= 5 || difficulty === 2) {
rows = 5; rows = 5;
cols = 5; cols = 5;
} else { } else if (age <= 7) {
rows = 6; rows = 6;
cols = 6; cols = 6;
} else if (age <= 9) {
rows = 7;
cols = 7;
} else {
rows = 8;
cols = 8;
} }
// Randomly select start and end edges - favor opposite edges for better gameplay // Randomly select start and end edges - favor opposite edges for better gameplay

View File

@ -96,7 +96,7 @@ function generateCharacterRow(text, y, baseScale = 1, color = '#000000') {
*/ */
function generateBaseline(y, width = 400) { function generateBaseline(y, width = 400) {
const startX = (500 - width) / 2; const startX = (500 - width) / 2;
return `<line x1='${startX}' y1='${y}' x2='${startX + width}' y2='${y}' stroke='#CCCCCC' stroke-width='1'/>`; return `<line x1='${startX}' y1='${y}' x2='${startX + width}' y2='${y}' stroke='#AAAAAA' stroke-width='1'/>`;
} }
/** /**

View File

@ -10,8 +10,7 @@ import { GoogleGenerativeAI } from '@google/generative-ai';
import { import {
generateWritingWorksheet, generateWritingWorksheet,
generateCountingWorksheet, generateCountingWorksheet,
generateMazeWorksheet, generateMazeWorksheet
generateColoringPage
} from './generators/index.js'; } from './generators/index.js';
dotenv.config(); dotenv.config();
@ -75,9 +74,9 @@ function addToGallery(item) {
const parameterPrompts = { const parameterPrompts = {
writing: (theme, age) => { writing: (theme, age) => {
const randomSeed = Math.floor(Math.random() * 1000); const randomSeed = Math.floor(Math.random() * 1000);
const wordLength = `${age} letters`; 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' : '1-9'; const numberRange = age <= 4 ? '1-5' : age <= 6 ? '1-9' : age <= 8 ? '1-15' : '1-20';
const numberCount = age <= 4 ? '3-4' : '4-5'; const numberCount = age <= 4 ? '3-4' : age <= 6 ? '4-5' : '5-6';
return ` return `
Create a children's tracing worksheet for theme "${theme}". Create a children's tracing worksheet for theme "${theme}".
@ -109,7 +108,7 @@ CRITICAL: You MUST return JSON with BOTH "shape" AND "quantities":
{"shape": "star", "quantities": [3, 1, 5, 2, 7, 4, 8, 6]} {"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. - "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}) - "quantities": Array of exactly 8 different numbers (1-${age <= 4 ? 5 : age <= 6 ? 10 : age <= 8 ? 15 : 20})
Return ONLY valid JSON. No explanation. Return ONLY valid JSON. No explanation.
`; `;
@ -126,7 +125,9 @@ Return ONLY a JSON object:
Rules: Rules:
- difficulty 1 = easy (4x4 grid) for age 3-4 - difficulty 1 = easy (4x4 grid) for age 3-4
- difficulty 2 = medium (5x5 grid) for age 5-6 - difficulty 2 = medium (5x5 grid) for age 5-6
- difficulty 3 = hard (6x6 grid) for age 7+ - 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. Return ONLY valid JSON, no explanation.
`, `,
@ -183,7 +184,7 @@ app.post('/api/surprise', async (req, res) => {
Current timestamp: ${timestamp} Current timestamp: ${timestamp}
Generate ONE creative, fun, specific theme for a children's worksheet (e.g. "Space-Rex Coding", "Underwater Hamster Tea Party"). 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 age between 3 and 10.
Pick a RANDOM activity type with EQUAL PROBABILITY from these 4 options: Pick a RANDOM activity type with EQUAL PROBABILITY from these 4 options:
1. "coloring" - coloring pages 1. "coloring" - coloring pages

View File

@ -50,96 +50,69 @@ Age: ${age} years old
🎯 YOUR GOAL: Create a coloring page with ONE clear subject that a 3-year-old can immediately identify as "${theme}". 🎯 YOUR GOAL: Create a coloring page with ONE clear subject that a 3-year-old can immediately identify as "${theme}".
🚨 ABSOLUTE SVG REQUIREMENTS (CRITICAL - NON-NEGOTIABLE): 🚨 ABSOLUTE QUALITY RULES (CRITICAL - NON-NEGOTIABLE):
- EVERY single path MUST have fill="none" 1. USE WHITE FILLS: Every closed shape (head, body, legs, eyes) MUST have fill="#FFFFFF". This hides lines behind it.
- EVERY single path MUST have stroke="black" or stroke="#000000" 2. DRAWING ORDER: Draw from BACK to FRONT. Background parts first (tail, back legs), then main body, then front details (eyes, nose).
- Use stroke-width="2" or "3" for all outlines 3. CLOSED PATHS: All shapes must be fully closed paths so they can be filled with white.
- NO filled shapes allowed - children need empty white areas to color 4. NO OVERLAPPING MESS: White fills solve this. If you draw a leg over a body, the white fill of the leg must hide the body line behind it.
- Example: <path d="M 100 100 L 200 100" fill="none" stroke="black" stroke-width="2"/>
📐 STEP-BY-STEP SVG CONSTRUCTION METHOD: 🚨 SVG REQUIREMENTS (CRITICAL):
- Background: <rect width="500" height="500" fill="#FFFFFF"/> (Start with white canvas)
- Stroke: stroke="black" or stroke="#000000"
- Stroke Width: stroke-width="2" for main outlines, "2" for details
- Fill: fill="#FFFFFF" for all object parts (to block lines behind)
- Fill: fill="none" ONLY for open paths like whiskers or smiles
- Example Closed Shape: <path d="M..." fill="#FFFFFF" stroke="black" stroke-width="3"/>
- Example Open Line: <path d="M..." fill="none" stroke="black" stroke-width="2"/>
For ANIMALS (cat, dog, panda, unicorn, etc.): 📐 STEP-BY-STEP CONSTRUCTION METHOD (Layer by Layer):
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.): For ANIMALS (Draw in this order):
1. MAIN BODY: Start with basic shape (rectangle for car/house, elongated triangle for rocket) 1. BACK LEGS/TAIL (Furthest back): Draw simple shapes, fill="#FFFFFF", stroke="black"
2. MAJOR COMPONENTS: Add 2-4 large recognizable parts (wheels, windows, door, roof) 2. BODY (Middle): Large oval/shape, fill="#FFFFFF", stroke="black" (Hides the top of back legs)
3. DETAILS: Add smaller features that make it identifiable 3. HEAD (Front): Large circle/shape, fill="#FFFFFF", stroke="black" (Hides neck/body connection)
4. FRONT LEGS (Front): Shapes overlapping body, fill="#FFFFFF", stroke="black"
5. FACE DETAILS (Topmost): Eyes, nose, mouth. Eyes can have black pupils.
For PLANTS (flower, tree, etc.): For OBJECTS (Car, Rocket, etc):
1. CENTER/TRUNK: Main central element 1. MAIN CHASSIS/BODY: Large filled shape (fill="#FFFFFF")
2. PETALS/LEAVES: 5-8 symmetrical rounded shapes around center 2. WHEELS/WINGS: Draw on top or behind as needed
3. STEM: Vertical line or path 3. WINDOWS/DOORS: Draw on top of body (fill="#FFFFFF")
4. BASE: Ground line or pot 4. DETAILS: Lines, handles, bolts
🎨 SPECIFIC THEME EXAMPLES: For PLANTS:
1. STEM/BRANCHES: Main lines
2. LEAVES BEHIND: Fill="#FFFFFF"
3. FLOWER CENTER: Fill="#FFFFFF" (Draw last so it's on top)
4. PETALS: Fill="#FFFFFF"
UNICORN: 🎨 SPECIFIC THEME GUIDES:
- 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: UNICORN:
- Large circle head (r="80") - Body: Oval (fill="#FFFFFF")
- 2 small circle ears on top - Legs: 4 rectangles (fill="#FFFFFF") - make sure they attach naturally
- 2 LARGE circle eye patches (r="25", OUTLINED not filled) - Head: Oval/Horse shape (fill="#FFFFFF")
- 2 smaller circle eyes inside patches - START with separate shapes, but ensure they LOOK connected
- Small circle nose - Horn: Triangle on forehead
- Smile below nose - Mane/Tail: Flowing shapes
- Large oval body
- 4 thick rounded legs
- Optional: bamboo stick to the side (2 parallel lines)
CAT: ROBOT:
- Circle head (r="60") - Use geometric shapes (rectangles, circles)
- 2 triangle ears pointing up - Fill ALL shapes with #FFFFFF to look solid
- 2 circle eyes - Add buttons/panels on top
- Small triangle nose
- Whiskers (3 lines each side)
- Curved smile
- Oval body
- 4 legs
- Curved S-shaped tail
FLOWER: 🔍 QUALITY CHECKLIST:
- Center circle (r="40") Is the main subject clearly visible?
- 6-8 rounded petal paths around center (use ellipses or bezier curves) Are lines hidden where shapes overlap? (e.g. body line not visible through arm)
- Straight stem line down from center Is everything filled with white (except open lines)?
- 2 leaf shapes on stem (ellipses at angle) Is the stroke width consistent (bold 3px)?
- Simple ground line Is the image centered in 500x500?
🔍 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: AVOID:
- Abstract or geometric art - fill="none" on main shapes (causes messy overlaps)
- Filled/colored areas - Translucent/opacity effects
- Tiny details (too hard for kids) - Tiny dust/specks
- Complex overlapping shapes - Disconnected lines that should be closed
- Unrecognizable forms
${baseRules} ${baseRules}
${ageRules(age)} ${ageRules(age)}

View File

@ -110,7 +110,10 @@ function App() {
{/* Footer */} {/* Footer */}
<footer className="bg-white border-t border-slate-200 mt-20 py-12"> <footer className="bg-white border-t border-slate-200 mt-20 py-12">
<div className="max-w-7xl mx-auto px-4 text-center text-slate-500 text-sm"> <div className="max-w-7xl mx-auto px-4 text-center text-slate-500 text-sm">
<p>© 2024 WonderSheets. All rights reserved.</p> <p className="mb-2">
<a href="https://wondersheets.art" className="font-semibold text-primary-600 hover:text-primary-700">wondersheets.art</a>
</p>
<p>© {new Date().getFullYear()} WonderSheets. All rights reserved.</p>
</div> </div>
</footer> </footer>
</div> </div>

View File

@ -135,6 +135,13 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
const pdfHeight = (imgProperties.height * pdfWidth) / imgProperties.width; const pdfHeight = (imgProperties.height * pdfWidth) / imgProperties.width;
pdf.addImage(data, 'PNG', 0, 0, pdfWidth, pdfHeight); pdf.addImage(data, 'PNG', 0, 0, pdfWidth, pdfHeight);
// Add watermark at bottom center
const pageHeight = pdf.internal.pageSize.getHeight();
pdf.setFontSize(10);
pdf.setTextColor(150, 150, 150); // Light gray
pdf.text('wondersheets.art', pdfWidth / 2, pageHeight - 5, { align: 'center' });
return pdf; return pdf;
}; };
@ -190,19 +197,19 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center px-1"> <div className="flex justify-between items-center px-1">
<label className="font-semibold text-slate-700">Age: {age}</label> <label className="font-semibold text-slate-700">Age: {age}</label>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">3 to 7 years</span> <span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">3 to 10 years</span>
</div> </div>
<input <input
type="range" type="range"
min="3" min="3"
max="7" max="10"
value={age} value={age}
disabled={isGenerating} disabled={isGenerating}
onChange={(e) => setAge(parseInt(e.target.value))} onChange={(e) => setAge(parseInt(e.target.value))}
className="w-full h-3 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-primary-600 disabled:opacity-50" className="w-full h-3 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-primary-600 disabled:opacity-50"
/> />
<div className="flex justify-between text-xs text-slate-400 px-1"> <div className="flex justify-between text-xs text-slate-400 px-1">
<span>3</span><span>4</span><span>5</span><span>6</span><span>7</span> <span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span>
</div> </div>
</div> </div>