okay let use this code then:
Browse files<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI T-Shirt Designer</title>
<!-- Load Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Load Lucide Icons -->
<script src="https://unpkg.com/lucide@latest"></script>
<style>
/* Custom Styles for better visual appeal */
body {
font-family: 'Inter', sans-serif;
background-color: #0d0d0d;
}
.container-shadow {
box-shadow: 0 10px 30px rgba(255, 69, 0, 0.2);
}
.animate-spin-slow {
animation: spin 3s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.drag-active {
border-color: #f87171 !important; /* Tailwind red-400 */
background-color: rgba(239, 68, 68, 0.1); /* Light red background */
}
</style>
</head>
<body class="min-h-screen flex items-center justify-center p-4">
<div class="max-w-4xl w-full bg-gray-900 rounded-3xl p-6 md:p-10 container-shadow border border-red-800/50">
<header class="text-center mb-10">
<h1 class="text-4xl md:text-5xl font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-red-400 to-red-600 mb-2">
Wearable AI Art 🎨
</h1>
<p class="text-gray-400 text-lg">Turn your idea or uploaded graphic into a custom T-shirt mockup.</p>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<!-- Control Panel -->
<div class="space-y-6">
<!-- Design Idea Input -->
<div id="promptSection" class="bg-gray-800 p-5 rounded-2xl shadow-inner">
<label for="designPrompt" class="block text-red-400 font-semibold mb-2 flex items-center">
<i data-lucide="sparkles" class="w-5 h-5 mr-2"></i> Design Idea (Ignored if image is uploaded)
</label>
<textarea id="designPrompt" rows="3" placeholder="A minimalist, cyberpunk cat riding a skateboard through a neon city street."
class="w-full bg-gray-700 text-white border border-gray-600 rounded-xl p-3 focus:ring-red-500 focus:border-red-500 transition resize-none"></textarea>
<p class="text-sm text-gray-500 mt-2">Describe the graphic (or leave blank if uploading).</p>
</div>
<!-- Upload Design -->
<div class="bg-gray-800 p-5 rounded-2xl shadow-inner">
<label class="block text-red-400 font-semibold mb-2 flex items-center">
<i data-lucide="upload" class="w-5 h-5 mr-2"></i> Upload Your Graphic (Optional)
</label>
<div id="dropZone" class="border-2 border-dashed border-gray-600 rounded-xl p-4 text-center cursor-pointer hover:border-red-500 transition">
<input type="file" id="imageUpload" accept="image/png, image/jpeg" class="hidden">
<p id="dropZoneText" class="text-gray-400 text-sm">Drag & drop or <span class="text-red-400 font-medium">click to upload</span> (.png, .jpg max 5MB)</p>
<p id="fileNameDisplay" class="text-red-400 text-sm mt-1 hidden"></p>
</div>
</div>
<!-- Color Selector -->
<div class="bg-gray-800 p-5 rounded-2xl shadow-inner">
<label class="block text-red-400 font-semibold mb-3 flex items-center">
<i data-lucide="shirt" class="w-5 h-5 mr-2"></i> T-Shirt Color
</label>
<div id="colorSelector" class="flex flex-wrap gap-3">
<!-- Color options will be generated by JS -->
</div>
<input type="hidden" id="selectedColor" value="white">
</div>
<button id="generateButton" onclick="generateTshirtMockup()"
class="w-full flex items-center justify-center px-6 py-4 bg-red-600 hover:bg-red-700 text-white font-bold text-xl rounded-full transition duration-300 transform hover:scale-105 shadow-lg shadow-red-500/50"
>
<span id="buttonText">Generate Mockup</span>
<i data-lucide="image" id="buttonIcon" class="w-6 h-6 ml-2"></i>
</button>
</div>
<!-- Mockup Display -->
<div class="bg-gray-800 rounded-2xl p-5 flex flex-col items-center justify-center min-h-[400px]">
<div id="mockupContainer" class="w-full max-w-sm aspect-square relative flex items-center justify-center rounded-xl overflow-hidden bg-gray-700/50">
<img id="mockupImage" src="https://placehold.co/1024x1024/ffffff/9ca3af?text=Design+Preview"
onerror="this.onerror=null; this.src='https://placehold.co/1024x1024/27272a/9ca3af?text=Load+Error'"
alt="AI Generated T-Shirt Mockup" class="w-full h-full object-cover transition-opacity duration-500 opacity-100 rounded-xl">
<div id="loadingIndicator" class="absolute inset-0 bg-gray-900/80 flex flex-col items-center justify-center hidden">
<i data-lucide="loader-circle" class="w-10 h-10 text-red-500 animate-spin-slow mb-4"></i>
<p class="text-white font-medium">Generating Design...</p>
<p class="text-sm text-gray-400 mt-1">This can take up to 30 seconds.</p>
</div>
<div id="errorMessage" class="absolute inset-0 bg-red-900/80 flex flex-col items-center justify-center hidden p-4 text-center rounded-xl">
<i data-lucide="alert-triangle" class="w-10 h-10 text-red-300 mb-4"></i>
<p class="text-white font-medium" id="errorText">Error during generation.</p>
<p class="text-sm text-red-200 mt-1">Please try again or simplify your prompt.</p>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="mt-8 pt-6 border-t border-gray-800 flex flex-col sm:flex-row justify-end gap-4">
<button onclick="downloadImage()" id="downloadButton" disabled
class="flex items-center justify-center px-4 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-full font-medium transition disabled:opacity-50 disabled:cursor-not-allowed">
<i data-lucide="download" class="w-5 h-5 mr-2"></i> Download Design
</button>
</div>
</div>
<script>
// --- API Configuration and Core Functions ---
const apiKey = ""; // API key will be provided by the Canvas environment
// Switched to gemini-2.5-flash-image-preview for image composition/editing tasks
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`;
// Utility: Exponential backoff for reliable API calls
async function fetchWithRetry(url, options, maxRetries = 5) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return response.json();
}
let errorMessage = `API call failed with status: ${response.status} ${response.statusText || ''}`;
try {
const errorData = await response.clone().json();
if (errorData.error && errorData.error.message) {
errorMessage = `API Error (${response.status}): ${errorData.error.message}`;
} else if (errorData.message) {
errorMessage = `API Error (${response.status}): ${errorData.message}`;
} else {
errorMessage = `API call failed with status: ${response.status}. Could not read error details.`;
}
} catch (jsonError) {
// Response body wasn't JSON, use statusText
}
if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
console.warn(`Request failed (${response.status}). Retrying in ${delay}ms... Details: ${errorMessage}`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw new Error(errorMessage);
} catch (error) {
if (i === maxRetries - 1) throw error;
const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
console.error(`Attempt ${i + 1} failed: ${error.message}. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// --- UI Elements and State ---
const elements = {
promptInput: document.getElementById('designPrompt'),
selectedColorInput: document.getElementById('selectedColor'),
colorSelector: document.getElementById('colorSelector'),
generateButton: document.getElementById('generateButton'),
buttonText: document.getElementById('buttonText'),
buttonIcon: document.getElementById('buttonIcon'),
mockupImage: document.getElementById('mockupImage'),
loadingIndicator: document.getElementById('loadingIndicator'),
errorMessage: document.getElementById('errorMessage'),
errorText: document.getElementById
- .env +2 -4
- index.html +97 -65
- package.json +7 -19
- server.js +2 -235
- style.css +2 -27
|
@@ -1,6 +1,4 @@
|
|
| 1 |
```
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
PORT=3001
|
| 5 |
-
NODE_ENV=production
|
| 6 |
```
|
|
|
|
| 1 |
```
|
| 2 |
+
# This file is no longer needed
|
| 3 |
+
# API key should be provided in the Canvas environment
|
|
|
|
|
|
|
| 4 |
```
|
|
@@ -1,10 +1,13 @@
|
|
|
|
|
| 1 |
<!DOCTYPE html>
|
| 2 |
<html lang="en">
|
| 3 |
<head>
|
| 4 |
<meta charset="UTF-8">
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>AI T-Shirt Designer</title>
|
|
|
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
| 8 |
<script src="https://unpkg.com/lucide@latest"></script>
|
| 9 |
<style>
|
| 10 |
/* Custom Styles for better visual appeal */
|
|
@@ -23,8 +26,8 @@
|
|
| 23 |
to { transform: rotate(360deg); }
|
| 24 |
}
|
| 25 |
.drag-active {
|
| 26 |
-
border-color: #f87171 !important;
|
| 27 |
-
background-color: rgba(239, 68, 68, 0.1);
|
| 28 |
}
|
| 29 |
</style>
|
| 30 |
</head>
|
|
@@ -33,22 +36,25 @@
|
|
| 33 |
<div class="max-w-4xl w-full bg-gray-900 rounded-3xl p-6 md:p-10 container-shadow border border-red-800/50">
|
| 34 |
<header class="text-center mb-10">
|
| 35 |
<h1 class="text-4xl md:text-5xl font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-red-400 to-red-600 mb-2">
|
| 36 |
-
Wearable AI Art 🎨
|
| 37 |
</h1>
|
| 38 |
-
<p class="text-gray-400 text-lg">
|
| 39 |
</header>
|
| 40 |
|
| 41 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
|
|
| 42 |
<div class="space-y-6">
|
|
|
|
| 43 |
<div id="promptSection" class="bg-gray-800 p-5 rounded-2xl shadow-inner">
|
| 44 |
<label for="designPrompt" class="block text-red-400 font-semibold mb-2 flex items-center">
|
| 45 |
-
<i data-lucide="sparkles" class="w-5 h-5 mr-2"></i>
|
| 46 |
</label>
|
| 47 |
-
<textarea id="designPrompt" rows="3" placeholder="A
|
| 48 |
class="w-full bg-gray-700 text-white border border-gray-600 rounded-xl p-3 focus:ring-red-500 focus:border-red-500 transition resize-none"></textarea>
|
| 49 |
-
<p class="text-sm text-gray-500 mt-2">Describe the
|
| 50 |
</div>
|
| 51 |
|
|
|
|
| 52 |
<div class="bg-gray-800 p-5 rounded-2xl shadow-inner">
|
| 53 |
<label class="block text-red-400 font-semibold mb-2 flex items-center">
|
| 54 |
<i data-lucide="upload" class="w-5 h-5 mr-2"></i> Upload Your Graphic (Optional)
|
|
@@ -60,23 +66,26 @@
|
|
| 60 |
</div>
|
| 61 |
</div>
|
| 62 |
|
|
|
|
| 63 |
<div class="bg-gray-800 p-5 rounded-2xl shadow-inner">
|
| 64 |
<label class="block text-red-400 font-semibold mb-3 flex items-center">
|
| 65 |
<i data-lucide="shirt" class="w-5 h-5 mr-2"></i> T-Shirt Color
|
| 66 |
</label>
|
| 67 |
<div id="colorSelector" class="flex flex-wrap gap-3">
|
| 68 |
-
<
|
|
|
|
| 69 |
<input type="hidden" id="selectedColor" value="white">
|
| 70 |
</div>
|
| 71 |
|
| 72 |
<button id="generateButton" onclick="generateTshirtMockup()"
|
| 73 |
class="w-full flex items-center justify-center px-6 py-4 bg-red-600 hover:bg-red-700 text-white font-bold text-xl rounded-full transition duration-300 transform hover:scale-105 shadow-lg shadow-red-500/50"
|
| 74 |
>
|
| 75 |
-
<span id="buttonText">Generate
|
| 76 |
<i data-lucide="image" id="buttonIcon" class="w-6 h-6 ml-2"></i>
|
| 77 |
</button>
|
| 78 |
</div>
|
| 79 |
|
|
|
|
| 80 |
<div class="bg-gray-800 rounded-2xl p-5 flex flex-col items-center justify-center min-h-[400px]">
|
| 81 |
<div id="mockupContainer" class="w-full max-w-sm aspect-square relative flex items-center justify-center rounded-xl overflow-hidden bg-gray-700/50">
|
| 82 |
<img id="mockupImage" src="https://placehold.co/1024x1024/ffffff/9ca3af?text=Design+Preview"
|
|
@@ -85,19 +94,20 @@
|
|
| 85 |
|
| 86 |
<div id="loadingIndicator" class="absolute inset-0 bg-gray-900/80 flex flex-col items-center justify-center hidden">
|
| 87 |
<i data-lucide="loader-circle" class="w-10 h-10 text-red-500 animate-spin-slow mb-4"></i>
|
| 88 |
-
<p class="text-white font-medium">
|
| 89 |
-
<p class="text-sm text-gray-400 mt-1">This
|
| 90 |
</div>
|
| 91 |
|
| 92 |
<div id="errorMessage" class="absolute inset-0 bg-red-900/80 flex flex-col items-center justify-center hidden p-4 text-center rounded-xl">
|
| 93 |
<i data-lucide="alert-triangle" class="w-10 h-10 text-red-300 mb-4"></i>
|
| 94 |
<p class="text-white font-medium" id="errorText">Error during generation.</p>
|
| 95 |
-
<p class="text-sm text-red-200 mt-1">Please
|
| 96 |
</div>
|
| 97 |
</div>
|
| 98 |
</div>
|
| 99 |
</div>
|
| 100 |
|
|
|
|
| 101 |
<div class="mt-8 pt-6 border-t border-gray-800 flex flex-col sm:flex-row justify-end gap-4">
|
| 102 |
<button onclick="downloadImage()" id="downloadButton" disabled
|
| 103 |
class="flex items-center justify-center px-4 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-full font-medium transition disabled:opacity-50 disabled:cursor-not-allowed">
|
|
@@ -108,11 +118,11 @@
|
|
| 108 |
|
| 109 |
<script>
|
| 110 |
// --- API Configuration and Core Functions ---
|
| 111 |
-
|
| 112 |
-
// which must handle the OpenAI API calls (including the key).
|
| 113 |
-
const proxyApiUrl = 'https://tshirt-ai-backend.onrender.com/api/generate-mockup'; // Production backend endpoint
|
| 114 |
-
// **CRITICAL SECURITY CHANGE:** Removed the hardcoded API key from the client-side.
|
| 115 |
|
|
|
|
|
|
|
|
|
|
| 116 |
// Utility: Exponential backoff for reliable API calls
|
| 117 |
async function fetchWithRetry(url, options, maxRetries = 5) {
|
| 118 |
for (let i = 0; i < maxRetries; i++) {
|
|
@@ -122,14 +132,18 @@
|
|
| 122 |
return response.json();
|
| 123 |
}
|
| 124 |
|
| 125 |
-
let errorMessage = `
|
| 126 |
try {
|
| 127 |
const errorData = await response.clone().json();
|
| 128 |
-
if (errorData.message) {
|
| 129 |
-
errorMessage = `
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
}
|
| 131 |
} catch (jsonError) {
|
| 132 |
-
// Response body wasn't JSON
|
| 133 |
}
|
| 134 |
|
| 135 |
if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
|
|
@@ -210,12 +224,11 @@
|
|
| 210 |
elements.fileNameDisplay.textContent = `Design uploaded: ${file.name}`;
|
| 211 |
elements.fileNameDisplay.classList.remove('hidden');
|
| 212 |
elements.dropZone.classList.add('border-red-500');
|
| 213 |
-
showNotification(`Graphic "${file.name}" ready for
|
| 214 |
|
| 215 |
-
//
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
}
|
| 219 |
};
|
| 220 |
reader.onerror = () => {
|
| 221 |
showError('Error reading file.');
|
|
@@ -230,11 +243,8 @@
|
|
| 230 |
elements.fileNameDisplay.textContent = '';
|
| 231 |
elements.fileNameDisplay.classList.add('hidden');
|
| 232 |
elements.dropZone.classList.remove('border-red-500');
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
if (elements.promptInput.value.trim() === defaultPrompt) {
|
| 236 |
-
elements.promptInput.value = '';
|
| 237 |
-
}
|
| 238 |
}
|
| 239 |
|
| 240 |
function setupUploadListeners() {
|
|
@@ -275,6 +285,7 @@
|
|
| 275 |
|
| 276 |
// --- Initialization ---
|
| 277 |
|
|
|
|
| 278 |
function renderColorButtons() {
|
| 279 |
elements.colorSelector.innerHTML = shirtColors.map(color => `
|
| 280 |
<div class="relative w-8 h-8 rounded-full cursor-pointer transition-all duration-200 hover:ring-4 hover:ring-red-500"
|
|
@@ -289,6 +300,7 @@
|
|
| 289 |
lucide.createIcons();
|
| 290 |
}
|
| 291 |
|
|
|
|
| 292 |
window.selectColor = function(colorName) {
|
| 293 |
elements.selectedColorInput.value = colorName;
|
| 294 |
shirtColors.forEach(color => {
|
|
@@ -299,6 +311,7 @@
|
|
| 299 |
checkIcon.classList.add('hidden');
|
| 300 |
}
|
| 301 |
});
|
|
|
|
| 302 |
const colorHex = shirtColors.find(c => c.name === colorName).hex.substring(1);
|
| 303 |
elements.mockupImage.src = `https://placehold.co/1024x1024/${colorHex}/9ca3af?text=Design+Preview`;
|
| 304 |
currentImageUrl = null;
|
|
@@ -310,14 +323,9 @@
|
|
| 310 |
window.generateTshirtMockup = async function() {
|
| 311 |
const promptText = elements.promptInput.value.trim();
|
| 312 |
const shirtColor = elements.selectedColorInput.value;
|
| 313 |
-
const designBase64 = uploadedImageBase64;
|
| 314 |
|
| 315 |
-
if (!promptText) {
|
| 316 |
-
showNotification('Please
|
| 317 |
-
return;
|
| 318 |
-
}
|
| 319 |
-
|
| 320 |
-
if (!designBase64 && !confirm("No uploaded image found. Continue to generate a design based on your text prompt only?")) {
|
| 321 |
return;
|
| 322 |
}
|
| 323 |
|
|
@@ -325,49 +333,73 @@
|
|
| 325 |
setLoading(true);
|
| 326 |
hideError();
|
| 327 |
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
const payload = {
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
|
|
|
| 333 |
};
|
| 334 |
-
|
| 335 |
const options = {
|
| 336 |
method: 'POST',
|
| 337 |
-
// ONLY send Content-Type header to the backend proxy
|
| 338 |
headers: { 'Content-Type': 'application/json' },
|
| 339 |
body: JSON.stringify(payload)
|
| 340 |
};
|
| 341 |
|
| 342 |
try {
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
| 345 |
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
const
|
|
|
|
| 349 |
|
| 350 |
elements.mockupImage.src = imageUrl;
|
| 351 |
currentImageUrl = imageUrl;
|
| 352 |
elements.downloadButton.disabled = false;
|
| 353 |
-
showNotification('
|
| 354 |
|
| 355 |
-
} else if (data.message) {
|
| 356 |
-
// Backend returns an error message
|
| 357 |
-
throw new Error(data.message);
|
| 358 |
} else {
|
| 359 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 360 |
}
|
| 361 |
|
| 362 |
} catch (error) {
|
| 363 |
-
console.error("
|
| 364 |
-
|
| 365 |
-
const displayMessage = error.message.includes("404") ?
|
| 366 |
-
"Backend server not responding. Please try again in a moment." :
|
| 367 |
-
error.message.includes("Failed to fetch") ?
|
| 368 |
-
"Cannot connect to AI service. Please check your internet connection and try again." :
|
| 369 |
-
`Generation failed: ${error.message.substring(0, 100)}`;
|
| 370 |
-
showError(displayMessage);
|
| 371 |
elements.downloadButton.disabled = true;
|
| 372 |
} finally {
|
| 373 |
// 2. UI State: Stop Loading
|
|
@@ -375,7 +407,7 @@ showError(displayMessage);
|
|
| 375 |
}
|
| 376 |
};
|
| 377 |
|
| 378 |
-
// --- Utility Functions
|
| 379 |
|
| 380 |
function setLoading(isLoading) {
|
| 381 |
elements.generateButton.disabled = isLoading;
|
|
@@ -385,7 +417,7 @@ showError(displayMessage);
|
|
| 385 |
elements.buttonIcon.classList.add('animate-spin');
|
| 386 |
} else {
|
| 387 |
elements.loadingIndicator.classList.add('hidden');
|
| 388 |
-
elements.buttonText.textContent = 'Generate
|
| 389 |
elements.buttonIcon.classList.remove('animate-spin');
|
| 390 |
}
|
| 391 |
lucide.createIcons();
|
|
@@ -398,6 +430,7 @@ showError(displayMessage);
|
|
| 398 |
function showError(message) {
|
| 399 |
elements.errorText.textContent = message;
|
| 400 |
elements.errorMessage.classList.remove('hidden');
|
|
|
|
| 401 |
const shirtColor = elements.selectedColorInput.value;
|
| 402 |
const colorHex = shirtColors.find(c => c.name === shirtColor).hex.substring(1);
|
| 403 |
elements.mockupImage.src = `https://placehold.co/1024x1024/${colorHex}/9ca3af?text=Design+Preview`;
|
|
@@ -449,6 +482,5 @@ showError(displayMessage);
|
|
| 449 |
selectColor('white'); // Ensure the first color is selected
|
| 450 |
});
|
| 451 |
</script>
|
| 452 |
-
<script src="https://huggingface.co/deepsite/deepsite-badge.js"></script>
|
| 453 |
</body>
|
| 454 |
-
</html>
|
|
|
|
| 1 |
+
|
| 2 |
<!DOCTYPE html>
|
| 3 |
<html lang="en">
|
| 4 |
<head>
|
| 5 |
<meta charset="UTF-8">
|
| 6 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
<title>AI T-Shirt Designer</title>
|
| 8 |
+
<!-- Load Tailwind CSS -->
|
| 9 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 10 |
+
<!-- Load Lucide Icons -->
|
| 11 |
<script src="https://unpkg.com/lucide@latest"></script>
|
| 12 |
<style>
|
| 13 |
/* Custom Styles for better visual appeal */
|
|
|
|
| 26 |
to { transform: rotate(360deg); }
|
| 27 |
}
|
| 28 |
.drag-active {
|
| 29 |
+
border-color: #f87171 !important; /* Tailwind red-400 */
|
| 30 |
+
background-color: rgba(239, 68, 68, 0.1); /* Light red background */
|
| 31 |
}
|
| 32 |
</style>
|
| 33 |
</head>
|
|
|
|
| 36 |
<div class="max-w-4xl w-full bg-gray-900 rounded-3xl p-6 md:p-10 container-shadow border border-red-800/50">
|
| 37 |
<header class="text-center mb-10">
|
| 38 |
<h1 class="text-4xl md:text-5xl font-extrabold bg-clip-text text-transparent bg-gradient-to-r from-red-400 to-red-600 mb-2">
|
| 39 |
+
Wearable AI Art 🎨
|
| 40 |
</h1>
|
| 41 |
+
<p class="text-gray-400 text-lg">Turn your idea or uploaded graphic into a custom T-shirt mockup.</p>
|
| 42 |
</header>
|
| 43 |
|
| 44 |
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
| 45 |
+
<!-- Control Panel -->
|
| 46 |
<div class="space-y-6">
|
| 47 |
+
<!-- Design Idea Input -->
|
| 48 |
<div id="promptSection" class="bg-gray-800 p-5 rounded-2xl shadow-inner">
|
| 49 |
<label for="designPrompt" class="block text-red-400 font-semibold mb-2 flex items-center">
|
| 50 |
+
<i data-lucide="sparkles" class="w-5 h-5 mr-2"></i> Design Idea (Ignored if image is uploaded)
|
| 51 |
</label>
|
| 52 |
+
<textarea id="designPrompt" rows="3" placeholder="A minimalist, cyberpunk cat riding a skateboard through a neon city street."
|
| 53 |
class="w-full bg-gray-700 text-white border border-gray-600 rounded-xl p-3 focus:ring-red-500 focus:border-red-500 transition resize-none"></textarea>
|
| 54 |
+
<p class="text-sm text-gray-500 mt-2">Describe the graphic (or leave blank if uploading).</p>
|
| 55 |
</div>
|
| 56 |
|
| 57 |
+
<!-- Upload Design -->
|
| 58 |
<div class="bg-gray-800 p-5 rounded-2xl shadow-inner">
|
| 59 |
<label class="block text-red-400 font-semibold mb-2 flex items-center">
|
| 60 |
<i data-lucide="upload" class="w-5 h-5 mr-2"></i> Upload Your Graphic (Optional)
|
|
|
|
| 66 |
</div>
|
| 67 |
</div>
|
| 68 |
|
| 69 |
+
<!-- Color Selector -->
|
| 70 |
<div class="bg-gray-800 p-5 rounded-2xl shadow-inner">
|
| 71 |
<label class="block text-red-400 font-semibold mb-3 flex items-center">
|
| 72 |
<i data-lucide="shirt" class="w-5 h-5 mr-2"></i> T-Shirt Color
|
| 73 |
</label>
|
| 74 |
<div id="colorSelector" class="flex flex-wrap gap-3">
|
| 75 |
+
<!-- Color options will be generated by JS -->
|
| 76 |
+
</div>
|
| 77 |
<input type="hidden" id="selectedColor" value="white">
|
| 78 |
</div>
|
| 79 |
|
| 80 |
<button id="generateButton" onclick="generateTshirtMockup()"
|
| 81 |
class="w-full flex items-center justify-center px-6 py-4 bg-red-600 hover:bg-red-700 text-white font-bold text-xl rounded-full transition duration-300 transform hover:scale-105 shadow-lg shadow-red-500/50"
|
| 82 |
>
|
| 83 |
+
<span id="buttonText">Generate Mockup</span>
|
| 84 |
<i data-lucide="image" id="buttonIcon" class="w-6 h-6 ml-2"></i>
|
| 85 |
</button>
|
| 86 |
</div>
|
| 87 |
|
| 88 |
+
<!-- Mockup Display -->
|
| 89 |
<div class="bg-gray-800 rounded-2xl p-5 flex flex-col items-center justify-center min-h-[400px]">
|
| 90 |
<div id="mockupContainer" class="w-full max-w-sm aspect-square relative flex items-center justify-center rounded-xl overflow-hidden bg-gray-700/50">
|
| 91 |
<img id="mockupImage" src="https://placehold.co/1024x1024/ffffff/9ca3af?text=Design+Preview"
|
|
|
|
| 94 |
|
| 95 |
<div id="loadingIndicator" class="absolute inset-0 bg-gray-900/80 flex flex-col items-center justify-center hidden">
|
| 96 |
<i data-lucide="loader-circle" class="w-10 h-10 text-red-500 animate-spin-slow mb-4"></i>
|
| 97 |
+
<p class="text-white font-medium">Generating Design...</p>
|
| 98 |
+
<p class="text-sm text-gray-400 mt-1">This can take up to 30 seconds.</p>
|
| 99 |
</div>
|
| 100 |
|
| 101 |
<div id="errorMessage" class="absolute inset-0 bg-red-900/80 flex flex-col items-center justify-center hidden p-4 text-center rounded-xl">
|
| 102 |
<i data-lucide="alert-triangle" class="w-10 h-10 text-red-300 mb-4"></i>
|
| 103 |
<p class="text-white font-medium" id="errorText">Error during generation.</p>
|
| 104 |
+
<p class="text-sm text-red-200 mt-1">Please try again or simplify your prompt.</p>
|
| 105 |
</div>
|
| 106 |
</div>
|
| 107 |
</div>
|
| 108 |
</div>
|
| 109 |
|
| 110 |
+
<!-- Action Buttons -->
|
| 111 |
<div class="mt-8 pt-6 border-t border-gray-800 flex flex-col sm:flex-row justify-end gap-4">
|
| 112 |
<button onclick="downloadImage()" id="downloadButton" disabled
|
| 113 |
class="flex items-center justify-center px-4 py-3 bg-gray-700 hover:bg-gray-600 text-white rounded-full font-medium transition disabled:opacity-50 disabled:cursor-not-allowed">
|
|
|
|
| 118 |
|
| 119 |
<script>
|
| 120 |
// --- API Configuration and Core Functions ---
|
| 121 |
+
const apiKey = ""; // API key will be provided by the Canvas environment
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
+
// Switched to gemini-2.5-flash-image-preview for image composition/editing tasks
|
| 124 |
+
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent?key=${apiKey}`;
|
| 125 |
+
|
| 126 |
// Utility: Exponential backoff for reliable API calls
|
| 127 |
async function fetchWithRetry(url, options, maxRetries = 5) {
|
| 128 |
for (let i = 0; i < maxRetries; i++) {
|
|
|
|
| 132 |
return response.json();
|
| 133 |
}
|
| 134 |
|
| 135 |
+
let errorMessage = `API call failed with status: ${response.status} ${response.statusText || ''}`;
|
| 136 |
try {
|
| 137 |
const errorData = await response.clone().json();
|
| 138 |
+
if (errorData.error && errorData.error.message) {
|
| 139 |
+
errorMessage = `API Error (${response.status}): ${errorData.error.message}`;
|
| 140 |
+
} else if (errorData.message) {
|
| 141 |
+
errorMessage = `API Error (${response.status}): ${errorData.message}`;
|
| 142 |
+
} else {
|
| 143 |
+
errorMessage = `API call failed with status: ${response.status}. Could not read error details.`;
|
| 144 |
}
|
| 145 |
} catch (jsonError) {
|
| 146 |
+
// Response body wasn't JSON, use statusText
|
| 147 |
}
|
| 148 |
|
| 149 |
if (response.status === 429 || (response.status >= 500 && response.status < 600)) {
|
|
|
|
| 224 |
elements.fileNameDisplay.textContent = `Design uploaded: ${file.name}`;
|
| 225 |
elements.fileNameDisplay.classList.remove('hidden');
|
| 226 |
elements.dropZone.classList.add('border-red-500');
|
| 227 |
+
showNotification(`Graphic "${file.name}" ready for mockup!`, 'bg-green-600', 'upload-cloud');
|
| 228 |
|
| 229 |
+
// Disable text prompt when an image is uploaded
|
| 230 |
+
elements.promptInput.disabled = true;
|
| 231 |
+
elements.promptInput.value = 'Design uploaded. Generating a realistic mockup.';
|
|
|
|
| 232 |
};
|
| 233 |
reader.onerror = () => {
|
| 234 |
showError('Error reading file.');
|
|
|
|
| 243 |
elements.fileNameDisplay.textContent = '';
|
| 244 |
elements.fileNameDisplay.classList.add('hidden');
|
| 245 |
elements.dropZone.classList.remove('border-red-500');
|
| 246 |
+
elements.promptInput.disabled = false;
|
| 247 |
+
elements.promptInput.value = ''; // Clear prompt if it was auto-set
|
|
|
|
|
|
|
|
|
|
| 248 |
}
|
| 249 |
|
| 250 |
function setupUploadListeners() {
|
|
|
|
| 285 |
|
| 286 |
// --- Initialization ---
|
| 287 |
|
| 288 |
+
// 1. Render Color Buttons
|
| 289 |
function renderColorButtons() {
|
| 290 |
elements.colorSelector.innerHTML = shirtColors.map(color => `
|
| 291 |
<div class="relative w-8 h-8 rounded-full cursor-pointer transition-all duration-200 hover:ring-4 hover:ring-red-500"
|
|
|
|
| 300 |
lucide.createIcons();
|
| 301 |
}
|
| 302 |
|
| 303 |
+
// 2. Select Color Handler
|
| 304 |
window.selectColor = function(colorName) {
|
| 305 |
elements.selectedColorInput.value = colorName;
|
| 306 |
shirtColors.forEach(color => {
|
|
|
|
| 311 |
checkIcon.classList.add('hidden');
|
| 312 |
}
|
| 313 |
});
|
| 314 |
+
// Update the placeholder image to show the selected color immediately (simple mock)
|
| 315 |
const colorHex = shirtColors.find(c => c.name === colorName).hex.substring(1);
|
| 316 |
elements.mockupImage.src = `https://placehold.co/1024x1024/${colorHex}/9ca3af?text=Design+Preview`;
|
| 317 |
currentImageUrl = null;
|
|
|
|
| 323 |
window.generateTshirtMockup = async function() {
|
| 324 |
const promptText = elements.promptInput.value.trim();
|
| 325 |
const shirtColor = elements.selectedColorInput.value;
|
|
|
|
| 326 |
|
| 327 |
+
if (!uploadedImageBase64 && !promptText) {
|
| 328 |
+
showNotification('Please enter a design idea OR upload a graphic.', 'bg-yellow-500', 'alert-circle');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
return;
|
| 330 |
}
|
| 331 |
|
|
|
|
| 333 |
setLoading(true);
|
| 334 |
hideError();
|
| 335 |
|
| 336 |
+
const parts = [];
|
| 337 |
+
|
| 338 |
+
if (uploadedImageBase64) {
|
| 339 |
+
// Multimodal request (Image + Text)
|
| 340 |
+
const mimeType = uploadedImageBase64.match(/^data:(image\/\w+);base64,/)?.[1] || 'image/png';
|
| 341 |
+
const data = uploadedImageBase64.replace(/^data:image\/\w+;base64,/, '');
|
| 342 |
+
|
| 343 |
+
parts.push({
|
| 344 |
+
inlineData: {
|
| 345 |
+
mimeType: mimeType,
|
| 346 |
+
data: data
|
| 347 |
+
}
|
| 348 |
+
});
|
| 349 |
+
|
| 350 |
+
// Prompt for image-to-image/composition
|
| 351 |
+
const img2imgPrompt = `Create a high-quality, photorealistic T-shirt mockup. The T-shirt is a clean, blank ${shirtColor} color, draped naturally. Take the uploaded image and place it prominently and realistically on the center front of the T-shirt as the graphic design. The uploaded image is the only design element. Ensure the final result is a high-quality mockup showing the design correctly printed on the fabric.`;
|
| 352 |
+
parts.push({ text: img2imgPrompt });
|
| 353 |
+
|
| 354 |
+
} else {
|
| 355 |
+
// Standard text-to-image prompt
|
| 356 |
+
const descriptivePrompt = `Generate a high-resolution, photorealistic, and professional T-shirt mockup. The T-shirt is a clean, blank ${shirtColor} color, draped naturally on a minimalist background. The shirt prominently features a bold, high-quality, and centered graphic design based on the idea: "${promptText}". Focus on rendering the graphic design clearly on the shirt fabric, with photorealistic lighting and shadows. The graphic must be centered and appropriate for a t-shirt print.`;
|
| 357 |
+
parts.push({ text: descriptivePrompt });
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
|
| 361 |
const payload = {
|
| 362 |
+
contents: [{ parts: parts }],
|
| 363 |
+
generationConfig: {
|
| 364 |
+
responseModalities: ["IMAGE", "TEXT"]
|
| 365 |
+
}
|
| 366 |
};
|
| 367 |
+
|
| 368 |
const options = {
|
| 369 |
method: 'POST',
|
|
|
|
| 370 |
headers: { 'Content-Type': 'application/json' },
|
| 371 |
body: JSON.stringify(payload)
|
| 372 |
};
|
| 373 |
|
| 374 |
try {
|
| 375 |
+
const data = await fetchWithRetry(apiUrl, options);
|
| 376 |
+
|
| 377 |
+
// The image data is nested in the parts array
|
| 378 |
+
const imagePart = data.candidates?.[0]?.content?.parts?.find(p => p.inlineData);
|
| 379 |
|
| 380 |
+
if (imagePart && imagePart.inlineData && imagePart.inlineData.data) {
|
| 381 |
+
const base64Data = imagePart.inlineData.data;
|
| 382 |
+
const mimeType = imagePart.inlineData.mimeType || 'image/png';
|
| 383 |
+
const imageUrl = `data:${mimeType};base64,${base64Data}`;
|
| 384 |
|
| 385 |
elements.mockupImage.src = imageUrl;
|
| 386 |
currentImageUrl = imageUrl;
|
| 387 |
elements.downloadButton.disabled = false;
|
| 388 |
+
showNotification('Mockup generated successfully!', 'bg-green-600', 'check-circle');
|
| 389 |
|
|
|
|
|
|
|
|
|
|
| 390 |
} else {
|
| 391 |
+
// Check for text response if image generation failed
|
| 392 |
+
const textPart = data.candidates?.[0]?.content?.parts?.find(p => p.text);
|
| 393 |
+
if (textPart) {
|
| 394 |
+
// The model might send a rejection message in text if it can't generate the image
|
| 395 |
+
throw new Error(`AI response (Text): ${textPart.text}`);
|
| 396 |
+
}
|
| 397 |
+
throw new Error('No valid image data received from the AI.');
|
| 398 |
}
|
| 399 |
|
| 400 |
} catch (error) {
|
| 401 |
+
console.error("AI API Error:", error);
|
| 402 |
+
showError(`Generation failed: ${error.message}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
elements.downloadButton.disabled = true;
|
| 404 |
} finally {
|
| 405 |
// 2. UI State: Stop Loading
|
|
|
|
| 407 |
}
|
| 408 |
};
|
| 409 |
|
| 410 |
+
// --- Utility Functions ---
|
| 411 |
|
| 412 |
function setLoading(isLoading) {
|
| 413 |
elements.generateButton.disabled = isLoading;
|
|
|
|
| 417 |
elements.buttonIcon.classList.add('animate-spin');
|
| 418 |
} else {
|
| 419 |
elements.loadingIndicator.classList.add('hidden');
|
| 420 |
+
elements.buttonText.textContent = 'Generate Mockup';
|
| 421 |
elements.buttonIcon.classList.remove('animate-spin');
|
| 422 |
}
|
| 423 |
lucide.createIcons();
|
|
|
|
| 430 |
function showError(message) {
|
| 431 |
elements.errorText.textContent = message;
|
| 432 |
elements.errorMessage.classList.remove('hidden');
|
| 433 |
+
// Revert image to color placeholder on error
|
| 434 |
const shirtColor = elements.selectedColorInput.value;
|
| 435 |
const colorHex = shirtColors.find(c => c.name === shirtColor).hex.substring(1);
|
| 436 |
elements.mockupImage.src = `https://placehold.co/1024x1024/${colorHex}/9ca3af?text=Design+Preview`;
|
|
|
|
| 482 |
selectColor('white'); // Ensure the first color is selected
|
| 483 |
});
|
| 484 |
</script>
|
|
|
|
| 485 |
</body>
|
| 486 |
+
</html>
|
|
@@ -1,28 +1,16 @@
|
|
| 1 |
json
|
| 2 |
{
|
| 3 |
-
"name": "tshirt-ai-
|
| 4 |
"version": "1.0.0",
|
| 5 |
-
"description": "
|
| 6 |
-
"main": "
|
| 7 |
"scripts": {
|
| 8 |
-
"start": "
|
| 9 |
-
"dev": "
|
| 10 |
},
|
| 11 |
-
"keywords": ["ai", "tshirt", "design", "
|
| 12 |
"author": "AI Designer",
|
| 13 |
"license": "MIT",
|
| 14 |
-
"dependencies": {
|
| 15 |
-
"express": "^4.18.2",
|
| 16 |
-
"cors": "^2.8.5",
|
| 17 |
-
"multer": "^1.4.5-lts.1",
|
| 18 |
-
"openai": "^4.20.1",
|
| 19 |
-
"dotenv": "^16.3.1",
|
| 20 |
-
"sharp": "^0.32.6",
|
| 21 |
-
"axios": "^1.6.2"
|
| 22 |
-
},
|
| 23 |
-
"devDependencies": {
|
| 24 |
-
"nodemon": "^3.0.1"
|
| 25 |
-
}
|
| 26 |
}
|
| 27 |
-
|
| 28 |
</html>
|
|
|
|
| 1 |
json
|
| 2 |
{
|
| 3 |
+
"name": "tshirt-ai-designer",
|
| 4 |
"version": "1.0.0",
|
| 5 |
+
"description": "AI T-Shirt Designer with Gemini API integration",
|
| 6 |
+
"main": "index.html",
|
| 7 |
"scripts": {
|
| 8 |
+
"start": "python -m http.server 8000",
|
| 9 |
+
"dev": "python -m http.server 8000"
|
| 10 |
},
|
| 11 |
+
"keywords": ["ai", "tshirt", "design", "gemini", "mockup"],
|
| 12 |
"author": "AI Designer",
|
| 13 |
"license": "MIT",
|
| 14 |
+
"dependencies": {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
}
|
|
|
|
| 16 |
</html>
|
|
@@ -1,236 +1,3 @@
|
|
| 1 |
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
const multer = require('multer');
|
| 5 |
-
const OpenAI = require('openai');
|
| 6 |
-
const sharp = require('sharp');
|
| 7 |
-
const path = require('path');
|
| 8 |
-
require('dotenv').config();
|
| 9 |
-
|
| 10 |
-
// Keep the server awake
|
| 11 |
-
const http = require('http');
|
| 12 |
-
setInterval(() => {
|
| 13 |
-
http.get('http://tshirt-ai-backend.onrender.com/api/health');
|
| 14 |
-
}, 25 * 60 * 1000); // Ping every 25 minutes
|
| 15 |
-
const app = express();
|
| 16 |
-
const PORT = process.env.PORT || 3001;
|
| 17 |
-
|
| 18 |
-
// Initialize OpenAI client
|
| 19 |
-
const openai = new OpenAI({
|
| 20 |
-
apiKey: process.env.OPENAI_API_KEY,
|
| 21 |
-
});
|
| 22 |
-
// Middleware
|
| 23 |
-
app.use(cors({
|
| 24 |
-
origin: ['http://localhost:3000', 'https://localhost:3000', 'http://127.0.0.1:3000', 'https://127.0.0.1:3000'],
|
| 25 |
-
credentials: true,
|
| 26 |
-
methods: ['GET', 'POST', 'OPTIONS'],
|
| 27 |
-
allowedHeaders: ['Content-Type', 'Authorization']
|
| 28 |
-
}));
|
| 29 |
-
app.use(express.json({ limit: '10mb' }));
|
| 30 |
-
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
| 31 |
-
|
| 32 |
-
// Configure multer for file uploads
|
| 33 |
-
const upload = multer({
|
| 34 |
-
storage: multer.memoryStorage(),
|
| 35 |
-
limits: {
|
| 36 |
-
fileSize: 10 * 1024 * 1024, // 10MB limit
|
| 37 |
-
},
|
| 38 |
-
fileFilter: (req, file, cb) => {
|
| 39 |
-
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
| 40 |
-
if (allowedTypes.includes(file.mimetype)) {
|
| 41 |
-
cb(null, true);
|
| 42 |
-
} else {
|
| 43 |
-
cb(new Error('Invalid file type. Only JPEG, PNG, and WebP are allowed.'), false);
|
| 44 |
-
}
|
| 45 |
-
}
|
| 46 |
-
});
|
| 47 |
-
// Health check endpoint
|
| 48 |
-
app.get('/api/health', (req, res) => {
|
| 49 |
-
res.json({
|
| 50 |
-
status: 'Server is running',
|
| 51 |
-
timestamp: new Date().toISOString(),
|
| 52 |
-
version: '1.0.0',
|
| 53 |
-
uptime: process.uptime()
|
| 54 |
-
});
|
| 55 |
-
});
|
| 56 |
-
|
| 57 |
-
// Root endpoint
|
| 58 |
-
app.get('/', (req, res) => {
|
| 59 |
-
res.json({
|
| 60 |
-
message: 'T-Shirt AI Backend Server',
|
| 61 |
-
status: 'Running',
|
| 62 |
-
endpoints: {
|
| 63 |
-
health: '/api/health',
|
| 64 |
-
generate: '/api/generate-mockup'
|
| 65 |
-
}
|
| 66 |
-
});
|
| 67 |
-
});
|
| 68 |
-
// Main mockup generation endpoint
|
| 69 |
-
app.post('/api/generate-mockup', async (req, res) => {
|
| 70 |
-
try {
|
| 71 |
-
const { prompt, shirtColor, designBase64 } = req.body;
|
| 72 |
-
|
| 73 |
-
if (!prompt) {
|
| 74 |
-
return res.status(400).json({
|
| 75 |
-
success: false,
|
| 76 |
-
message: 'Prompt is required'
|
| 77 |
-
});
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
console.log('Generating mockup with:', { prompt: prompt.substring(0, 100), shirtColor, hasDesign: !!designBase64 });
|
| 81 |
-
|
| 82 |
-
let finalPrompt = prompt;
|
| 83 |
-
|
| 84 |
-
// If there's a design, enhance the prompt
|
| 85 |
-
if (designBase64) {
|
| 86 |
-
finalPrompt += ` Create a realistic product photography of a ${shirtColor} T-shirt with the uploaded design printed on it. The design should look naturally integrated, not floating. Show proper fabric texture, shadows, and realistic lighting. The T-shirt should be displayed on a mannequin or flat lay with professional studio lighting.`;
|
| 87 |
-
} else {
|
| 88 |
-
finalPrompt += ` Create a high-quality product photography of a ${shirtColor} T-shirt. Show realistic fabric texture, proper folds, shadows, and professional studio lighting. Display it as if it's a real product photo for an e-commerce store.`;
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
// Enhanced prompt for better quality
|
| 92 |
-
finalPrompt += ' Photorealistic, 8k resolution, detailed, professional lighting, commercial photography, high quality, sharp focus.';
|
| 93 |
-
|
| 94 |
-
console.log('Enhanced prompt:', finalPrompt);
|
| 95 |
-
|
| 96 |
-
// Generate image using OpenAI DALL-E 3
|
| 97 |
-
const response = await openai.images.generate({
|
| 98 |
-
model: "dall-e-3",
|
| 99 |
-
prompt: finalPrompt,
|
| 100 |
-
n: 1,
|
| 101 |
-
size: "1024x1024",
|
| 102 |
-
quality: "hd",
|
| 103 |
-
style: "natural",
|
| 104 |
-
response_format: "b64_json"
|
| 105 |
-
});
|
| 106 |
-
|
| 107 |
-
if (!response.data || response.data.length === 0) {
|
| 108 |
-
throw new Error('No image generated from OpenAI');
|
| 109 |
-
}
|
| 110 |
-
|
| 111 |
-
const imageData = response.data[0].b64_json;
|
| 112 |
-
|
| 113 |
-
// If there's a design to composite, process it
|
| 114 |
-
if (designBase64 && imageData) {
|
| 115 |
-
try {
|
| 116 |
-
// This is a simplified composite operation
|
| 117 |
-
// In a production environment, you'd want more sophisticated image processing
|
| 118 |
-
const compositeImage = await compositeDesignOnTshirt(imageData, designBase64, shirtColor);
|
| 119 |
-
|
| 120 |
-
res.json({
|
| 121 |
-
success: true,
|
| 122 |
-
finalImageBase64: compositeImage,
|
| 123 |
-
message: 'Mockup generated successfully with design'
|
| 124 |
-
});
|
| 125 |
-
} catch (compositeError) {
|
| 126 |
-
console.error('Composite error:', compositeError);
|
| 127 |
-
// Fallback to the generated image without composite
|
| 128 |
-
res.json({
|
| 129 |
-
success: true,
|
| 130 |
-
finalImageBase64: imageData,
|
| 131 |
-
message: 'Mockup generated (design composite failed, using generated image)'
|
| 132 |
-
});
|
| 133 |
-
}
|
| 134 |
-
} else {
|
| 135 |
-
// Return the generated image as is
|
| 136 |
-
res.json({
|
| 137 |
-
success: true,
|
| 138 |
-
finalImageBase64: imageData,
|
| 139 |
-
message: 'Mockup generated successfully'
|
| 140 |
-
});
|
| 141 |
-
}
|
| 142 |
-
|
| 143 |
-
} catch (error) {
|
| 144 |
-
console.error('Error generating mockup:', error);
|
| 145 |
-
|
| 146 |
-
let errorMessage = 'Failed to generate mockup';
|
| 147 |
-
let statusCode = 500;
|
| 148 |
-
|
| 149 |
-
if (error.message.includes('Invalid API key')) {
|
| 150 |
-
errorMessage = 'Invalid OpenAI API key';
|
| 151 |
-
statusCode = 401;
|
| 152 |
-
} else if (error.message.includes('rate limit')) {
|
| 153 |
-
errorMessage = 'Rate limit exceeded. Please try again later.';
|
| 154 |
-
statusCode = 429;
|
| 155 |
-
} else if (error.message.includes('content policy')) {
|
| 156 |
-
errorMessage = 'Content policy violation. Please modify your prompt.';
|
| 157 |
-
statusCode = 400;
|
| 158 |
-
} else if (error.message.includes('insufficient credits')) {
|
| 159 |
-
errorMessage = 'Insufficient API credits. Please check your OpenAI account.';
|
| 160 |
-
statusCode = 402;
|
| 161 |
-
}
|
| 162 |
-
|
| 163 |
-
res.status(statusCode).json({
|
| 164 |
-
success: false,
|
| 165 |
-
message: errorMessage,
|
| 166 |
-
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
| 167 |
-
});
|
| 168 |
-
}
|
| 169 |
-
});
|
| 170 |
-
|
| 171 |
-
// Composite design on T-shirt (simplified version)
|
| 172 |
-
async function compositeDesignOnTshirt(tshirtBase64, designBase64, shirtColor) {
|
| 173 |
-
try {
|
| 174 |
-
// Convert base64 to buffers
|
| 175 |
-
const tshirtBuffer = Buffer.from(tshirtBase64, 'base64');
|
| 176 |
-
const designBuffer = Buffer.from(designBase64.split(',')[1] || designBase64, 'base64');
|
| 177 |
-
|
| 178 |
-
// Resize design to appropriate size for T-shirt
|
| 179 |
-
const design = await sharp(designBuffer)
|
| 180 |
-
.resize({
|
| 181 |
-
width: 300,
|
| 182 |
-
height: 300,
|
| 183 |
-
fit: 'contain',
|
| 184 |
-
background: { r: 0, g: 0, b: 0, alpha: 0 }
|
| 185 |
-
})
|
| 186 |
-
.png()
|
| 187 |
-
.toBuffer();
|
| 188 |
-
|
| 189 |
-
// Get T-shirt metadata
|
| 190 |
-
const tshirtMetadata = await sharp(tshirtBuffer).metadata();
|
| 191 |
-
|
| 192 |
-
// Calculate position for design (center of T-shirt)
|
| 193 |
-
const designX = Math.floor((tshirtMetadata.width - 300) / 2);
|
| 194 |
-
const designY = Math.floor((tshirtMetadata.height - 300) / 3); // Upper chest area
|
| 195 |
-
|
| 196 |
-
// Composite design on T-shirt
|
| 197 |
-
const compositeBuffer = await sharp(tshirtBuffer)
|
| 198 |
-
.composite([{
|
| 199 |
-
input: design,
|
| 200 |
-
left: designX,
|
| 201 |
-
top: designY,
|
| 202 |
-
blend: 'over'
|
| 203 |
-
}])
|
| 204 |
-
.png()
|
| 205 |
-
.toBuffer();
|
| 206 |
-
|
| 207 |
-
// Convert back to base64
|
| 208 |
-
return compositeBuffer.toString('base64');
|
| 209 |
-
|
| 210 |
-
} catch (error) {
|
| 211 |
-
console.error('Composite error:', error);
|
| 212 |
-
throw error;
|
| 213 |
-
}
|
| 214 |
-
}
|
| 215 |
-
|
| 216 |
-
// Serve static files if needed
|
| 217 |
-
app.use(express.static(path.join(__dirname, 'public')));
|
| 218 |
-
|
| 219 |
-
// Error handling middleware
|
| 220 |
-
app.use((error, req, res, next) => {
|
| 221 |
-
console.error('Unhandled error:', error);
|
| 222 |
-
res.status(500).json({
|
| 223 |
-
success: false,
|
| 224 |
-
message: 'Internal server error'
|
| 225 |
-
});
|
| 226 |
-
});
|
| 227 |
-
|
| 228 |
-
// Start server
|
| 229 |
-
app.listen(PORT, () => {
|
| 230 |
-
console.log(`🚀 T-Shirt AI Backend Server is running on port ${PORT}`);
|
| 231 |
-
console.log(`📡 API endpoint: http://localhost:${PORT}/api/generate-mockup`);
|
| 232 |
-
console.log(`🏥 Health check: http://localhost:${PORT}/api/health`);
|
| 233 |
-
console.log(`🔗 Frontend URL: ${process.env.FRONTEND_URL || 'http://localhost:3000'}`);
|
| 234 |
-
});
|
| 235 |
-
|
| 236 |
-
module.exports = app;
|
|
|
|
| 1 |
|
| 2 |
+
# This file is no longer needed for the new Gemini-based implementation
|
| 3 |
+
# The application now runs directly in the browser using the Gemini API
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,28 +1,3 @@
|
|
| 1 |
-
body {
|
| 2 |
-
padding: 2rem;
|
| 3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
| 4 |
-
}
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
margin-top: 0;
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
p {
|
| 12 |
-
color: rgb(107, 114, 128);
|
| 13 |
-
font-size: 15px;
|
| 14 |
-
margin-bottom: 10px;
|
| 15 |
-
margin-top: 5px;
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
.card {
|
| 19 |
-
max-width: 620px;
|
| 20 |
-
margin: 0 auto;
|
| 21 |
-
padding: 16px;
|
| 22 |
-
border: 1px solid lightgray;
|
| 23 |
-
border-radius: 16px;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
.card p:last-child {
|
| 27 |
-
margin-bottom: 0;
|
| 28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
|
| 2 |
+
/* This CSS is now embedded in the HTML file */
|
| 3 |
+
/* No separate CSS file needed for this implementation */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|