import React, { useState } from 'react'; import { generateAvatarImage } from '../services/geminiService'; import { analyzeAvatarImage } from '../services/visionService'; import { stitchAssets } from '../services/imageService'; import LoadingSpinner from './LoadingSpinner'; import { Rect } from '../types'; interface AvatarCreatorProps { onAvatarGenerated: (url: string, name: string, initialData?: { leftEye?: Rect, rightEye?: Rect, mouth?: Rect, skinColor?: string, mainBody?: Rect, textureClosedEye?: Rect, textureOpenMouth?: Rect }) => void; } const AvatarCreator: React.FC = ({ onAvatarGenerated }) => { const [mode, setMode] = useState<'generate' | 'upload'>('generate'); // Generation State const [prompt, setPrompt] = useState(''); const [name, setName] = useState(''); const [status, setStatus] = useState<'idle' | 'generating' | 'analyzing' | 'stitching'>('idle'); const [error, setError] = useState(null); // Upload State const [baseFile, setBaseFile] = useState(null); const [blinkFile, setBlinkFile] = useState(null); const [talkFile, setTalkFile] = useState(null); const handleGenerate = async () => { if (!prompt || !name) return; setStatus('generating'); setError(null); try { // 1. Generate Image (Now creates a character sheet) const imageUrl = await generateAvatarImage(prompt); // 2. Analyze Image for Landmarks setStatus('analyzing'); const analysisData = await analyzeAvatarImage(imageUrl); // 3. Pass to parent if (analysisData) { onAvatarGenerated(imageUrl, name, analysisData); } else { onAvatarGenerated(imageUrl, name); } } catch (err) { console.error(err); setError("Failed to generate avatar. Please try again."); } finally { setStatus('idle'); } }; const handleUpload = async () => { if (!baseFile || !name) return; setStatus('stitching'); setError(null); try { // 1. Stitch Assets into Sheet const { imageUrl, mainBody, textureClosedEye, textureOpenMouth } = await stitchAssets(baseFile, blinkFile || undefined, talkFile || undefined); // 2. Analyze the Main Body part of the image // Note: analyzeAvatarImage analyzes the whole image, but since we put the face on the left (or full image), // it should find the face correctly. setStatus('analyzing'); const analysisData = await analyzeAvatarImage(imageUrl); // 3. Combine manual stitch data with automatic vision data const initialData = { ...(analysisData || {}), mainBody, textureClosedEye, textureOpenMouth }; onAvatarGenerated(imageUrl, name, initialData); } catch (err) { console.error(err); setError("Failed to process uploaded images. Please ensure they are valid image files."); } finally { setStatus('idle'); } }; const handleFileChange = (e: React.ChangeEvent, setter: (f: File | null) => void) => { if (e.target.files && e.target.files[0]) { setter(e.target.files[0]); } }; return (
{/* Tabs */}

{mode === 'generate' ? 'Design Your Avatar' : 'Import Your Model'}

{mode === 'generate' ? 'Describe your dream VTuber model. Gemini will generate a character sheet with expression assets.' : 'Upload your existing character art. We support separate files for blink and talk variants.' }

setName(e.target.value)} placeholder="e.g., Neon Kitsune" className="w-full bg-slate-900/50 border border-slate-600 rounded-xl px-4 py-3 text-white placeholder-slate-500 focus:ring-2 focus:ring-cyan-500 focus:border-transparent transition-all outline-none" />
{mode === 'generate' ? (