Mosako/chef-guru
0
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 Chef | Precision Cooking Engine</title>
7
8<style>
9:root {
10 --bg: #0f0f0f;
11 --card: #1b1b1b;
12 --glass: rgba(255,255,255,0.05);
13 --accent: #FF9800;
14 --accent-soft: rgba(255,152,0,0.15);
15 --text: #ffffff;
16 --muted: #aaaaaa;
17 --danger: #ff5252;
18 --success: #4CAF50;
19}
20
21* { box-sizing: border-box; }
22
23body {
24 margin: 0;
25 background: radial-gradient(circle at top, #1a1a1a 0%, #0f0f0f 60%);
26 font-family: 'Segoe UI', system-ui, sans-serif;
27 color: var(--text);
28 padding: 30px 15px;
29}
30
31.container {
32 max-width: 900px;
33 margin: auto;
34}
35
36header {
37 text-align: center;
38 margin-bottom: 40px;
39}
40
41h1 {
42 margin: 0;
43 font-size: 2.8rem;
44 letter-spacing: 2px;
45}
46
47.subtitle {
48 color: var(--accent);
49 font-weight: 600;
50 margin-top: 8px;
51}
52
53/* --- CARD STYLE --- */
54.card {
55 background: var(--card);
56 padding: 30px;
57 border-radius: 18px;
58 box-shadow: 0 15px 40px rgba(0,0,0,0.6);
59 backdrop-filter: blur(10px);
60}
61
62/* --- INPUT BUILDER --- */
63.input-row {
64 display: flex;
65 gap: 10px;
66 margin-bottom: 10px;
67 animation: fadeIn 0.3s ease;
68}
69
70@keyframes fadeIn {
71 from { opacity: 0; transform: translateY(-8px); }
72 to { opacity: 1; transform: translateY(0); }
73}
74
75input {
76 flex: 1;
77 padding: 14px;
78 border-radius: 10px;
79 border: 1px solid #333;
80 background: #262626;
81 color: white;
82 font-size: 15px;
83 transition: 0.2s;
84}
85
86input:focus {
87 border-color: var(--accent);
88 box-shadow: 0 0 0 2px var(--accent-soft);
89 outline: none;
90}
91
92/* --- BUTTONS --- */
93.btn {
94 padding: 14px 20px;
95 border-radius: 10px;
96 font-weight: 600;
97 cursor: pointer;
98 border: none;
99 transition: 0.2s ease;
100}
101
102.btn-add {
103 background: transparent;
104 border: 1px solid var(--accent);
105 color: var(--accent);
106}
107
108.btn-remove {
109 background: var(--danger);
110 color: white;
111 padding: 0 14px;
112}
113
114.btn-submit {
115 background: linear-gradient(135deg, #ff9800, #ffb74d);
116 color: black;
117 width: 100%;
118 margin-top: 15px;
119 font-size: 1.05rem;
120}
121
122.btn:hover { transform: translateY(-2px); opacity: 0.95; }
123.btn:disabled { opacity: 0.5; cursor: not-allowed; }
124
125/* --- LOADER --- */
126#loader {
127 display: none;
128 text-align: center;
129 margin: 40px 0;
130}
131
132.spinner {
133 width: 55px;
134 height: 55px;
135 border: 6px solid #333;
136 border-top: 6px solid var(--accent);
137 border-radius: 50%;
138 animation: spin 1s linear infinite;
139 margin: auto;
140}
141
142@keyframes spin {
143 to { transform: rotate(360deg); }
144}
145
146/* --- RECIPE CARD --- */
147#recipe-card {
148 display: none;
149 margin-top: 35px;
150}
151
152.recipe-title {
153 color: var(--accent);
154 font-size: 2rem;
155 margin-bottom: 10px;
156}
157
158.recipe-desc {
159 color: var(--muted);
160 line-height: 1.6;
161 margin-bottom: 25px;
162}
163
164.section-title {
165 margin-top: 30px;
166 margin-bottom: 15px;
167 font-size: 0.9rem;
168 text-transform: uppercase;
169 letter-spacing: 1px;
170 color: var(--accent);
171 border-bottom: 1px solid #333;
172 padding-bottom: 6px;
173}
174
175/* --- INGREDIENTS --- */
176.ing-item {
177 padding: 8px 0;
178 border-bottom: 1px solid #2a2a2a;
179}
180
181/* --- STEPS --- */
182.step-container {
183 background: #222;
184 padding: 16px;
185 border-radius: 12px;
186 margin-bottom: 12px;
187 cursor: pointer;
188 transition: 0.2s;
189 border-left: 4px solid var(--accent);
190}
191
192.step-container:hover {
193 background: #2b2b2b;
194}
195
196.step-header {
197 display: flex;
198 justify-content: space-between;
199 align-items: flex-start;
200}
201
202.step-text {
203 font-weight: 600;
204}
205
206.step-time {
207 background: #333;
208 padding: 5px 10px;
209 border-radius: 20px;
210 font-size: 0.8rem;
211 color: var(--accent);
212}
213
214.step-desc {
215 font-size: 0.9rem;
216 color: var(--muted);
217 margin-top: 8px;
218}
219
220/* --- EXPLANATION PANEL --- */
221.step-detail {
222 display: none;
223 margin-top: 15px;
224 background: #181818;
225 padding: 18px;
226 border-radius: 10px;
227 border: 1px solid #333;
228 font-size: 0.9rem;
229 line-height: 1.6;
230}
231
232.detail-section {
233 margin-bottom: 14px;
234}
235
236.detail-title {
237 color: var(--accent);
238 font-weight: 600;
239 margin-bottom: 4px;
240}
241
242.error-msg {
243 display: none;
244 background: rgba(255,82,82,0.1);
245 padding: 15px;
246 border-radius: 10px;
247 margin-top: 20px;
248 color: var(--danger);
249 text-align: center;
250}
251</style>
252</head>
253
254<body>
255
256<div class="container">
257
258<header>
259 <h1>๐จโ๐ณ AI CHEF</h1>
260 <div class="subtitle">Precision Local LLM Cooking Engine</div>
261</header>
262
263<div class="card">
264 <div id="ingredients-list">
265 <div class="input-row">
266 <input type="text" class="ingredient-input" placeholder="Enter ingredient..." onkeypress="handleEnter(event)">
267 </div>
268 </div>
269
270 <div style="display:flex; gap:10px;">
271 <button class="btn btn-add" onclick="addIngredientField()" style="flex:1;">+ Add Ingredient</button>
272 </div>
273
274 <button class="btn btn-submit" onclick="submitRecipe()" id="submit-btn">Generate Recipe</button>
275</div>
276
277<div id="error-box" class="error-msg"></div>
278
279<div id="loader">
280 <div class="spinner"></div>
281 <p>Analyzing culinary compatibility...</p>
282</div>
283
284<div id="recipe-card" class="card">
285 <h2 class="recipe-title" id="r-title"></h2>
286 <p class="recipe-desc" id="r-desc"></p>
287
288 <div class="section-title">Ingredients</div>
289 <div id="r-ingredients"></div>
290
291 <div class="section-title">Instructions (Click for Deep Explanation)</div>
292 <div id="r-instructions"></div>
293</div>
294
295</div>
296
297<script>
298function addIngredientField() {
299 const container = document.getElementById('ingredients-list');
300 const div = document.createElement('div');
301 div.className = 'input-row';
302 div.innerHTML = `
303 <input type="text" class="ingredient-input" placeholder="Enter ingredient..." onkeypress="handleEnter(event)">
304 <button class="btn btn-remove" onclick="removeField(this)">ร</button>
305 `;
306 container.appendChild(div);
307}
308
309function removeField(btn) { btn.parentElement.remove(); }
310
311function handleEnter(e) {
312 if (e.key === 'Enter') {
313 e.preventDefault();
314 addIngredientField();
315 }
316}
317
318async function submitRecipe() {
319 const inputs = document.querySelectorAll('.ingredient-input');
320 const submitBtn = document.getElementById('submit-btn');
321 const loader = document.getElementById('loader');
322 const card = document.getElementById('recipe-card');
323 const errorBox = document.getElementById('error-box');
324
325 errorBox.style.display = 'none';
326 card.style.display = 'none';
327
328 let ingredientsList = [];
329 inputs.forEach(i => { if (i.value.trim()) ingredientsList.push(i.value.trim()); });
330
331 if (!ingredientsList.length) {
332 showError("Add at least one ingredient.");
333 return;
334 }
335
336 submitBtn.disabled = true;
337 loader.style.display = 'block';
338
339 try {
340 const response = await fetch('/generate', {
341 method: 'POST',
342 headers: { 'Content-Type': 'application/json' },
343 body: JSON.stringify({ ingredients: ingredientsList })
344 });
345
346 const data = await response.json();
347 if (!response.ok) throw new Error(data.error);
348
349 document.getElementById('r-title').innerText = data.title;
350 document.getElementById('r-desc').innerText = data.description;
351
352 const ingDiv = document.getElementById('r-ingredients');
353 ingDiv.innerHTML = data.ingredients.map(i => `<div class="ing-item">โข ${i}</div>`).join('');
354
355 const instDiv = document.getElementById('r-instructions');
356 instDiv.innerHTML = data.instructions.map((step, index) => `
357 <div class="step-container" onclick="toggleExplanation(this, '${index}')">
358 <div class="step-header">
359 <div class="step-text">Step ${index+1}: ${step.step}</div>
360 ${step.timing ? `<div class="step-time">${step.timing}</div>` : ''}
361 </div>
362 ${step.description ? `<div class="step-desc">${step.description}</div>` : ''}
363 <div id="explain-${index}" class="step-detail" data-loaded="false"></div>
364 </div>
365 `).join('');
366
367 window.currentRecipeDescription = data.description;
368 card.style.display = 'block';
369
370 } catch (e) {
371 showError(e.message);
372 } finally {
373 submitBtn.disabled = false;
374 loader.style.display = 'none';
375 }
376}
377
378async function toggleExplanation(element, index) {
379 const box = document.getElementById(`explain-${index}`);
380 const stepText = element.querySelector('.step-text').innerText.replace(`Step ${parseInt(index)+1}: `,'');
381
382 if (box.style.display === 'block') {
383 box.style.display = 'none';
384 return;
385 }
386
387 if (box.dataset.loaded === "true") {
388 box.style.display = 'block';
389 return;
390 }
391
392 box.style.display = 'block';
393 box.innerHTML = "Thinking...";
394
395 try {
396 const response = await fetch('/explain_step', {
397 method: 'POST',
398 headers: { 'Content-Type': 'application/json' },
399 body: JSON.stringify({
400 step: stepText,
401 context: window.currentRecipeDescription
402 })
403 });
404
405 const data = await response.json();
406 if (data.error) throw new Error(data.error);
407
408 const e = data.explanation || data;
409 console.log("data:", data);
410 console.log("e:", e);
411
412
413
414 box.innerHTML = `
415 ${renderSection("Purpose", e.purpose)}
416 ${renderSection("What Is Happening", e.what_is_happening)}
417 ${renderSection("Visual Indicators", e.visual_indicators)}
418 ${renderSection("Aroma & Sound", e.aroma_and_sound)}
419 ${renderSection("Texture Change", e.texture_transformation)}
420 ${renderSection("Heat Control", e.heat_control)}
421 ${renderList("Common Errors", e.common_errors)}
422 ${renderList("How To Fix", e.how_to_fix_errors)}
423 ${renderSection("Expert Tip", e.expert_tip)}
424 ${renderSection("Safety Note", e.safety_note)}
425 `;
426
427 box.dataset.loaded = "true";
428
429 } catch (err) {
430 box.innerHTML = `<span style="color:var(--danger)">Error: ${err.message}</span>`;
431 }
432}
433
434function renderSection(title, content) {
435 if (!content) return "";
436 return `
437 <div class="detail-section">
438 <div class="detail-title">${title}</div>
439 <div>${content}</div>
440 </div>
441 `;
442}
443
444function renderList(title, items) {
445 if (!items || !items.length) return "";
446 return `
447 <div class="detail-section">
448 <div class="detail-title">${title}</div>
449 <ul>${items.map(i => `<li>${i}</li>`).join('')}</ul>
450 </div>
451 `;
452}
453
454function showError(msg) {
455 const box = document.getElementById('error-box');
456 box.innerText = msg;
457 box.style.display = 'block';
458}
459</script>
460
461</body>
462</html>