Wills17 commited on
Commit
98d6f80
·
verified ·
1 Parent(s): a556d44

Update static/scripts.js

Browse files
Files changed (1) hide show
  1. static/scripts.js +37 -44
static/scripts.js CHANGED
@@ -26,7 +26,8 @@ const elements = {
26
  };
27
 
28
  // Global abort controller
29
- let abortController = null;
 
30
 
31
  // Initialize app
32
  function init() {
@@ -105,20 +106,23 @@ function handleFileUpload(file) {
105
 
106
  // Reset upload
107
  function resetUpload() {
108
- state.uploadedImage = null;
109
- state.detectedIngredients = [];
 
110
  elements.fileInput.value = '';
111
  elements.uploadArea.style.display = 'block';
112
  elements.previewSection.style.display = 'none';
113
  elements.scanButton.style.display = 'none';
114
  elements.resultsSection.style.display = 'none';
115
  elements.heroSection.style.display = 'block';
 
 
116
  }
117
 
118
  async function handleMissingApiKeyWarning() {
119
  if (state.geminiApiKey || state.skipApiKeyWarning) return;
120
  const proceed = confirm("Continue without Gemini API key?\n\n• Slower recipe generation\n• Lower quality possible\n\nProceed anyway?");
121
- if (!proceed) throw new Error("Cancelled");
122
  if (confirm("Don't show this again?")) {
123
  state.skipApiKeyWarning = true;
124
  localStorage.setItem('skipApiKeyWarning', 'true');
@@ -129,21 +133,21 @@ function updateScanButton(isProcessing) {
129
  elements.scanButton.disabled = false;
130
 
131
  if (isProcessing) {
132
- elements.scanButton.classList.add("processing");
133
  elements.scanButtonText.textContent = "Cancel";
134
  } else {
135
- elements.scanButton.classList.remove("processing");
136
  elements.scanButtonText.textContent = "Scan Ingredients";
137
  }
138
  }
139
 
140
  // Image Scan and Backend Processing
141
  async function handleScan() {
142
- // If already running → act as Cancel button
143
  if (state.isProcessing) {
144
  if (abortController) {
145
  abortController.abort();
146
- console.log("Cancelled by user");
147
  }
148
  return;
149
  }
@@ -153,10 +157,7 @@ async function handleScan() {
153
  // Reset previous controller
154
  abortController = new AbortController();
155
  state.isProcessing = true;
156
-
157
- // Show Cancel
158
- elements.scanButtonText.textContent = "Cancel";
159
- elements.scanButton.classList.add("cancel-mode");
160
 
161
  // Reset results
162
  elements.ingredientsList.innerHTML = "";
@@ -168,7 +169,7 @@ async function handleScan() {
168
 
169
  // Detect ingredients
170
  const detectForm = new FormData();
171
- detectForm.append("file", new File([blob], "image.jpg", { type: blob.type }));
172
 
173
  const detectResponse = await fetch("/detect-ingredients/", {
174
  method: "POST",
@@ -177,67 +178,61 @@ async function handleScan() {
177
  });
178
 
179
  if (!detectResponse.ok) {
180
- if (abortController.signal.aborted) throw new Error("cancelled");
181
- throw new Error("Detection failed");
 
182
  }
183
 
184
  const { ingredients } = await detectResponse.json();
185
- state.detectedIngredients = ingredients.map(i => ({
186
- name: i.name,
187
- confidence: i.confidence
188
- }));
189
-
190
  displayIngredients(state.detectedIngredients);
191
 
192
- // Show thinking card
193
  const card = document.createElement("div");
194
  card.className = "recipe-card";
195
  card.innerHTML = `<div class="recipe-header"><h4>AI-Generated Recipe</h4></div>
196
- <div class="recipe-section"><p style="text-align:center;padding:2rem">
197
- <em>Chef is cooking...</em><br><br>Up to 60s without API key
198
  </p></div>`;
199
  elements.recipesList.appendChild(card);
200
  elements.recipesSection.style.display = "block";
201
 
202
- // 2. Generate recipe
203
  const recipeForm = new FormData();
204
  recipeForm.append("ingredients", state.detectedIngredients.map(i => i.name).join(", "));
205
  recipeForm.append("api_key", state.geminiApiKey.trim());
206
 
207
- const recipeRes = await fetch("/generate-recipe/", {
208
  method: "POST",
209
  body: recipeForm,
210
  signal: abortController.signal
211
  });
212
 
213
- if (!recipeRes.ok) {
214
- if (abortController.signal.aborted) throw new Error("cancelled");
215
- throw new Error("Recipe failed");
 
216
  }
217
 
218
- const { recipe } = await recipeRes.json();
219
  card.innerHTML = `<div class="recipe-header"><h4>AI-Generated Recipe</h4></div>
220
  <div class="recipe-section"><div class="recipe-markdown">${marked.parse(recipe)}</div></div>`;
221
 
222
  } catch (err) {
223
  if (err.message === "cancelled" || abortController.signal.aborted) {
224
- elements.recipesList.innerHTML = `<div class="recipe-card" style="text-align:center;padding:2rem">
225
- <p>You cancelled the operation.</p>
226
- <button onclick="handleScan()" class="scan-button">
227
- <span id="scanButtonText">Try Again</span>
228
- </button>
229
  </div>`;
230
  } else {
231
  console.error(err);
232
  elements.recipesList.innerHTML = `<div class="recipe-card" style="color:var(--error);text-align:center;padding:2rem">
233
- <h4>Error</h4><p>Try again or add Gemini API key</p>
234
  </div>`;
235
  }
236
  } finally {
237
  state.isProcessing = false;
238
  abortController = null;
239
- elements.scanButtonText.textContent = "Scan Ingredients";
240
- elements.scanButton.classList.remove("cancel-mode");
241
  }
242
  }
243
 
@@ -246,12 +241,12 @@ async function handleScan() {
246
  function displayIngredients(ingredients) {
247
  elements.ingredientsList.innerHTML = '';
248
  ingredients.forEach((ing, i) => {
249
- const conf = Math.round(ing.confidence * 100);
250
  const color = conf >= 70 ? "#2ecc71" : conf >= 40 ? "#f1c40f" : "#e74c3c";
251
 
252
  const item = document.createElement('div');
253
  item.className = 'ingredient-item';
254
- item.style.animation = `fadeIn 0.8s ease-out ${i * 0.1}s forwards`;
255
  item.innerHTML = `
256
  <div class="ingredient-header">
257
  <span class="ingredient-name">${ing.name}</span>
@@ -260,13 +255,10 @@ function displayIngredients(ingredients) {
260
  </span>
261
  </div>
262
  <div class="confidence-bar">
263
- <div class="confidence-fill" style="background:${color};width:0%;transition:width 1s ease-out"></div>
264
  </div>`;
265
  elements.ingredientsList.appendChild(item);
266
-
267
- setTimeout(() => {
268
- item.querySelector('.confidence-fill').style.width = `${conf}%`;
269
- }, 100);
270
  });
271
  }
272
 
@@ -275,6 +267,7 @@ document.head.insertAdjacentHTML('beforeend', `
275
  <style>
276
  @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
277
  .ingredient-item{opacity:0}
 
278
  </style>`);
279
 
280
  // Start app
 
26
  };
27
 
28
  // Global abort controller
29
+ let abortController = null;
30
+
31
 
32
  // Initialize app
33
  function init() {
 
106
 
107
  // Reset upload
108
  function resetUpload() {
109
+ if (state.isProcessing && abortController) abortController.abort();
110
+ state = { uploadedImage: null, detectedIngredients: [], isProcessing: false, geminiApiKey: state.geminiApiKey, skipApiKeyWarning: state.skipApiKeyWarning };
111
+ abortController = null;
112
  elements.fileInput.value = '';
113
  elements.uploadArea.style.display = 'block';
114
  elements.previewSection.style.display = 'none';
115
  elements.scanButton.style.display = 'none';
116
  elements.resultsSection.style.display = 'none';
117
  elements.heroSection.style.display = 'block';
118
+ elements.scanButtonText.textContent = "Scan Ingredients";
119
+ elements.scanButton.classList.remove("cancel-mode");
120
  }
121
 
122
  async function handleMissingApiKeyWarning() {
123
  if (state.geminiApiKey || state.skipApiKeyWarning) return;
124
  const proceed = confirm("Continue without Gemini API key?\n\n• Slower recipe generation\n• Lower quality possible\n\nProceed anyway?");
125
+ if (!proceed) throw new Error("User cancelled");
126
  if (confirm("Don't show this again?")) {
127
  state.skipApiKeyWarning = true;
128
  localStorage.setItem('skipApiKeyWarning', 'true');
 
133
  elements.scanButton.disabled = false;
134
 
135
  if (isProcessing) {
136
+ elements.scanButton.classList.add("cancel-mode");
137
  elements.scanButtonText.textContent = "Cancel";
138
  } else {
139
+ elements.scanButton.classList.remove("cancel-mode");
140
  elements.scanButtonText.textContent = "Scan Ingredients";
141
  }
142
  }
143
 
144
  // Image Scan and Backend Processing
145
  async function handleScan() {
146
+ // If already running → display as Cancel button
147
  if (state.isProcessing) {
148
  if (abortController) {
149
  abortController.abort();
150
+ console.log("Scan cancelled by user");
151
  }
152
  return;
153
  }
 
157
  // Reset previous controller
158
  abortController = new AbortController();
159
  state.isProcessing = true;
160
+ updateScanButton(true);
 
 
 
161
 
162
  // Reset results
163
  elements.ingredientsList.innerHTML = "";
 
169
 
170
  // Detect ingredients
171
  const detectForm = new FormData();
172
+ detectForm.append("file", new File([blob], "fridge.jpg", { type: blob.type }));
173
 
174
  const detectResponse = await fetch("/detect-ingredients/", {
175
  method: "POST",
 
178
  });
179
 
180
  if (!detectResponse.ok) {
181
+ const text = await detectResponse.text();
182
+ if (abortController.signal.aborted || detectResponse.status === 499) throw new Error("cancelled");
183
+ throw new Error(text || "Detection failed");
184
  }
185
 
186
  const { ingredients } = await detectResponse.json();
187
+ state.detectedIngredients = ingredients.map(i => ({ name: i.name, confidence: i.confidence }));
 
 
 
 
188
  displayIngredients(state.detectedIngredients);
189
 
190
+ // Generate recipe
191
  const card = document.createElement("div");
192
  card.className = "recipe-card";
193
  card.innerHTML = `<div class="recipe-header"><h4>AI-Generated Recipe</h4></div>
194
+ <div class="recipe-section"><p style="text-align:center;padding:3rem">
195
+ <em>Chef is thinking...</em><br><br>This can take up to 60s without a Gemini API key
196
  </p></div>`;
197
  elements.recipesList.appendChild(card);
198
  elements.recipesSection.style.display = "block";
199
 
 
200
  const recipeForm = new FormData();
201
  recipeForm.append("ingredients", state.detectedIngredients.map(i => i.name).join(", "));
202
  recipeForm.append("api_key", state.geminiApiKey.trim());
203
 
204
+ const recipeResponse = await fetch("/generate-recipe/", {
205
  method: "POST",
206
  body: recipeForm,
207
  signal: abortController.signal
208
  });
209
 
210
+ if (!recipeResponse.ok) {
211
+ const text = await recipeResponse.text();
212
+ if (abortController.signal.aborted || recipeResponse.status === 499) throw new Error("cancelled");
213
+ throw new Error(text || "Recipe generation failed");
214
  }
215
 
216
+ const { recipe } = await recipeResponse.json();
217
  card.innerHTML = `<div class="recipe-header"><h4>AI-Generated Recipe</h4></div>
218
  <div class="recipe-section"><div class="recipe-markdown">${marked.parse(recipe)}</div></div>`;
219
 
220
  } catch (err) {
221
  if (err.message === "cancelled" || abortController.signal.aborted) {
222
+ elements.recipesList.innerHTML = `<div class="recipe-card" style="text-align:center;padding:2rem;color:var(--text-secondary)">
223
+ <p>Operation cancelled.</p>
224
+ <button onclick="handleScan()" class="scan-button small">Try Again</button>
 
 
225
  </div>`;
226
  } else {
227
  console.error(err);
228
  elements.recipesList.innerHTML = `<div class="recipe-card" style="color:var(--error);text-align:center;padding:2rem">
229
+ <h4>Error</h4><p>Something went wrong. Try again or add a Gemini API key for better results.</p>
230
  </div>`;
231
  }
232
  } finally {
233
  state.isProcessing = false;
234
  abortController = null;
235
+ updateScanButton(false);
 
236
  }
237
  }
238
 
 
241
  function displayIngredients(ingredients) {
242
  elements.ingredientsList.innerHTML = '';
243
  ingredients.forEach((ing, i) => {
244
+ const conf = Math.round((ing.confidence || 0.7) * 100);
245
  const color = conf >= 70 ? "#2ecc71" : conf >= 40 ? "#f1c40f" : "#e74c3c";
246
 
247
  const item = document.createElement('div');
248
  item.className = 'ingredient-item';
249
+ item.style.animation = `fadeIn 2s ease-out ${i * 0.1}s forwards`;
250
  item.innerHTML = `
251
  <div class="ingredient-header">
252
  <span class="ingredient-name">${ing.name}</span>
 
255
  </span>
256
  </div>
257
  <div class="confidence-bar">
258
+ <div class="confidence-fill" style="background:${color};width:0%"></div>
259
  </div>`;
260
  elements.ingredientsList.appendChild(item);
261
+ setTimeout(() => item.querySelector('.confidence-fill').style.width = `${conf}%`, 100);
 
 
 
262
  });
263
  }
264
 
 
267
  <style>
268
  @keyframes fadeIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}
269
  .ingredient-item{opacity:0}
270
+ .scan-button.small{padding:0.5rem 1rem;font-size:0.9rem}
271
  </style>`);
272
 
273
  // Start app