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 ? (
) : (
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) */}

{/* Overlay Image (Original) with Clip Path */}

{/* 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 */}
{/* Left Column: Inputs */}
{/* User Photo Panel */}
{/* Clothing Panel */}
{/* Toggle Input Mode */}
{clothingInputMode === "upload" ? (
setClothingImage(null)}
/>
) : (
{clothingImage && (
)}
)}
{/* Advanced Options */}
{/* Action Button */}
{errorMsg && (
)}
{/* Right Column: Output Gallery */}
Result Canvas
{generatedImage && (
)}
{/* Display Area */}
{/* Subtle Grid Pattern */}
{/* Loading Overlay */}
{isGenerating && (
{/* Context Background */}
{userImage &&

}
{/* Scanner Laser */}
{/* Content */}
Tailoring your fit...
{loadingStep}
)}
{/* Content */}
{!generatedImage && !isGenerating ? (
Studio Canvas
Upload your photo and garment on the left, then ignite the magic.
) : generatedImage && !isGenerating ? (
viewMode === "result" ? (

) : (
)
) : null}
{/* Session History Footer Gallery */}
{sessionHistory.length > 0 && (
Session History
{sessionHistory.map((item) => (
{
setUserImage(item.userImg);
setClothingImage(item.clothingImg);
setGeneratedImage(item.resultImg);
setViewMode("result");
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
className="relative shrink-0 w-36 h-48 rounded-2xl overflow-hidden cursor-pointer group shadow-md hover:shadow-xl border-2 border-white/50 dark:border-slate-700 snap-start transition-all hover:-translate-y-2"
>
{/* Generated Image Thumbnail */}

{/* Overlay overlay of original garment */}
{/* Hover State overlay */}
))}
)}
{/* Tailwind Custom Utilities (normally in css) */}
);
}