Vitron-Front/app/components/AIPromoteModal.tsx
2026-04-29 01:44:16 +03:30

586 lines
21 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useRef, useEffect } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogOverlay,
} from "./ui/dialog";
import { Button } from "./ui/button";
import {
Sparkles,
Upload,
ImageIcon,
X,
Download,
RotateCcw,
CheckCircle,
Share2,
} from "lucide-react";
import { useRootData, useAuthToken } from "../hooks/use-root-data";
import { LoginRequiredDialog } from "./LoginRequiredDialog";
import { useMutation } from "@tanstack/react-query";
import { getApiBaseUrl } from "~/utils/api-config";
interface AIPromoteModalProps {
isOpen: boolean;
onClose: () => void;
collectionId?: string;
productId?: string;
}
export default function AIPromoteModal({
isOpen,
onClose,
collectionId,
productId,
}: AIPromoteModalProps) {
const isProductMode = !!productId;
const { user } = useRootData();
const token = useAuthToken();
const [selectedImage, setSelectedImage] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const [resultImage, setResultImage] = useState<string | null>(null);
const [isUploading, setIsUploading] = useState(false);
const [isLoginDialogOpen, setIsLoginDialogOpen] = useState(false);
const [loadingStep, setLoadingStep] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(null);
// Loading steps messages
const loadingSteps = [
{ text: "در حال آماده‌سازی لباس...", icon: "👗" },
{ text: "در حال اندازه‌گیری سایز شما...", icon: "📏" },
{ text: "در حال دوختن لباس به اندازه شما...", icon: "✂️" },
{ text: "در حال اتو کردن و مرتب کردن...", icon: "✨" },
{ text: "در حال پرو کردن روی شما...", icon: "🎯" },
{ text: "تقریباً آماده شده یخورده هم صبر کن...", icon: "⏳" },
];
// Cycle through loading steps
useEffect(() => {
if (!isUploading) {
setLoadingStep(0);
return;
}
const interval = setInterval(() => {
setLoadingStep((prev) => {
// Stay on last step if we've gone through all
if (prev >= loadingSteps.length - 1) return prev;
return prev + 1;
});
}, 4000);
return () => clearInterval(interval);
}, [isUploading, loadingSteps.length]);
// AI Try-on mutation for collection
const tryOnCollectionMutation = useMutation({
mutationFn: async ({
image,
collectionId,
}: {
image: Blob;
collectionId: string;
}) => {
if (!token) throw new Error("Authentication required");
const baseUrl = getApiBaseUrl();
const formData = new FormData();
formData.append("collection_id", collectionId);
formData.append("image", image);
const response = await fetch(`${baseUrl}/api/aifit/v1/try-on/`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
});
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
// Response is binary PNG, convert to blob URL
const blob = await response.blob();
return URL.createObjectURL(blob);
},
});
// AI Try-on mutation for product
const tryOnProductMutation = useMutation({
mutationFn: async ({
image,
productId,
}: {
image: Blob;
productId: string;
}) => {
if (!token) throw new Error("Authentication required");
const baseUrl = getApiBaseUrl();
const formData = new FormData();
formData.append("product_id", productId);
formData.append("image", image);
// AI processing can take a while, set 2 minute timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000);
try {
const response = await fetch(
`${baseUrl}/api/aifit/v1/try-on/product/`,
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
body: formData,
signal: controller.signal,
}
);
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text().catch(() => "");
console.error("API error response:", errorText);
throw new Error(`API error: ${response.status}`);
}
// Response is binary PNG, convert to blob URL
const blob = await response.blob();
console.log("Received blob:", blob.type, blob.size);
return URL.createObjectURL(blob);
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
throw new Error(
"درخواست زمان زیادی طول کشید. لطفا دوباره تلاش کنید."
);
}
throw error;
}
},
});
// Image Upload Handler
const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
// Validate file type
if (!file.type.startsWith("image/")) {
alert("لطفا یک فایل تصویری انتخاب کنید");
return;
}
// Validate file size (max 10MB)
if (file.size > 10 * 1024 * 1024) {
alert("حجم فایل نباید بیشتر از 10 مگابایت باشد");
return;
}
setSelectedImage(file);
// Create preview
const reader = new FileReader();
reader.onloadend = () => {
setImagePreview(reader.result as string);
};
reader.readAsDataURL(file);
}
};
// Handle login required - close AI modal first, then show login
const handleLoginRequired = () => {
onClose(); // Close AI modal first
// Small delay to let the modal close animation complete
setTimeout(() => {
setIsLoginDialogOpen(true);
}, 150);
};
// Handle AI Promote Submit
const handleAIPromote = async () => {
// Check if user is logged in
if (!user) {
handleLoginRequired();
return;
}
if (!selectedImage) {
alert("لطفا ابتدا عکس خود را آپلود کنید");
return;
}
setIsUploading(true);
try {
console.log("Sending AI try-on request...");
let result;
if (isProductMode && productId) {
// Product try-on
result = await tryOnProductMutation.mutateAsync({
image: selectedImage,
productId,
});
} else if (collectionId) {
// Collection try-on
result = await tryOnCollectionMutation.mutateAsync({
image: selectedImage,
collectionId,
});
} else {
throw new Error("No product or collection ID provided");
}
console.log("AI try-on completed successfully", result);
// Result is already a blob URL from the mutation
setResultImage(result);
// Reset state
// handleModalClose();
} catch (error) {
console.error("Error in AI try-on:", error);
alert("متاسفانه خطایی رخ داد. لطفا دوباره تلاش کنید.");
} finally {
setIsUploading(false);
}
};
// Reset modal state when closed
const handleModalClose = () => {
setSelectedImage(null);
setImagePreview(null);
setResultImage(null);
setIsUploading(false);
onClose();
};
// Reset to try again
const handleTryAgain = () => {
setResultImage(null);
setSelectedImage(null);
setImagePreview(null);
};
// Download result image
const handleDownload = () => {
if (!resultImage) return;
const link = document.createElement("a");
link.href = resultImage;
link.download = `ai-tryon-${Date.now()}.png`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// Share result image
const handleShare = async () => {
if (!resultImage) return;
try {
// Convert blob URL to actual blob for sharing
const response = await fetch(resultImage);
const blob = await response.blob();
const file = new File([blob], `ai-tryon-${Date.now()}.png`, {
type: "image/png",
});
// Build product URL for sharing
const siteUrl =
typeof window !== "undefined"
? window.location.origin
: "https://vitrown.com";
const productUrl = productId
? `${siteUrl}/product/${productId}`
: siteUrl;
const shareText = `ببین این لباس رو تن من چطور شده! 👗✨\n\nاین محصول رو از ویترون ببین:\n${productUrl}`;
// Check if Web Share API is available and supports sharing files
if (navigator.share && navigator.canShare?.({ files: [file] })) {
await navigator.share({
title: "پرو کردن با هوش مصنوعی - ویترون",
text: shareText,
files: [file],
});
} else {
// Fallback: Copy image to clipboard or show message
try {
await navigator.clipboard.write([
new ClipboardItem({ "image/png": blob }),
]);
alert(
"تصویر در کلیپ‌بورد کپی شد! حالا میتونی توی هر اپلیکیشنی Paste کنی."
);
} catch {
// If clipboard also fails, just download
handleDownload();
alert("تصویر دانلود شد. میتونی از گالری به اشتراک بذاری.");
}
}
} catch (error) {
console.error("Error sharing:", error);
}
};
return (
<>
{/* Login Required Dialog - outside of main dialog to avoid overlap */}
<LoginRequiredDialog
isOpen={isLoginDialogOpen}
onOpenChange={setIsLoginDialogOpen}
loginTitle="برای استفاده از قابلیت پرو کردن با AI ابتدا باید وارد حساب کاربری خود شوید."
/>
<Dialog open={isOpen} onOpenChange={handleModalClose}>
<DialogOverlay />
<DialogContent className="bg-WHITE rounded-2xl max-w-md w-[calc(100%-32px)] sm:w-full p-4 border border-inner-border">
{/* Close Button */}
<button
onClick={handleModalClose}
disabled={isUploading}
className="absolute top-4 right-4 text-GRAY hover:text-BLACK transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
aria-label="بستن"
>
<X className="w-6 h-6" />
</button>
<DialogHeader>
<DialogTitle className="text-B16 mt-8 font-bold text-right flex items-center gap-2 pl-8">
<span className="w-full">
{isProductMode
? "پرو کردن محصول با هوش مصنوعی"
: "پرو کردن کالکشن با هوش مصنوعی"}
</span>
<Sparkles className="w-6 h-6 text-BLACK" />
</DialogTitle>
<DialogDescription className="text-R14 text-GRAY text-right">
{isProductMode
? "برای اینکه AI بتواند این محصول رو روی بدن شما نمایش بده، لطفا یک عکس قدی از خود را آپلود کنید."
: "برای اینکه AI بتواد این کالکشن رو روی بدن شما نمایش بده، لطفا یک عکس قدی از خود را آپلود کنید."}
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-4 mt-4">
{resultImage ? (
// Result View
<div className="flex flex-col gap-4">
{/* Success Badge */}
<div className="flex items-center justify-center gap-2 p-3 bg-GREEN/10 rounded-xl border border-GREEN/30">
<CheckCircle className="w-5 h-5 text-GREEN" />
<p className="text-B14 font-bold text-GREEN">
پرو کردن با موفقیت انجام شد!
</p>
</div>
{/* Result Image */}
<div className="relative w-full h-80 rounded-xl overflow-hidden bg-WHITE2 border-2 border-GREEN/30 shadow-lg">
<img
src={resultImage}
alt="AI Try-on Result"
className="w-full h-full object-contain"
/>
</div>
{/* Action Buttons */}
<div className="flex flex-col gap-3">
{/* Share Button - Full Width */}
<Button
variant="dark"
size="lg"
onClick={handleShare}
className="w-full"
>
<div className="flex items-center gap-2">
<Share2 className="w-4 h-4" />
<span>ارسال به دوستان</span>
</div>
</Button>
{/* Download & Try Again */}
<div className="flex gap-3">
<Button
variant="outline"
size="lg"
onClick={handleTryAgain}
className="flex-1"
>
<div className="flex items-center gap-2">
<RotateCcw className="w-4 h-4" />
<span>امتحان مجدد</span>
</div>
</Button>
<Button
variant="outline"
size="lg"
onClick={handleDownload}
className="flex-1"
>
<div className="flex items-center gap-2">
<Download className="w-4 h-4" />
<span>دانلود</span>
</div>
</Button>
</div>
</div>
</div>
) : (
// Upload View
<>
{/* Image Upload Area */}
<div className="flex flex-col gap-3">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageSelect}
className="hidden"
id="ai-image-upload"
/>
{imagePreview ? (
// Image Preview
<div className="relative w-full h-64 rounded-xl overflow-hidden bg-WHITE2 border-2 border-BLACK/10">
<img
src={imagePreview}
alt="Preview"
className="w-full h-full object-contain"
/>
<button
onClick={() => {
setSelectedImage(null);
setImagePreview(null);
}}
className="absolute top-2 right-2 bg-RED text-WHITE rounded-full p-2 hover:bg-RED/80 transition-colors shadow-lg"
aria-label="حذف عکس"
>
<X className="w-4 h-4" />
</button>
</div>
) : (
// Upload Button
<label
htmlFor="ai-image-upload"
className="w-full h-64 rounded-xl border-2 border-dashed border-GRAY/30
hover:border-BLACK transition-colors cursor-pointer
flex flex-col items-center justify-center gap-3 bg-WHITE2 hover:bg-WHITE3"
>
<div className="w-16 h-16 rounded-full bg-BLACK/5 flex items-center justify-center">
<Upload className="w-8 h-8 text-BLACK" />
</div>
<div className="flex flex-col items-center gap-1">
<p className="text-B16 font-bold text-BLACK">
آپلود عکس
</p>
<p className="text-R12 text-GRAY text-center px-4">
عکس باید تمام قد باشه تا AI بتونه بهتر کالکشن رو روی
بدن شما نمایش بده
</p>
<p className="text-R10 text-GRAY mt-2">
حداکثر حجم: 10MB
</p>
</div>
</label>
)}
</div>
{/* Action Button or Loading State */}
{isUploading ? (
// Loading State - replaces button and info boxes
<div className="flex flex-col items-center gap-4 py-6 px-4 bg-gradient-to-b from-BLACK/5 to-BLACK/10 rounded-xl border border-BLACK/10">
{/* Animated Icon */}
<div className="text-5xl animate-bounce">
{loadingSteps[loadingStep].icon}
</div>
{/* Loading Text */}
<p className="text-B16 font-bold text-BLACK text-center">
{loadingSteps[loadingStep].text}
</p>
{/* Progress Dots */}
<div className="flex items-center gap-2">
{loadingSteps.map((_, index) => (
<div
key={index}
className={`w-2.5 h-2.5 rounded-full transition-all duration-300 ${
index <= loadingStep
? "bg-BLACK scale-100"
: "bg-BLACK/20 scale-75"
}`}
/>
))}
</div>
{/* Step Counter */}
<p className="text-R12 text-GRAY">
مرحله {loadingStep + 1} از {loadingSteps.length}
</p>
</div>
) : (
// Normal State - Button and Info boxes
<>
<Button
variant="dark"
size="xl"
onClick={handleAIPromote}
disabled={!selectedImage}
className="w-full"
>
<div className="flex items-center gap-2">
<Sparkles className="w-4 h-4" />
<span>شروع پرو کردن</span>
</div>
</Button>
{/* Info Box */}
<div className="flex items-start gap-2 p-3 bg-BLUE/5 rounded-lg border border-BLUE/20">
<ImageIcon className="w-5 h-5 text-BLUE mt-0.5 flex-shrink-0" />
<p className="text-R12 text-GRAY text-right">
<span className="font-bold text-BLACK">نکته:</span> برای
بهترین نتیجه، عکس باید در نورپردازی مناسب و با زمینه
ساده باشه. AI با استفاده از این عکس،{" "}
{isProductMode ? "این محصول" : "محصولات کالکشن"} رو روی
بدن شما نمایش میده.
</p>
</div>
{/* Privacy Notice */}
<div className="flex items-center justify-center gap-1.5 p-2 bg-GREEN/5 rounded-lg border border-GREEN/20">
<svg
className="w-4 h-4 text-GREEN flex-shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
/>
</svg>
<p className="text-R11 text-GRAY text-center">
<span className="font-bold text-GREEN">
حریم خصوصی شما مهمه:
</span>{" "}
عکسهای شما هیچجا ذخیره نمیشه
</p>
</div>
</>
)}
</>
)}
</div>
</DialogContent>
</Dialog>
</>
);
}