dodey917 commited on
Commit
de208fe
·
verified ·
1 Parent(s): 1e37fa0

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

Files changed (5) hide show
  1. .env +2 -4
  2. index.html +97 -65
  3. package.json +7 -19
  4. server.js +2 -235
  5. style.css +2 -27
.env CHANGED
@@ -1,6 +1,4 @@
1
  ```
2
- OPENAI_API_KEY=your_openai_api_key_here
3
- FRONTEND_URL=http://localhost:3000
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
  ```
index.html CHANGED
@@ -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 🎨 (Pro Mode)
37
  </h1>
38
- <p class="text-gray-400 text-lg">Uses secure backend for realistic graphic placement.</p>
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> T-Shirt & Background Description
46
  </label>
47
- <textarea id="designPrompt" rows="3" placeholder="A high-quality blank T-shirt in white, naturally draped on a simple, textured background."
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 desired T-shirt color, style, and background.</p>
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
- </div>
 
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 Realistic Mockup</span>
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">Sending to AI for realistic placement...</p>
89
- <p class="text-sm text-gray-400 mt-1">This process can take up to 60 seconds.</p>
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 check your backend connection or simplify the prompt.</p>
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
- // NOTE: The front-end now calls a secure backend proxy server,
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 = `Proxy call failed with status: ${response.status} ${response.statusText || ''}`;
126
  try {
127
  const errorData = await response.clone().json();
128
- if (errorData.message) {
129
- errorMessage = `Proxy Error (${response.status}): ${errorData.message}`;
 
 
 
 
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 composition!`, 'bg-green-600', 'upload-cloud');
214
 
215
- // Ensure a base prompt is set
216
- if (!elements.promptInput.value.trim()) {
217
- elements.promptInput.value = `A clean, blank T-shirt in ${elements.selectedColorInput.value} color, naturally draped on a simple, textured background.`;
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
- // If the prompt was the default one, clear it.
234
- const defaultPrompt = `A clean, blank T-shirt in ${elements.selectedColorInput.value} color, naturally draped on a simple, textured background.`;
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 describe the T-shirt and background.', 'bg-yellow-500', 'alert-circle');
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
- // Data payload sent to your backend proxy server
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  const payload = {
330
- prompt: promptText, // Describe the T-shirt scene
331
- shirtColor: shirtColor,
332
- designBase64: designBase64 // Uploaded image data (or null)
 
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
- // Step 1: Call your secure backend proxy
344
- const data = await fetchWithRetry(proxyApiUrl, options);
 
 
345
 
346
- // The backend is expected to return a single data URL (base64) of the final composite image.
347
- if (data.finalImageBase64) {
348
- const imageUrl = `data:image/png;base64,${data.finalImageBase64}`;
 
349
 
350
  elements.mockupImage.src = imageUrl;
351
  currentImageUrl = imageUrl;
352
  elements.downloadButton.disabled = false;
353
- showNotification('Realistic mockup generated successfully!', 'bg-green-600', 'check-circle');
354
 
355
- } else if (data.message) {
356
- // Backend returns an error message
357
- throw new Error(data.message);
358
  } else {
359
- throw new Error('No valid image data received from the backend.');
 
 
 
 
 
 
360
  }
361
 
362
  } catch (error) {
363
- console.error("Mockup Generation Error:", error);
364
- // If the error message is too long, truncate it
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 (unchanged) ---
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 Realistic Mockup';
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>
package.json CHANGED
@@ -1,28 +1,16 @@
1
  json
2
  {
3
- "name": "tshirt-ai-backend",
4
  "version": "1.0.0",
5
- "description": "Backend server for AI T-Shirt Designer with OpenAI integration",
6
- "main": "server.js",
7
  "scripts": {
8
- "start": "node server.js",
9
- "dev": "nodemon server.js"
10
  },
11
- "keywords": ["ai", "tshirt", "design", "openai", "dalle"],
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>
server.js CHANGED
@@ -1,236 +1,3 @@
1
 
2
- const express = require('express');
3
- const cors = require('cors');
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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
style.css CHANGED
@@ -1,28 +1,3 @@
1
- body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
 
6
- h1 {
7
- font-size: 16px;
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 */