import React, { useState } from 'react'; import { analyzeAvatarImage } from '../services/visionService'; import { stitchAssets, fileToDataUrl } from '../services/imageService'; import { generateAvatarImage } from '../services/geminiService'; import LoadingSpinner from './LoadingSpinner'; import { Rect } from '../../shared/types'; const placeholderGenerate = async (prompt: string) => { const text = encodeURIComponent((prompt || '').substring(0, 80)); return `data:image/svg+xml;utf8,Placeholder: ${text}`; }; 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'); const [prompt, setPrompt] = useState(''); const [name, setName] = useState(''); const [status, setStatus] = useState<'idle' | 'generating' | 'analyzing' | 'stitching'>('idle'); const [error, setError] = useState(null); 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 { const imageUrl = await generateAvatarImage(prompt); setStatus('analyzing'); const analysisData = await analyzeAvatarImage(imageUrl); 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 { const baseDataUrl = await fileToDataUrl(baseFile); const baseAnalysis = await analyzeAvatarImage(baseDataUrl); let blinkDataUrl, blinkAnalysis; if (blinkFile) { blinkDataUrl = await fileToDataUrl(blinkFile); blinkAnalysis = await analyzeAvatarImage(blinkDataUrl); } let talkDataUrl, talkAnalysis; if (talkFile) { talkDataUrl = await fileToDataUrl(talkFile); talkAnalysis = await analyzeAvatarImage(talkDataUrl); } const { imageUrl, mainBody, textureClosedEye: stitchBlinkRect, textureOpenMouth: stitchTalkRect } = await stitchAssets(baseDataUrl, blinkDataUrl, talkDataUrl); const mapRect = (r: Rect, container: Rect) => ({ x: container.x + r.x * container.w, y: container.y + r.y * container.h, w: r.w * container.w, h: r.h * container.h }); let initialData: any = { mainBody, textureClosedEye: stitchBlinkRect, textureOpenMouth: stitchTalkRect }; if (baseAnalysis) { initialData.leftEye = mapRect(baseAnalysis.leftEye, mainBody); initialData.rightEye = mapRect(baseAnalysis.rightEye, mainBody); initialData.mouth = mapRect(baseAnalysis.mouth, mainBody); initialData.skinColor = baseAnalysis.skinColor; } if (blinkAnalysis && stitchBlinkRect) { const be = blinkAnalysis; const minX = Math.min(be.leftEye.x, be.rightEye.x); const minY = Math.min(be.leftEye.y, be.rightEye.y); const maxX = Math.max(be.leftEye.x + be.leftEye.w, be.rightEye.x + be.rightEye.w); const maxY = Math.max(be.leftEye.y + be.leftEye.h, be.rightEye.y + be.rightEye.h); const eyesRect = { x: minX, y: minY, w: maxX - minX, h: maxY - minY }; initialData.textureClosedEye = mapRect(eyesRect, stitchBlinkRect); } if (talkAnalysis && stitchTalkRect) { initialData.textureOpenMouth = mapRect(talkAnalysis.mouth, stitchTalkRect); } 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 (

{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' ? (