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 && (
{step === 'reading' && (
{step === 'input' && (
{storyPages.map((page, index) => (
))}
)}
{/* Main UI Header */}
{storyTitle}
{page.text}
Page {index + 1}
{index === storyPages.length - 1 && (
)}
made with kahaaniAI
'Brandspro Magic Story Book Creator'
Cartoon Storybook
{isBookComplete && (
)}
)}
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 ? (
) : (
)}
Click to upload photo
Clear, front-facing face works best.