import React, { useState, useRef, useEffect, useCallback } from 'react'; import { UploadCloud, Link as LinkIcon, Sparkles, RefreshCcw, Download, Image as ImageIcon, CheckCircle, AlertCircle, ChevronRight, ChevronLeft, MoveHorizontal, Trash2 } from 'lucide-react'; // --- Configuration --- const apiKey = ""; // API key is injected by the execution environment const MODEL_NAME = "gemini-2.5-flash-image-preview"; // --- Utility Functions --- // Client-side image compression using Canvas const compressImage = (fileOrUrl, isUrl = false) => { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "Anonymous"; // Crucial for fetching URLs img.onload = () => { const canvas = document.createElement('canvas'); const MAX_WIDTH = 1024; const MAX_HEIGHT = 1024; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_WIDTH) { height *= MAX_WIDTH / width; width = MAX_WIDTH; } } else { if (height > MAX_HEIGHT) { width *= MAX_HEIGHT / height; height = MAX_HEIGHT; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); resolve(canvas.toDataURL('image/jpeg', 0.85)); // 85% quality JPEG }; img.onerror = (err) => reject(new Error("Failed to load image for compression.")); if (isUrl) { img.src = fileOrUrl; } else { const reader = new FileReader(); reader.onload = (e) => { img.src = e.target.result; }; reader.onerror = (err) => reject(err); reader.readAsDataURL(fileOrUrl); } }); }; const getBase64DataAndMime = (dataUrl) => { const [prefix, data] = dataUrl.split(','); const mimeType = prefix.split(';')[0].split(':')[1]; return { mimeType, data }; }; const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // --- Components --- // Drag and Drop Uploader Component const ImageUploader = ({ label, hint, image, onImageChange, onClear }) => { const [isDragging, setIsDragging] = useState(false); const fileInputRef = useRef(null); const handleDrag = (e) => { e.preventDefault(); e.stopPropagation(); if (e.type === "dragenter" || e.type === "dragover") setIsDragging(true); else if (e.type === "dragleave") setIsDragging(false); }; const handleDrop = async (e) => { e.preventDefault(); e.stopPropagation(); setIsDragging(false); if (e.dataTransfer.files && e.dataTransfer.files[0]) { handleFile(e.dataTransfer.files[0]); } }; const handleFile = async (file) => { if (!file.type.startsWith('image/')) return; try { const compressedDataUrl = await compressImage(file); onImageChange(compressedDataUrl); } catch (err) { console.error("Compression failed", err); } }; return (
{hint && {hint}}
{image ? (
{label}
) : (
fileInputRef.current?.click()} >

{isDragging ? "Drop to upload" : "Click or drag your photo"}

High-res JPEG or PNG

e.target.files?.[0] && handleFile(e.target.files[0])} />
)}
); }; // Interactive Before/After Slider Component const CompareSlider = ({ original, generated }) => { const [sliderPosition, setSliderPosition] = useState(50); const [isDragging, setIsDragging] = useState(false); const containerRef = useRef(null); const handleMove = useCallback((clientX) => { if (!containerRef.current || !isDragging) return; const rect = containerRef.current.getBoundingClientRect(); const x = Math.max(0, Math.min(clientX - rect.left, rect.width)); const percentage = (x / rect.width) * 100; setSliderPosition(percentage); }, [isDragging]); const onMouseMove = (e) => handleMove(e.clientX); const onTouchMove = (e) => handleMove(e.touches[0].clientX); useEffect(() => { if (isDragging) { window.addEventListener('mousemove', onMouseMove); window.addEventListener('touchmove', onTouchMove); window.addEventListener('mouseup', () => setIsDragging(false)); window.addEventListener('touchend', () => setIsDragging(false)); } return () => { window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('touchmove', onTouchMove); window.removeEventListener('mouseup', () => setIsDragging(false)); window.removeEventListener('touchend', () => setIsDragging(false)); }; }, [isDragging, handleMove]); return (
{ setIsDragging(true); handleMove(e.clientX); }} onTouchStart={(e) => { setIsDragging(true); handleMove(e.touches[0].clientX); }} > {/* Base Image (Generated) */} Generated result {/* Overlay Image (Original) with Clip Path */} Original model {/* Slider Handle */}
Original
Generated
); }; // Main Application Component export default function App() { // --- State --- const [userImage, setUserImage] = useState(null); const [clothingImage, setClothingImage] = useState(null); const [clothingUrl, setClothingUrl] = useState(""); const [clothingInputMode, setClothingInputMode] = useState("upload"); // 'upload' or 'url' const [instructions, setInstructions] = useState(""); const [isGenerating, setIsGenerating] = useState(false); const [loadingStep, setLoadingStep] = useState(""); const [errorMsg, setErrorMsg] = useState(""); const [generatedImage, setGeneratedImage] = useState(null); const [viewMode, setViewMode] = useState("result"); // 'result' or 'compare' const [sessionHistory, setSessionHistory] = useState([]); // --- Handlers --- const handleUrlFetch = async () => { if (!clothingUrl) return; setLoadingStep("Fetching image from URL..."); setIsGenerating(true); setErrorMsg(""); try { const dataUrl = await compressImage(clothingUrl, true); setClothingImage(dataUrl); } catch (err) { setErrorMsg("Failed to fetch image. The site might block direct access (CORS). Try downloading and uploading it manually."); } finally { setIsGenerating(false); setLoadingStep(""); } }; const handleReset = () => { if (window.confirm("Clear all inputs and start over?")) { setUserImage(null); setClothingImage(null); setClothingUrl(""); setInstructions(""); setGeneratedImage(null); setErrorMsg(""); } }; const downloadImage = () => { if (!generatedImage) return; const a = document.createElement("a"); a.href = generatedImage; a.download = `tishka-tryon-${Date.now()}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); }; // --- Core API Logic --- const generateTryOn = async () => { if (!userImage || !clothingImage) { setErrorMsg("Please provide both a user photo and a clothing item."); return; } setIsGenerating(true); setErrorMsg(""); setGeneratedImage(null); setViewMode("result"); try { setLoadingStep("Preparing image payload..."); const userImgData = getBase64DataAndMime(userImage); const clothingImgData = getBase64DataAndMime(clothingImage); const systemPrompt = `You are a world-class AI fashion stylist and photorealistic image editor. Task: Realistically dress the person in the provided user photo (Image 1) with the provided clothing item (Image 2). Requirements: 1. Seamlessly integrate the garment onto the user's body. 2. Respect the original body pose, lighting, skin tone, and background. 3. Adapt the clothing to natural folds and shadows based on the pose. 4. CRUCIAL: Ensure the generated image contains NO watermarks, logos, text overlays, or any other identifying marks. User specific instructions: ${instructions || "Make it look as natural and photorealistic as possible."}`; const payload = { contents: [ { parts: [ { text: systemPrompt }, { inlineData: { mimeType: userImgData.mimeType, data: userImgData.data } }, { inlineData: { mimeType: clothingImgData.mimeType, data: clothingImgData.data } } ] } ], generationConfig: { responseModalities: ["IMAGE"] } }; setLoadingStep("AI is rendering your virtual try-on (this may take up to 20s)..."); const MAX_RETRIES = 3; const BASE_DELAY = 1000; let resultData = null; for (let i = 0; i <= MAX_RETRIES; i++) { try { const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/${MODEL_NAME}:generateContent?key=${apiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) } ); if (!response.ok) { const errData = await response.json(); throw new Error(errData.error?.message || `API Error: ${response.status}`); } const result = await response.json(); const candidateParts = result.candidates?.[0]?.content?.parts; const imagePart = candidateParts?.find(p => p.inlineData); if (imagePart && imagePart.inlineData) { resultData = `data:${imagePart.inlineData.mimeType};base64,${imagePart.inlineData.data}`; break; // Success, exit retry loop } else { throw new Error("No image data returned from API."); } } catch (err) { if (i === MAX_RETRIES) throw err; setLoadingStep(`Network issue, retrying attempt ${i + 1}/${MAX_RETRIES}...`); await delay(BASE_DELAY * Math.pow(2, i)); } } setLoadingStep("Finalizing masterpiece..."); await delay(500); // UI polish setGeneratedImage(resultData); // Save to history setSessionHistory(prev => [{ id: Date.now(), userImg: userImage, clothingImg: clothingImage, resultImg: resultData }, ...prev]); } catch (err) { console.error(err); setErrorMsg(`Generation failed: ${err.message}. Please try again.`); } finally { setIsGenerating(false); setLoadingStep(""); } }; return (
{/* Ambient Background Glows */}
{/* Header - Floating Glassmorphism */}

Tishka Studio

{/* Left Column: Inputs */}
{/* User Photo Panel */}
1

Your Photo

setUserImage(null)} />
{/* Clothing Panel */}
2

Garment

{/* Toggle Input Mode */}
{clothingInputMode === "upload" ? ( setClothingImage(null)} /> ) : (
setClothingUrl(e.target.value)} className="w-full pl-10 pr-4 py-3 bg-white dark:bg-slate-900/50 border border-slate-200 dark:border-slate-700 rounded-xl focus:ring-4 focus:ring-indigo-500/20 focus:border-indigo-500 dark:focus:border-indigo-500 outline-none transition-all text-sm shadow-sm" />
{clothingImage && (
Fetched garment
)}
)}
{/* Advanced Options */}

Styling Notes (Optional)