Agents will appear here when they're working
No projects yet
🤖
Clemence
What are we building today?
Tell me about your book idea — dump everything. Competitor links, screenshots, half-formed thoughts, genre ideas. I'll make sense of it and coordinate the team.
I want to make a puzzle book for kids
Help me research a niche
I have a competitor book I want to beat
I have an idea but I'm not sure of the market

Projects

Analyze

Research competitor books to find market gaps and brief Clemence

Add a competitor book
📷
Cover image (optional)
PDF (optional — for content analysis)
Amazon URL (paste and extract data)
Additional competitor URLs (one per line, optional)
Analysed Books
✨ Create New Book
Choose how to start
Start From Scratch
Build a new book from concept to completion
🔄
From Analysis
Use competitor insights to create something better
📋
From Template
Start with proven structures
Coming Soon
Publishing Stage 6
KDP Listing Builder
Build an optimized Amazon listing from your analysis

Your Team

Skills and memory — what each agent knows and how they work
🧠
Loading…

Tools

Puzzle & activity generators — export SVG and Excel, print-ready

Ready
🔡
Word Search
AI-suggested words, scored placement, 10×10 to 30×30
Ready
✏️
Crossword
Scored intersection layout, exports puzzle SVG + solution SVG + clue sheet
Ready
Crisscross
Interlocking word grid, no clues needed
Ready
🌸
Word Flower
Hexagonal letter arrangement puzzle
Ready
🔢
Code Word
Each number maps to a letter — crack the code
Ready
🔐
Decipher
Encoded messages with substitution cipher
Ready
🔀
Word Scramble
Scrambled letter boxes with answer spaces — great for vocab practice
Ready
🪜
Word Ladder
Change one letter at a time from start to end word — classic word chain puzzle
Ready
🔀
Word Jumble
Unscramble each word — circled letters reveal a mystery bonus word
Ready
✏️
Fill in the Blank
Passage with missing words and a shuffled word bank — great for learning
Ready
🔢
Sudoku
Easy → Expert, guaranteed unique solution, exports puzzle + solution SVG
Ready
🔢
Number Sequence
Fill-in-the-blank number patterns — arithmetic, geometric, Fibonacci and more
Ready
🧩
Jigsaw
Printable jigsaw cut-guide with interlocking bezier tabs — print, stick, cut
Ready
🌀
Maze
Square, circle, or triangle — random or custom size
Ready
Quiz Builder
Multiple choice, true/false, fill-in-the-blank — exports quiz + answer key SVG
Word Search
🔡Configure your puzzle and press Generate

Studio

AI-powered image generation for storybooks, stickers, and more

AI
🎨
Image Generator
Generate illustrations, icons, and artwork for your books
AI
🏷️
Sticker Book
Generate sticker sheets — icons laid out on a printable page
AI
✏️
How to Draw
Step-by-step drawing guides with progressive line art — print-ready
AI
Dot-to-Dot
Generate numbered dot-to-dot puzzles from AI illustrations
Image Generator
🎨

Configure your image and press Generate

