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
Select an analysis to build from
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
📚
Active book — AI will use this topic for suggestions
Word Puzzles
Ready
🔡
Word Search
AI-suggested words, scored placement, 10×10 to 30×30
AI-powered image generation for storybooks, stickers, and more
Image & Visual
AI
🎨
Image Generator
Generate illustrations, icons, and artwork for your books
AI
🏷️
Sticker Book
Generate sticker sheets — icons laid out on a printable page
Drawing Guides
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
Convert to:
Team Chat
🤖Clemence
⊞Dashboard
🛠️Tools
🎨Studio
🧠Memory Bank
🔍Analyze
✨Create
📋Listing Builder
⚙️Settings
// ===== 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 = ``;
// 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 = ``;
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 = ``;
// 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 = ``; return s;
})();
puzzleState.svg = svg;
puzzleState.solutionSvg = solSvgClean;
document.getElementById('tool-preview').innerHTML = `