ACTIVE MANUSCRIPTS

VIEW ALL

MARKET ANALYSES

VIEW ALL

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

NEW MANUSCRIPT WIZARD

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

MANUSCRIPTS

Books in progress — click to open the editor

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
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
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 = ''; }
Team Decisions
Review, edit or delete entries before building. Add your own notes too.
All
Decisions
Briefs
Findings
Notes

Changelog

Loading…
Team Working
Watch your agents collaborate in real time
Queued 0
Working 0
Needs Answer 0
Done 0
TOOLS