feat: Add Clean Image (AI) button to pre-process images before Meshy 3D generation
Build and Deploy / build-and-push (push) Successful in 1m11s

This commit is contained in:
AI Bot
2026-09-30 16:29:16 +05:30
parent aa5de9f197
commit 0623f30837
+35 -2
View File
@@ -5,6 +5,7 @@ import { fal } from '@fal-ai/client';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { useAuthStore } from '../store/authStore'; import { useAuthStore } from '../store/authStore';
import { api } from '../lib/api'; import { api } from '../lib/api';
import { editUVLayer } from '../lib/api/apiUtils';
import { Canvas } from '@react-three/fiber'; import { Canvas } from '@react-three/fiber';
import { OrbitControls, Stage } from '@react-three/drei'; import { OrbitControls, Stage } from '@react-three/drei';
import { GLTFLoader } from 'three-stdlib'; import { GLTFLoader } from 'three-stdlib';
@@ -46,7 +47,7 @@ const ModelPreview = ({ url }: { url: string }) => {
}; };
export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose, onPushToApp }) => { export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose, onPushToApp }) => {
const { profile, setProfile } = useAuthStore(); const { profile, setProfile, token } = useAuthStore();
const [apiProvider, setApiProvider] = useState<'fal' | 'tripo' | 'meshy'>('fal'); const [apiProvider, setApiProvider] = useState<'fal' | 'tripo' | 'meshy'>('fal');
const [apiKey, setApiKey] = useState(''); const [apiKey, setApiKey] = useState('');
const [tripoApiKey, setTripoApiKey] = useState(''); const [tripoApiKey, setTripoApiKey] = useState('');
@@ -72,6 +73,7 @@ export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose,
const [progressMessage, setProgressMessage] = useState(''); const [progressMessage, setProgressMessage] = useState('');
const [generatedModelUrl, setGeneratedModelUrl] = useState<string | null>(null); const [generatedModelUrl, setGeneratedModelUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [isCleaningImage, setIsCleaningImage] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [isValidatingKey, setIsValidatingKey] = useState(false); const [isValidatingKey, setIsValidatingKey] = useState(false);
const [keyValidationStatus, setKeyValidationStatus] = useState<'idle' | 'valid' | 'invalid' | 'error'>('idle'); const [keyValidationStatus, setKeyValidationStatus] = useState<'idle' | 'valid' | 'invalid' | 'error'>('idle');
@@ -137,6 +139,29 @@ export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose,
} }
}; };
const handleCleanImage = async (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (!imageFile || !token) return;
setIsCleaningImage(true);
try {
const base64 = await fileToDataUrl(imageFile);
const newUrl = await editUVLayer("Remove all text, labels, and logos from this object, leaving a clean plain surface of the exact same shape.", base64, token);
const res = await fetch(newUrl);
const blob = await res.blob();
const newFile = new File([blob], "cleaned-image.png", { type: "image/png" });
setImageFile(newFile);
setImagePreview(newUrl);
} catch (err: any) {
alert("Failed to clean image: " + err.message);
} finally {
setIsCleaningImage(false);
}
};
// Utility to convert file to data URL for Fal API if needed // Utility to convert file to data URL for Fal API if needed
const fileToDataUrl = (file: File): Promise<string> => { const fileToDataUrl = (file: File): Promise<string> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -453,8 +478,16 @@ export const ImageTo3DModal: React.FC<ImageTo3DModalProps> = ({ isOpen, onClose,
{imagePreview ? ( {imagePreview ? (
<> <>
<img src={imagePreview} alt="Preview" className="absolute inset-0 w-full h-full object-contain p-2" /> <img src={imagePreview} alt="Preview" className="absolute inset-0 w-full h-full object-contain p-2" />
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity"> <div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 flex flex-col items-center justify-center gap-3 transition-opacity">
<span className="text-white text-xs font-medium flex items-center gap-2"><Upload size={14}/> Change Image</span> <span className="text-white text-xs font-medium flex items-center gap-2"><Upload size={14}/> Change Image</span>
<button
onClick={handleCleanImage}
disabled={isCleaningImage}
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white rounded text-xs font-bold flex items-center gap-2 transition-colors shadow-lg shadow-black/50"
>
{isCleaningImage ? <Loader2 size={12} className="animate-spin" /> : null}
Clean Image (AI)
</button>
</div> </div>
<button <button
onClick={(e) => { onClick={(e) => {