// ===== WORD JUMBLE ===== function renderWordJumbleControls() { document.getElementById('tool-ws-title').textContent = 'Word Jumble'; document.getElementById('tool-controls').innerHTML = `
`; } async function wjAiWords() { const topic = document.getElementById('wj-topic')?.value?.trim(); if (!topic) return; const btn = document.querySelector('#tool-controls .tc-ai-btn'); if (btn) btn.textContent = '...'; try { const r = await fetch('/api/chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ messages:[{role:'user',content:`Generate 6 words for a Word Jumble puzzle on the topic: "${topic}". Words should be 5-8 letters each. Return ONLY a plain list, one word per line, uppercase, no punctuation.`}], max_tokens:120 }) }); const d = await r.json(); const text = d.content?.[0]?.text || d.choices?.[0]?.message?.content || ''; const words = text.split('\n').map(w=>w.trim().toUpperCase().replace(/[^A-Z]/g,'')).filter(w=>w.length>=4).slice(0,8); const ta = document.getElementById('wj-words'); if (ta) ta.value = words.join('\n'); } catch(e) { alert('AI error: '+e.message); } if (btn) btn.textContent = 'AI✦'; } function scrambleWord(word) { const arr = word.split(''); // Keep shuffling until different from original let shuffled = word; let attempts = 0; while (shuffled === word && attempts < 20) { for (let i = arr.length-1; i > 0; i--) { const j = Math.floor(Math.random()*(i+1)); [arr[i],arr[j]] = [arr[j],arr[i]]; } shuffled = arr.join(''); attempts++; } return shuffled; } function generateWordJumble() { const raw = document.getElementById('wj-words')?.value?.trim(); if (!raw) return; const words = raw.split('\n').map(w=>w.trim().toUpperCase().replace(/[^A-Z]/g,'')).filter(w=>w.length>=3).slice(0,10); if (!words.length) return; const title = document.getElementById('wj-title')?.value?.trim() || 'Word Jumble'; const clue = document.getElementById('wj-clue')?.value?.trim(); const font = document.getElementById('wj-font')?.value || 'Familjen Grotesk'; const cellSize = parseInt(document.getElementById('wj-cell')?.value||38); const scrambled = words.map(w => scrambleWord(w)); // Pick one circle position per word (letter 1, or a varied position) const circlePosInOriginal = words.map((w,i) => i % Math.max(2, Math.floor(w.length/2))); // Circle letters are from the ORIGINAL words at those positions const circleLetters = words.map((w,i) => w[circlePosInOriginal[i]]); // In the SVG, we circle the answer box at that position (not the scrambled row) const pad = 36, rowGap = 14; const strokeW = 2.5; const fs = Math.round(cellSize * 0.5); const rowH = cellSize * 2 + rowGap + 24; const maxLen = Math.max(...words.map(w=>w.length)); const gridW = maxLen * (cellSize + 5); const labelW = 30; const W = pad*2 + labelW + 14 + gridW; const titleH = 52; const H = pad + titleH + words.length * (rowH + 12) + 80 + (clue ? 28 : 0); let svg = ``; svg += ``; svg += ``; svg += `${E(title)}`; svg += `Unscramble each word · Write your answers in the boxes below · Circle letters build the bonus word`; words.forEach((word, wi) => { const y = pad + titleH + wi * (rowH + 12); const sc = scrambled[wi]; // Row number svg += `${wi+1}.`; // Scrambled letters sc.split('').forEach((ch, ci) => { const bx = pad + labelW + ci*(cellSize+5); svg += ``; svg += `${ch}`; }); // Answer boxes below (blank) const circlePos = circlePosInOriginal[wi]; word.split('').forEach((_, li) => { const ax = pad + labelW + li*(cellSize+5); const ay = y + cellSize + rowGap; const isCircle = li === circlePos; if (isCircle) { // Circled box — accent colour svg += ``; svg += ``; } else { svg += ``; } }); }); // Mystery bonus section const bonusY = pad + titleH + words.length*(rowH+12) + 16; svg += ``; svg += `${clue || 'Bonus: unscramble the circled letters!'}`; circleLetters.forEach((_, li) => { const bx = pad + li*(cellSize+5); const by = bonusY + 30; svg += ``; svg += ``; }); svg += ``; // Solution SVG let solSvg = svg.replace(/]*>\s*<\/circle>/g,''); // Rebuild with answers filled in solSvg = buildWordJumbleSolution(words, scrambled, circlePosInOriginal, circleLetters, title, clue, font, cellSize, W, H, pad, labelW, rowH, rowGap, titleH); puzzleState.svg = svg; puzzleState.solutionSvg = solSvg; document.getElementById('tool-preview').innerHTML = `
${svg}
`; document.getElementById('tool-preview-info').textContent = `${words.length} words · ${circleLetters.join('')} (bonus letters)`; document.getElementById('tool-export-btn').style.display = ''; document.getElementById('export-paths-btn').style.display = ''; document.getElementById('export-font-btn').style.display = ''; } function buildWordJumbleSolution(words, scrambled, circlePosInOriginal, circleLetters, title, clue, font, cellSize, W, H, pad, labelW, rowH, rowGap, titleH) { const fs = Math.round(cellSize*0.5); let svg = ``; svg += ``; svg += ``; svg += `${E(title)} — ANSWERS`; svg += `Answer Key`; words.forEach((word, wi) => { const y = pad + titleH + wi*(rowH+12); const sc = scrambled[wi]; svg += `${wi+1}.`; sc.split('').forEach((ch,ci) => { const bx = pad+labelW+ci*(cellSize+5); svg += ``; svg += `${ch}`; }); const circlePos = circlePosInOriginal[wi]; word.split('').forEach((ch,li) => { const ax = pad+labelW+li*(cellSize+5); const ay = y+cellSize+rowGap; const fill = li===circlePos?'#e8f5e9':'white'; const stroke = li===circlePos?'#111':'#ccc'; const sw = li===circlePos?'2.5':'1.5'; svg += ``; svg += `${ch}`; }); }); const bonusY = pad+titleH+words.length*(rowH+12)+16; svg += ``; svg += `${clue||'Bonus answer:'}`; circleLetters.forEach((ch,li) => { const bx=pad+li*(cellSize+5); const by=bonusY+30; svg += ``; svg += `${ch}`; }); svg += ``; return svg; } // ===== FILL IN THE BLANK ===== function renderFillBlankControls() { document.getElementById('tool-ws-title').textContent = 'Fill in the Blank'; document.getElementById('tool-controls').innerHTML = `
`; } async function fbAiPassage() { const topic = document.getElementById('fb-topic')?.value?.trim(); if (!topic) return; const btn = document.querySelector('#tool-controls .tc-ai-btn'); if (btn) btn.textContent = '...'; try { const r = await fetch('/api/chat', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ messages:[{role:'user',content:`Write a short educational passage (3-5 sentences, 60-100 words) about: "${topic}". Use clear, factual language suitable for a kids activity book. Return only the passage text, no title, no extra formatting.`}], max_tokens:200 }) }); const d = await r.json(); const text = d.content?.[0]?.text || d.choices?.[0]?.message?.content || ''; const ta = document.getElementById('fb-passage'); if (ta) ta.value = text.trim(); } catch(e) { alert('AI error: '+e.message); } if (btn) btn.textContent = 'AI✦'; } function generateFillBlank() { const passage = document.getElementById('fb-passage')?.value?.trim(); if (!passage || passage.length < 20) { document.getElementById('tool-preview').innerHTML='
✏️Write or generate a passage first
'; return; } const title = document.getElementById('fb-title')?.value?.trim() || 'Fill in the Blank'; const font = document.getElementById('fb-font')?.value || 'Familjen Grotesk'; const count = parseInt(document.getElementById('fb-count')?.value||6); // Tokenise into words, pick content words to blank const tokens = passage.split(/(\s+|[^\w']+)/); // split preserving separators const wordIndices = []; tokens.forEach((t,i) => { if (/^[a-zA-Z']{3,}$/.test(t)) wordIndices.push(i); }); // Pick `count` evenly spaced from word indices const step = Math.max(1, Math.floor(wordIndices.length / count)); const blankedIndices = new Set(wordIndices.filter((_,i) => i % step === Math.floor(step/2)).slice(0, count)); const blankedWords = []; const blankTokens = tokens.map((t,i) => { if (blankedIndices.has(i)) { blankedWords.push(t); return '___'; } return t; }); const blankPassage = blankTokens.join(''); // Shuffle word bank const wordBank = [...blankedWords].sort(() => Math.random()-0.5); // Build SVG const W = 680, pad = 48, lineH = 28, fs = 15, titleFs = 22; // Wrap text const words = blankPassage.split(' '); const lines = []; let cur = ''; const approxCharW = fs * 0.58; const maxChars = Math.floor((W - pad*2) / approxCharW); words.forEach(w => { if ((cur + ' ' + w).trim().length > maxChars && cur) { lines.push(cur.trim()); cur = w; } else cur = (cur + ' ' + w).trim(); }); if (cur) lines.push(cur.trim()); const wbLineH = 32; const wbLines = Math.ceil(wordBank.length / 4); const H = pad + titleFs + 16 + 14 + lines.length * lineH + pad + wbLines * wbLineH + pad; let svg = ``; svg += ``; svg += ``; svg += `${E(title)}`; svg += `Fill in the blanks using the word bank below.`; let ty = pad + titleFs + 14 + lineH; lines.forEach(line => { // Render each word — blanks as underlines let lx = pad; const lineWords = line.split(' '); lineWords.forEach((w, wi) => { if (w === '___') { const uw = 72; svg += ``; lx += uw + (wi < lineWords.length-1 ? 6 : 0); } else { const ww = w.length * approxCharW; svg += `${E(w)}`; lx += ww + (wi < lineWords.length-1 ? 5 : 0); } }); ty += lineH; }); // Word bank ty += 16; svg += ``; ty += 16; svg += `WORD BANK`; ty += 14; wordBank.forEach((w,i) => { const col = i % 4; const row = Math.floor(i/4); const wx = pad + col * ((W-pad*2)/4); const wy = ty + row * wbLineH; svg += ``; svg += `${E(w)}`; }); svg += ``; // Answer key SVG — same but blanks filled in const solSvg = svg.replace(/___/g, (_, offset) => { // Just rebuild with answers return ''; }).replace(//g, ''); // Simpler: just add ANSWER KEY label and replace blanks with words let solTokens = [...tokens]; let wordIdx = 0; const solSvgClean = (() => { let s = ``; s += ``; s += ``; s += `${E(title)} — ANSWERS`; s += `Answer Key`; let aty = pad+titleFs+14+lineH; let bIdx=0; lines.forEach(line => { let lx=pad; line.split(' ').forEach((w,wi)=>{ const dispWord = w==='___' ? blankedWords[bIdx++] : w; const ww = dispWord.length*approxCharW; const fill = w==='___'?'#2a6e00':'#111'; s += `${E(dispWord)}`; lx += ww + (wi`; aty+=16; s+=`WORD BANK`; aty+=14; wordBank.forEach((w,i)=>{const col=i%4;const row=Math.floor(i/4);const wx=pad+col*((W-pad*2)/4);const wy=aty+row*wbLineH;s+=``;s+=`${E(w)}`;}); s+=``; return s; })(); puzzleState.svg = svg; puzzleState.solutionSvg = solSvgClean; document.getElementById('tool-preview').innerHTML = `
${svg}
`; document.getElementById('tool-preview-info').textContent = `${blankedWords.length} blanks · ${lines.length} lines`; document.getElementById('tool-export-btn').style.display = ''; document.getElementById('export-paths-btn').style.display = ''; document.getElementById('export-font-btn').style.display = ''; }