story book

import React, { useState, useRef, useEffect } from 'react'; import { BookOpen, Sparkles, Upload, Image as ImageIcon, ChevronRight, ChevronLeft, Loader2, Wand2, AlertCircle, Languages, ScanFace, Download } from 'lucide-react'; const apiKey = ""; // The execution environment provides the key at runtime // --- API Utility with Exponential Backoff --- const fetchWithRetry = async (url, options, retries = 5) => { const delays = [1000, 2000, 4000, 8000, 16000]; for (let i = 0; i < retries; i++) { try { const response = await fetch(url, options); if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return await response.json(); } catch (error) { if (i === retries - 1) throw error; await new Promise(res => setTimeout(res, delays[i])); } } }; // --- Main Application Component --- export default function App() { const [step, setStep] = useState('input'); // 'input', 'generating_story', 'reading' const [childName, setChildName] = useState(''); const [theme, setTheme] = useState('Magical Forest'); const [language, setLanguage] = useState('English'); const [referenceImage, setReferenceImage] = useState(null); const [storyTitle, setStoryTitle] = useState(''); const [storyPages, setStoryPages] = useState([]); const [currentPage, setCurrentPage] = useState(0); const [error, setError] = useState(''); const [loadingMessage, setLoadingMessage] = useState(''); const [isPdfReady, setIsPdfReady] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const fileInputRef = useRef(null); const themes = [ "Magical Forest", "Space Adventure", "Underwater Kingdom", "Dinosaur World", "Candy Land", "Fairy Tale Castle" ]; const languages = ["English", "Hindi"]; // Load PDF Generation Libraries (jsPDF & html2canvas) useEffect(() => { const loadScript = (src) => new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); Promise.all([ loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'), loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js') ]).then(() => setIsPdfReady(true)).catch(err => console.error("Failed to load PDF libraries", err)); }, []); const handleImageUpload = (e) => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => setReferenceImage(reader.result); reader.readAsDataURL(file); } }; const analyzeUploadedPhoto = async (base64Data, mimeType) => { const prompt = `Analyze this photo and provide a detailed description of the child's appearance so it can be adapted into a recognizable cartoon character. Focus on: 1. Face: Eye shape/color, skin tone, prominent features (like eyelashes, kajal). 2. Hairstyle: Hair color, length, and styling. 3. Clothing: Describe the exact clothing in extreme detail (colors, large letters/logos, accessories like backpack straps). Make the description concise but comprehensive.`; const payload = { contents: [{ role: "user", parts: [{ text: prompt }, { inlineData: { mimeType, data: base64Data } }] }] }; const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`; const result = await fetchWithRetry(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); return result.candidates?.[0]?.content?.parts?.[0]?.text || "A child in detailed clothing."; }; const generateStoryAndImages = async () => { try { setStep('generating_story'); setError(''); const base64Data = referenceImage.split(',')[1]; const mimeType = referenceImage.split(';')[0].split(':')[1]; setLoadingMessage('Scanning photo for character details...'); const dynamicCharacterDescription = await analyzeUploadedPhoto(base64Data, mimeType); setLoadingMessage('Writing your magical cartoon adventure...'); const storyPrompt = `Write a 4-page children's story in ${language} about a kid named ${childName || 'the child'} who goes on an adventure in a ${theme}. Respond ONLY with a valid JSON object containing a title and pages array. Schema: { "title": "A catchy story title in ${language}", "pages": [ { "text": "The story text for this page in ${language} (2-3 short, engaging sentences).", "imagePrompt": "A brief visual description IN ENGLISH of ONLY the action and background (e.g., 'standing near a glowing tree'). Do NOT describe the character's appearance here." } ] }`; const payload = { contents: [{ parts: [{ text: storyPrompt }] }], systemInstruction: { parts: [{ text: "You are a creative children's book author. Always return valid JSON matching the exact schema." }] }, generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { title: { type: "STRING" }, pages: { type: "ARRAY", items: { type: "OBJECT", properties: { text: { type: "STRING" }, imagePrompt: { type: "STRING" } }, required: ["text", "imagePrompt"] } } }, required: ["title", "pages"] } } }; const storyUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`; const storyResult = await fetchWithRetry(storyUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const jsonText = storyResult.candidates?.[0]?.content?.parts?.[0]?.text; let parsedData; try { parsedData = JSON.parse(jsonText); } catch (e) { const match = jsonText.match(/\{[\s\S]*\}/); parsedData = match ? JSON.parse(match[0]) : { title: "Magical Adventure", pages: [] }; } if (!parsedData.pages || parsedData.pages.length === 0) throw new Error("Failed to generate story text."); setStoryTitle(parsedData.title); const initialPages = parsedData.pages.map(page => ({ ...page, imageUrl: null, isGeneratingImage: true, imageError: false })); setStoryPages(initialPages); setStep('reading'); setCurrentPage(0); generateImagesForPages(initialPages, dynamicCharacterDescription, base64Data, mimeType); } catch (err) { console.error(err); setError('Failed to generate the story. Please try again.'); setStep('input'); } }; const generateImagesForPages = async (pages, dynamicDescription, base64Data, mimeType) => { for (let i = 0; i < pages.length; i++) { try { // UPDATED: Strict instructions for a CARTOON style const imagePrompt = `CARTOON STORYBOOK ADAPTATION. You must recreate the child from the reference image into a highly appealing, vibrant 2D cartoon character. CHARACTER DETAILS (Adapt these features into a cute Disney/Pixar-like 2D style): ${dynamicDescription} ACTION/SCENE TO DRAW: ${pages[i].imagePrompt}. STYLE: Beautiful, vibrant 2D cartoon children's storybook illustration. Flat colors, soft shading, expressive cute cartoon proportions. DO NOT make it photorealistic. Make it a gorgeous, stylized 2D cartoon illustration, but ensure the clothing and key facial features from the reference are clearly recognizable in the cartoon adaptation.`; const payload = { contents: [{ parts: [ { text: imagePrompt }, { inlineData: { mimeType: mimeType, data: base64Data } } ] }], generationConfig: { responseModalities: ['IMAGE'] } }; const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`; const result = await fetchWithRetry(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const generatedBase64 = result.candidates?.[0]?.content?.parts?.find(p => p.inlineData)?.inlineData?.data; if (generatedBase64) { setStoryPages(prev => { const newPages = [...prev]; newPages[i].imageUrl = `data:image/jpeg;base64,${generatedBase64}`; newPages[i].isGeneratingImage = false; return newPages; }); } else { throw new Error("No image data returned"); } } catch (err) { console.error(`Failed to generate image for page ${i}:`, err); setStoryPages(prev => { const newPages = [...prev]; newPages[i].isGeneratingImage = false; newPages[i].imageError = true; return newPages; }); } } }; const handleDownloadPDF = async () => { if (!window.jspdf || !window.html2canvas) { alert("PDF libraries are still loading. Please try again in a moment."); return; } setIsDownloading(true); try { const { jsPDF } = window.jspdf; // Use standard A4 measurements const pdf = new jsPDF('p', 'mm', 'a4'); const pagesContainer = document.getElementById('pdf-export-container'); const pageElements = pagesContainer.children; for (let i = 0; i < pageElements.length; i++) { // html2canvas takes a snapshot of the hidden DOM nodes const canvas = await window.html2canvas(pageElements[i], { scale: 2, // High resolution useCORS: true, backgroundColor: '#ffffff' }); const imgData = canvas.toDataURL('image/jpeg', 0.8); if (i > 0) pdf.addPage(); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = (canvas.height * pdfWidth) / canvas.width; pdf.addImage(imgData, 'JPEG', 0, 0, pdfWidth, pdfHeight); } pdf.save(`${storyTitle.replace(/[^a-z0-9]/gi, '_').toLowerCase() || 'storybook'}.pdf`); } catch (err) { console.error("PDF Generation Error:", err); alert("Failed to generate PDF. Make sure all images have finished loading."); } finally { setIsDownloading(false); } }; const resetApp = () => { setStep('input'); setChildName(''); setReferenceImage(null); setStoryPages([]); setStoryTitle(''); setCurrentPage(0); }; // Check if all pages are ready for PDF download const isBookComplete = storyPages.length > 0 && storyPages.every(p => !p.isGeneratingImage && !p.imageError); return (
{/* Hidden Container for PDF Export (Used by html2canvas) */} {step === 'reading' && isBookComplete && (
{storyPages.map((page, index) => (

{storyTitle}

Story

{page.text}

Page {index + 1} {index === storyPages.length - 1 && (

made with kahaaniAI

'Brandspro Magic Story Book Creator'

)}
))}
)} {/* Main UI Header */}

Cartoon Storybook

{step === 'reading' && (
{isBookComplete && ( )}
)}
{step === 'input' && (

Cartoon Magic!

Turn a photo into a gorgeous cartoon storybook.

{error && (

{error}

)}
setChildName(e.target.value)} placeholder="e.g. Aarohi" className="w-full px-4 py-3 rounded-xl border border-slate-200 focus:ring-2 focus:ring-purple-500 focus:border-purple-500 outline-none transition-all text-lg" />
fileInputRef.current?.click()} className={`border-2 border-dashed rounded-2xl p-8 text-center cursor-pointer transition-all ${referenceImage ? 'border-purple-500 bg-purple-50' : 'border-slate-300 hover:border-purple-400 hover:bg-slate-50'}`} > {referenceImage ? (
Reference

Photo Selected

) : (

Click to upload photo

Clear, front-facing face works best.

)}