From e5ccc82950615e48d9ce206cbddad86e3489d0b2 Mon Sep 17 00:00:00 2001 From: francy Date: Fri, 30 Jan 2026 00:40:10 +0100 Subject: [PATCH] feat: Implement 'Surprise Me' worksheet generation, enhance SVG output quality with detailed prompt instructions, and display recent creations in the gallery. --- server/index.js | 83 +++++++++++++++++++++++++++++--------- src/App.jsx | 21 +++++++++- src/components/Gallery.jsx | 4 +- src/components/Hero.jsx | 83 ++++++++++++++++++++++++-------------- 4 files changed, 139 insertions(+), 52 deletions(-) diff --git a/server/index.js b/server/index.js index 9c01c95e..af53218b 100644 --- a/server/index.js +++ b/server/index.js @@ -14,6 +14,35 @@ app.use(express.json()); // Initialize Gemini API const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || ''); +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-flash" }); + const prompt = ` + 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-the-way, counting. + + Return JSON ONLY: + { + "theme": "string", + "age": number, + "section": "string" + } + `; + + const result = await model.generateContent(prompt); + const text = result.response.text(); + const jsonStr = text.replace(/```json/g, '').replace(/```/g, '').trim(); + res.json(JSON.parse(jsonStr)); + } 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; @@ -27,37 +56,53 @@ app.post('/api/generate', async (req, res) => { const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash" }); const prompt = ` - Create educational worksheet metadata for a ${age}-year-old. + +You are a professional illustrator of high-quality children's coloring books. Create ONLY clean, simple, bold-outline black-and-white SVGs in classic kid-book style: thick lines, large closed colorable areas, recognizable cute subjects, no distortions/blobs/weird features. + +Create educational worksheet metadata for a ${age}-year-old child. + Theme: ${theme} + Sections: ${sections.join(', ')} -Return ONLY a valid JSON object. +Return ONLY valid JSON. No other text, no explanations. -STRICT SVG REQUIREMENTS: -1. Viewbox: '0 0 500 500'. -2. Style: Black stroke (#000000), white fill (#ffffff), no gradients/filters. -3. Pathing: All paths must be CLOSED and colorable. Use smooth curves. -4. Completeness: DO NOT TRUNCATE. Ensure the SVG code is 100% complete and valid XML. -5. Age-Appropriateness: - - For 3-4: Use 8px strokes, bold outlines, and 'toddler-friendly' shapes (huge circles, squares). - - For 5-7: Use 3px strokes, add interior textures (scales, leaves, stars), and more realistic proportions. +MANDATORY STYLE & QUALITY RULES — MUST FOLLOW EXACTLY: +- Depict the theme literally and recognizably using standard, cute children's illustration style (e.g. animals look like real animals with big eyes/smiles; no invented blobs, hybrids, pigtails/bows on wrong things, distorted proportions, extra random parts). +- Every visible element MUST be FULLY CLOSED paths (use Z/z to close , or ///). NO open paths, NO , NO unless closed, NO stroke-only lines without enclosed fill regions. +- Forbidden forever: blobs, distorted/wrong anatomy (e.g. elephant with pigtails, sheep with leaf legs), floating disconnected lines/parts, random dots/scribbles/noise/artifacts, self-intersecting paths making uncolorable slivers, cut-off/incomplete shapes, inconsistent stroke widths, tiny details, fill="none" on main objects (except holes), any greyscale/shading. +- Stroke: #000000 solid (no dasharray except for traceable writing). Fill: #ffffff on all colorable regions. +- ViewBox: exactly '0 0 500 500'. Center content with 50–100px margins on all sides. No touching edges. +- Attributes: single quotes ' only for all XML. +- SVG: complete valid XML — ... . Use smooth curves (C, Q, S) for natural shapes. 8–20 large closed regions for coloring fun. +- High-quality kid style: bold thick outlines, big simple shapes, cute friendly expressions (big round eyes as circles/ovals, smiling curved mouth), balanced proportions (big head for cuteness), no complexity/noise. -JSON Structure: +AGE-SPECIFIC RULES — STRICT: +- For 3–4 years: Ultra-simple (1–2 huge objects max), stroke width 10–16px, very large regions (4–10 total), cartoon-big features, no small details/textures/patterns inside. +- For 5–7 years: Slightly more (3–6 elements), stroke 4–8px, allow small closed interior shapes (e.g. spots as tiny circles), but still bold and clear. + +SECTION-SPECIFIC LOGIC — MUST BE EDUCATIONAL & COHERENT: +- colouring: One main cute subject (or 2–3 related) in simple pose/scene, many large closed areas (body parts, accessories), bold outlines, friendly look. +- numbers: Large closed-path digits (1–10 or similar) + theme-related countable objects grouped nearby. +- writing: Dotted/dashed traceable letters/numbers/words (use closed shapes for forms + dots/arrows for direction), clean practice space. +- find-the-way: Simple maze with closed barrier walls (rect/curved), clear start (arrow/circle) + end (goal object), theme elements along path, no traps. +- counting: Groups of identical closed objects (e.g. 3–10 apples) matching a shown number, tied to theme. + +JSON exactly this structure: { - "title": "Title", - "subtitle": "Subtitle", + "title": "Short fun title", + "subtitle": "Clear one-sentence kid instruction (e.g. Color this happy animal!)", "sections": [ { - "id": "id", - "type": "coloring|writing|numbers", - "content_svg": "..." + "id": "coloring-main | numbers-1-5 | writing-letters | find-the-way-maze | counting-objects", + "type": "coloring|numbers|writing|find-the-way|counting", + "content_svg": "...FULL complete valid SVG code here..." } + // exactly one object per section in array ] } -CRITICAL: The 'content_svg' string must use SINGLE QUOTES for all XML attributes to prevent JSON parsing errors. - `; - +CRITICAL: content_svg MUST be a string with single quotes in SVG attributes. Paths smooth and 100% closed.`; const result = await model.generateContent(prompt); const response = await result.response; const text = response.text(); diff --git a/src/App.jsx b/src/App.jsx index a9d51ed3..446db797 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -10,6 +10,8 @@ function App() { const [generatedSheet, setGeneratedSheet] = React.useState(null); const [isGenerating, setIsGenerating] = React.useState(false); + const [history, setHistory] = React.useState([]); + const handleGenerate = async (data) => { setIsGenerating(true); try { @@ -25,6 +27,20 @@ function App() { const result = await response.json(); setGeneratedSheet(result); + + // Add to history + const newHistoryItem = { + id: Date.now(), + title: result.title, + subtitle: result.subtitle, + age: data.age, + downloads: 0, + bgColor: 'bg-primary-50', // Default color + // We'll need to figure out how to preview it in the gallery. + // For now, Gallery expects 'bgColor'. + }; + setHistory(prev => [newHistoryItem, ...prev]); + } catch (error) { console.error("Error generating sheet:", error); // Optional: Show error state @@ -33,6 +49,9 @@ function App() { } }; + const galleryItems = history.length > 0 ? history.slice(0, 3) : popularSheets.slice(0, 3); + const galleryTitle = history.length > 0 ? "Your Recent Creations" : "Most Popular Printables"; + return (
{/* Navbar */} @@ -55,7 +74,7 @@ function App() { {/* Main Content */}
- +
diff --git a/src/components/Gallery.jsx b/src/components/Gallery.jsx index fc08343a..560a7fa9 100644 --- a/src/components/Gallery.jsx +++ b/src/components/Gallery.jsx @@ -3,11 +3,11 @@ import { motion } from 'framer-motion'; import { Wand2, Download, Star } from 'lucide-react'; import { Button } from './ui/Button'; -export const Gallery = ({ items }) => { +export const Gallery = ({ items, title = "Most Popular Printables" }) => { return (
-

Most Popular Printables

+

{title}

diff --git a/src/components/Hero.jsx b/src/components/Hero.jsx index de37257b..9a775ddc 100644 --- a/src/components/Hero.jsx +++ b/src/components/Hero.jsx @@ -10,38 +10,64 @@ import { sections } from '../data/mockData'; export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => { const [theme, setTheme] = useState(''); const [age, setAge] = useState(5); - const [selectedSections, setSelectedSections] = useState(['coloring']); + const [selectedSection, setSelectedSection] = useState('coloring'); - const toggleSection = (id) => { - setSelectedSections(prev => - prev.includes(id) - ? prev.filter(x => x !== id) - : [...prev, id] - ); + const handleSectionChange = (id) => { + if (!isGenerating) { + setSelectedSection(id); + } }; - const handleSurpriseMe = () => { + const handleSurpriseMe = async () => { if (isGenerating) return; - const funnyThemes = ['Dinosaur Disco', 'Unicorn Space Party', 'Pirate Picnic', 'Robot Garden']; - const randomTheme = funnyThemes[Math.floor(Math.random() * funnyThemes.length)]; - setTheme(randomTheme); - setAge(Math.floor(Math.random() * 5) + 3); - const randomSections = sections.slice(0, 3).map(s => s.id); - setSelectedSections(randomSections); - // Auto-generate for surprise - if (onGenerate) { - onGenerate({ - theme: randomTheme, - age: Math.floor(Math.random() * 5) + 3, - sections: randomSections - }); + try { + // Optional: indicate "thinking" state here if needed, or rely on parent loading state + // 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('http://localhost:3001/api/surprise', { method: 'POST' }); + if (!res.ok) throw new Error('Surprise failed'); + + const idea = await res.json(); + + // Validate section ID exists in our map, else fallback + const validSection = sections.find(s => s.id === idea.section) ? idea.section : 'coloring'; + + setTheme(idea.theme); + setAge(idea.age); + setSelectedSection(validSection); + + // Auto-generate for surprise + if (onGenerate) { + onGenerate({ + theme: idea.theme, + age: idea.age, + sections: [validSection] + }); + } + } catch (err) { + console.error(err); + // Fallback to local random if network/AI fails + const funnyThemes = ['Dinosaur Disco', 'Unicorn Space Party', 'Pirate Picnic', 'Robot Garden']; + const randomTheme = funnyThemes[Math.floor(Math.random() * funnyThemes.length)]; + setTheme(randomTheme); + setAge(Math.floor(Math.random() * 5) + 3); + const randomSection = sections[Math.floor(Math.random() * sections.length)].id; + setSelectedSection(randomSection); + if (onGenerate) { + onGenerate({ + theme: randomTheme, + age: Math.floor(Math.random() * 5) + 3, + sections: [randomSection] + }); + } } }; const handleCreate = () => { if (onGenerate && !isGenerating) { - onGenerate({ theme, age, sections: selectedSections }); + onGenerate({ theme, age, sections: [selectedSection] }); } }; @@ -133,23 +159,20 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => { {/* Sections */}
- +
{sections.map(section => { - const isSelected = selectedSections.includes(section.id); - const isDisabled = !isSelected && selectedSections.length >= 2; + const isSelected = selectedSection === section.id; return (