| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>FoodAI - 智能食材识别 | Smart Ingredient Recognition</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| } |
| .animate-pulse { |
| animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; |
| } |
| .camera-circle { |
| box-shadow: 0 0 0 12px rgba(255, 255, 255, 0.2); |
| } |
| .ingredient-tag { |
| transition: all 0.3s ease; |
| } |
| .ingredient-tag:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| } |
| .recipe-card:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
| } |
| .slide-up { |
| animation: slideUp 0.5s ease-out forwards; |
| } |
| @keyframes slideUp { |
| from { transform: translateY(100px); opacity: 0; } |
| to { transform: translateY(0); opacity: 1; } |
| } |
| .lang-btn.active { |
| background-color: rgba(255,255,255,0.3); |
| font-weight: bold; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 font-sans"> |
| <div class="max-w-md mx-auto bg-white min-h-screen shadow-lg overflow-hidden"> |
| |
| <header class="bg-gradient-to-r from-green-500 to-emerald-600 text-white p-6"> |
| <div class="flex justify-between items-center"> |
| <div> |
| <h1 class="text-2xl font-bold">FoodAI</h1> |
| <p class="text-sm opacity-90" id="appSubtitle">智能烹饪助手 | Smart cooking companion</p> |
| </div> |
| <div class="flex space-x-4"> |
| <div class="flex space-x-1 bg-white bg-opacity-20 rounded-full p-1"> |
| <button class="lang-btn px-2 py-1 rounded-full text-xs active" data-lang="zh">中文</button> |
| <button class="lang-btn px-2 py-1 rounded-full text-xs" data-lang="en">EN</button> |
| </div> |
| <button class="p-2 rounded-full bg-white bg-opacity-20 hover:bg-opacity-30 transition" id="settingsBtn"> |
| <i class="fas fa-cog"></i> |
| </button> |
| </div> |
| </div> |
| </header> |
|
|
| |
| <main class="p-6"> |
| |
| <div id="cameraSection" class="mb-8 slide-up"> |
| <div class="text-center mb-6"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-2" id="cameraTitle">厨房里有什么? | What's in your kitchen?</h2> |
| <p class="text-gray-600" id="cameraSubtitle">拍摄您的食材照片,我们将推荐美味食谱 | Snap a photo of your ingredients and we'll suggest delicious recipes</p> |
| </div> |
| |
| <div class="relative"> |
| <div class="bg-gray-100 rounded-2xl overflow-hidden h-64 flex items-center justify-center"> |
| |
| <div id="cameraPlaceholder" class="text-center"> |
| <div class="mx-auto w-20 h-20 bg-white rounded-full flex items-center justify-center camera-circle mb-4"> |
| <i class="fas fa-camera text-emerald-500 text-2xl"></i> |
| </div> |
| <p class="text-gray-500" id="cameraPrompt">点击拍摄食材 | Tap to capture ingredients</p> |
| </div> |
| |
| <div id="cameraView" class="hidden w-full h-full"></div> |
| </div> |
| |
| <div class="flex justify-center mt-6 space-x-4"> |
| <button id="captureBtn" class="bg-emerald-500 hover:bg-emerald-600 text-white rounded-full w-16 h-16 flex items-center justify-center shadow-lg"> |
| <i class="fas fa-camera text-2xl"></i> |
| </button> |
| <button id="uploadBtn" class="bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 rounded-full w-16 h-16 flex items-center justify-center shadow-lg"> |
| <i class="fas fa-images text-2xl"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="ingredientsSection" class="hidden mb-8"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="font-medium text-gray-800" id="ingredientsTitle">检测到的食材 | Detected Ingredients</h3> |
| <button id="rescanBtn" class="text-sm text-emerald-600 hover:text-emerald-700" data-zh="重新扫描" data-en="Rescan">重新扫描 | Rescan</button> |
| </div> |
| |
| <div id="ingredientsList" class="flex flex-wrap gap-2"> |
| |
| </div> |
| |
| <div class="mt-4"> |
| <button id="addManuallyBtn" class="text-sm text-gray-600 hover:text-gray-800 flex items-center" data-zh="手动添加" data-en="Add manually"> |
| <i class="fas fa-plus-circle mr-1"></i> <span>手动添加 | Add manually</span> |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="preferencesSection" class="hidden mb-8"> |
| <h3 class="font-medium text-gray-800 mb-3" id="preferencesTitle">您的偏好 | Your Preferences</h3> |
| |
| <div class="space-y-4"> |
| <div> |
| <label class="block text-sm text-gray-600 mb-1" id="dietLabel">饮食限制 | Dietary Restrictions</label> |
| <div class="flex flex-wrap gap-2"> |
| <button class="preference-btn" data-pref="vegetarian" data-zh="素食" data-en="Vegetarian">素食 | Vegetarian</button> |
| <button class="preference-btn" data-pref="vegan" data-zh="纯素" data-en="Vegan">纯素 | Vegan</button> |
| <button class="preference-btn" data-pref="gluten-free" data-zh="无麸质" data-en="Gluten-Free">无麸质 | Gluten-Free</button> |
| <button class="preference-btn" data-pref="dairy-free" data-zh="无乳制品" data-en="Dairy-Free">无乳制品 | Dairy-Free</button> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm text-gray-600 mb-1" id="cuisineLabel">菜系偏好 | Cuisine Preference</label> |
| <div class="flex flex-wrap gap-2"> |
| <button class="preference-btn" data-pref="italian" data-zh="意大利" data-en="Italian">意大利 | Italian</button> |
| <button class="preference-btn" data-pref="asian" data-zh="亚洲" data-en="Asian">亚洲 | Asian</button> |
| <button class="preference-btn" data-pref="mexican" data-zh="墨西哥" data-en="Mexican">墨西哥 | Mexican</button> |
| <button class="preference-btn" data-pref="mediterranean" data-zh="地中海" data-en="Mediterranean">地中海 | Mediterranean</button> |
| </div> |
| </div> |
| |
| <div> |
| <label class="block text-sm text-gray-600 mb-1" id="timeLabel">烹饪时间 | Cooking Time</label> |
| <div class="flex flex-wrap gap-2"> |
| <button class="preference-btn" data-pref="quick" data-zh="快速 (30分钟内)" data-en="Quick (under 30min)">快速 (30分钟内) | Quick (under 30min)</button> |
| <button class="preference-btn" data-pref="medium" data-zh="中等 (30-60分钟)" data-en="Medium (30-60min)">中等 (30-60分钟) | Medium (30-60min)</button> |
| <button class="preference-btn" data-pref="slow" data-zh="慢煮 (60分钟以上)" data-en="Slow (60min+)">慢煮 (60分钟以上) | Slow (60min+)</button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="mt-6"> |
| <button id="findRecipesBtn" class="w-full bg-emerald-500 hover:bg-emerald-600 text-white py-3 px-4 rounded-lg font-medium transition" data-zh="查找食谱" data-en="Find Recipes"> |
| 查找食谱 | Find Recipes |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="recipesSection" class="hidden"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="font-medium text-gray-800" id="recipesTitle">推荐食谱 | Recommended Recipes</h3> |
| <button id="newSearchBtn" class="text-sm text-emerald-600 hover:text-emerald-700" data-zh="新的搜索" data-en="New Search">新的搜索 | New Search</button> |
| </div> |
| |
| <div id="recipesList" class="space-y-4"> |
| |
| </div> |
| </div> |
|
|
| |
| <div id="loadingSection" class="hidden flex flex-col items-center justify-center py-12"> |
| <div class="animate-spin rounded-full h-16 w-16 border-t-4 border-b-4 border-emerald-500 mb-4"></div> |
| <p class="text-gray-600" id="loadingText">正在分析您的食材... | Analyzing your ingredients...</p> |
| <p class="text-sm text-gray-500 mt-1" id="loadingSubtext">正在为您寻找完美食谱 | Finding the perfect recipes for you</p> |
| </div> |
| </main> |
|
|
| |
| <nav class="bg-white border-t border-gray-200 p-4"> |
| <div class="flex justify-around"> |
| <button class="text-emerald-600 p-2 rounded-full"> |
| <i class="fas fa-home text-xl"></i> |
| </button> |
| <button class="text-gray-500 hover:text-emerald-600 p-2 rounded-full"> |
| <i class="fas fa-search text-xl"></i> |
| </button> |
| <button class="bg-emerald-500 text-white p-4 rounded-full -mt-6 shadow-lg"> |
| <i class="fas fa-camera text-xl"></i> |
| </button> |
| <button class="text-gray-500 hover:text-emerald-600 p-2 rounded-full"> |
| <i class="fas fa-heart text-xl"></i> |
| </button> |
| <button class="text-gray-500 hover:text-emerald-600 p-2 rounded-full"> |
| <i class="fas fa-user text-xl"></i> |
| </button> |
| </div> |
| </nav> |
| </div> |
|
|
| <script> |
| |
| const sampleIngredients = [ |
| { name: "Tomato", name_zh: "番茄", confidence: 92 }, |
| { name: "Onion", name_zh: "洋葱", confidence: 88 }, |
| { name: "Garlic", name_zh: "大蒜", confidence: 85 }, |
| { name: "Chicken", name_zh: "鸡肉", confidence: 95 }, |
| { name: "Bell Pepper", name_zh: "甜椒", confidence: 78 } |
| ]; |
| |
| const sampleRecipes = [ |
| { |
| name: "Chicken Stir Fry", |
| name_zh: "鸡肉炒菜", |
| ingredients: ["Chicken", "Bell Pepper", "Onion", "Garlic"], |
| ingredients_zh: ["鸡肉", "甜椒", "洋葱", "大蒜"], |
| time: "25 min", |
| difficulty: "Easy", |
| difficulty_zh: "简单", |
| rating: 4.5, |
| image: "https://images.unsplash.com/photo-1626804471355-3a98a8c0e212?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=500&q=80" |
| }, |
| { |
| name: "Tomato Basil Pasta", |
| name_zh: "番茄罗勒意面", |
| ingredients: ["Tomato", "Garlic", "Onion"], |
| ingredients_zh: ["番茄", "大蒜", "洋葱"], |
| time: "20 min", |
| difficulty: "Easy", |
| difficulty_zh: "简单", |
| rating: 4.2, |
| image: "https://images.unsplash.com/photo-1611270629569-8b357cb88da9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=500&q=80" |
| }, |
| { |
| name: "Chicken Fajitas", |
| name_zh: "鸡肉法吉塔", |
| ingredients: ["Chicken", "Bell Pepper", "Onion"], |
| ingredients_zh: ["鸡肉", "甜椒", "洋葱"], |
| time: "35 min", |
| difficulty: "Medium", |
| difficulty_zh: "中等", |
| rating: 4.7, |
| image: "https://images.unsplash.com/photo-1574897578259-a05fea79a1cd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=500&q=80" |
| } |
| ]; |
| |
| |
| const cameraSection = document.getElementById('cameraSection'); |
| const ingredientsSection = document.getElementById('ingredientsSection'); |
| const preferencesSection = document.getElementById('preferencesSection'); |
| const recipesSection = document.getElementById('recipesSection'); |
| const loadingSection = document.getElementById('loadingSection'); |
| const ingredientsList = document.getElementById('ingredientsList'); |
| const recipesList = document.getElementById('recipesList'); |
| const captureBtn = document.getElementById('captureBtn'); |
| const uploadBtn = document.getElementById('uploadBtn'); |
| const rescanBtn = document.getElementById('rescanBtn'); |
| const addManuallyBtn = document.getElementById('addManuallyBtn'); |
| const findRecipesBtn = document.getElementById('findRecipesBtn'); |
| const newSearchBtn = document.getElementById('newSearchBtn'); |
| const preferenceBtns = document.querySelectorAll('.preference-btn'); |
| const langBtns = document.querySelectorAll('.lang-btn'); |
| |
| |
| let selectedPreferences = new Set(); |
| let currentLang = 'zh'; |
| |
| |
| captureBtn.addEventListener('click', simulateCapture); |
| uploadBtn.addEventListener('click', simulateUpload); |
| rescanBtn.addEventListener('click', resetToCamera); |
| addManuallyBtn.addEventListener('click', addManualIngredient); |
| findRecipesBtn.addEventListener('click', findRecipes); |
| newSearchBtn.addEventListener('click', resetToCamera); |
| |
| |
| langBtns.forEach(btn => { |
| btn.addEventListener('click', function() { |
| currentLang = this.getAttribute('data-lang'); |
| updateLanguage(); |
| |
| |
| langBtns.forEach(b => b.classList.remove('active')); |
| this.classList.add('active'); |
| }); |
| }); |
| |
| preferenceBtns.forEach(btn => { |
| btn.addEventListener('click', function() { |
| const pref = this.getAttribute('data-pref'); |
| if (selectedPreferences.has(pref)) { |
| selectedPreferences.delete(pref); |
| this.classList.remove('bg-emerald-500', 'text-white'); |
| this.classList.add('bg-gray-100', 'text-gray-700'); |
| } else { |
| selectedPreferences.add(pref); |
| this.classList.add('bg-emerald-500', 'text-white'); |
| this.classList.remove('bg-gray-100', 'text-gray-700'); |
| } |
| }); |
| }); |
| |
| |
| function updateLanguage() { |
| |
| document.querySelectorAll('[data-zh], [data-en]').forEach(el => { |
| if (el.tagName === 'BUTTON' || el.tagName === 'LABEL' || el.tagName === 'P' || el.tagName === 'H1' || el.tagName === 'H2' || el.tagName === 'H3' || el.tagName === 'SPAN') { |
| el.textContent = el.getAttribute(`data-${currentLang}`); |
| } |
| }); |
| |
| |
| document.getElementById('appSubtitle').textContent = currentLang === 'zh' ? '智能烹饪助手' : 'Smart cooking companion'; |
| document.getElementById('cameraTitle').textContent = currentLang === 'zh' ? '厨房里有什么?' : 'What\'s in your kitchen?'; |
| document.getElementById('cameraSubtitle').textContent = currentLang === 'zh' ? '拍摄您的食材照片,我们将推荐美味食谱' : 'Snap a photo of your ingredients and we\'ll suggest delicious recipes'; |
| document.getElementById('cameraPrompt').textContent = currentLang === 'zh' ? '点击拍摄食材' : 'Tap to capture ingredients'; |
| document.getElementById('ingredientsTitle').textContent = currentLang === 'zh' ? '检测到的食材' : 'Detected Ingredients'; |
| document.getElementById('preferencesTitle').textContent = currentLang === 'zh' ? '您的偏好' : 'Your Preferences'; |
| document.getElementById('dietLabel').textContent = currentLang === 'zh' ? '饮食限制' : 'Dietary Restrictions'; |
| document.getElementById('cuisineLabel').textContent = currentLang === 'zh' ? '菜系偏好' : 'Cuisine Preference'; |
| document.getElementById('timeLabel').textContent = currentLang === 'zh' ? '烹饪时间' : 'Cooking Time'; |
| document.getElementById('recipesTitle').textContent = currentLang === 'zh' ? '推荐食谱' : 'Recommended Recipes'; |
| document.getElementById('loadingText').textContent = currentLang === 'zh' ? '正在分析您的食材...' : 'Analyzing your ingredients...'; |
| document.getElementById('loadingSubtext').textContent = currentLang === 'zh' ? '正在为您寻找完美食谱' : 'Finding the perfect recipes for you'; |
| |
| |
| if (!ingredientsSection.classList.contains('hidden')) { |
| displayIngredients(); |
| } |
| if (!recipesSection.classList.contains('hidden')) { |
| displayRecipes(); |
| } |
| } |
| |
| function simulateCapture() { |
| cameraSection.classList.add('hidden'); |
| loadingSection.classList.remove('hidden'); |
| |
| |
| setTimeout(() => { |
| loadingSection.classList.add('hidden'); |
| ingredientsSection.classList.remove('hidden'); |
| preferencesSection.classList.remove('hidden'); |
| displayIngredients(); |
| }, 2000); |
| } |
| |
| function simulateUpload() { |
| |
| simulateCapture(); |
| } |
| |
| function displayIngredients() { |
| ingredientsList.innerHTML = ''; |
| sampleIngredients.forEach(ingredient => { |
| const ingredientEl = document.createElement('div'); |
| ingredientEl.className = 'ingredient-tag bg-emerald-100 text-emerald-800 px-3 py-1 rounded-full text-sm flex items-center'; |
| const displayName = currentLang === 'zh' ? ingredient.name_zh : ingredient.name; |
| ingredientEl.innerHTML = ` |
| ${displayName} |
| <span class="ml-1 text-xs opacity-70">${ingredient.confidence}%</span> |
| <button class="ml-2 text-emerald-600 hover:text-emerald-800"> |
| <i class="fas fa-times"></i> |
| </button> |
| `; |
| ingredientsList.appendChild(ingredientEl); |
| |
| |
| ingredientEl.querySelector('button').addEventListener('click', function() { |
| ingredientEl.remove(); |
| }); |
| }); |
| } |
| |
| function addManualIngredient() { |
| const promptText = currentLang === 'zh' ? '输入食材名称:' : 'Enter ingredient name:'; |
| const ingredientName = prompt(promptText); |
| if (ingredientName) { |
| const ingredientEl = document.createElement('div'); |
| ingredientEl.className = 'ingredient-tag bg-blue-100 text-blue-800 px-3 py-1 rounded-full text-sm flex items-center'; |
| ingredientEl.innerHTML = ` |
| ${ingredientName} |
| <button class="ml-2 text-blue-600 hover:text-blue-800"> |
| <i class="fas fa-times"></i> |
| </button> |
| `; |
| ingredientsList.appendChild(ingredientEl); |
| |
| |
| ingredientEl.querySelector('button').addEventListener('click', function() { |
| ingredientEl.remove(); |
| }); |
| } |
| } |
| |
| function findRecipes() { |
| ingredientsSection.classList.add('hidden'); |
| preferencesSection.classList.add('hidden'); |
| loadingSection.classList.remove('hidden'); |
| |
| |
| setTimeout(() => { |
| loadingSection.classList.add('hidden'); |
| recipesSection.classList.remove('hidden'); |
| displayRecipes(); |
| }, 1500); |
| } |
| |
| function displayRecipes() { |
| recipesList.innerHTML = ''; |
| sampleRecipes.forEach(recipe => { |
| const recipeEl = document.createElement('div'); |
| recipeEl.className = 'recipe-card bg-white rounded-xl overflow-hidden shadow-md transition duration-300'; |
| const displayName = currentLang === 'zh' ? recipe.name_zh : recipe.name; |
| const displayIngredients = currentLang === 'zh' ? recipe.ingredients_zh.join(', ') : recipe.ingredients.join(', '); |
| const displayDifficulty = currentLang === 'zh' ? recipe.difficulty_zh : recipe.difficulty; |
| |
| recipeEl.innerHTML = ` |
| <div class="relative h-40"> |
| <img src="${recipe.image}" alt="${displayName}" class="w-full h-full object-cover"> |
| <div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-4"> |
| <h4 class="text-white font-semibold text-lg">${displayName}</h4> |
| </div> |
| <div class="absolute top-2 right-2 bg-white/90 text-emerald-600 px-2 py-1 rounded-full text-xs font-medium"> |
| ${recipe.time} |
| </div> |
| </div> |
| <div class="p-4"> |
| <div class="flex justify-between items-center mb-2"> |
| <span class="text-sm text-gray-600">${displayIngredients}</span> |
| <div class="flex items-center"> |
| <i class="fas fa-star text-yellow-400 mr-1"></i> |
| <span class="text-sm font-medium">${recipe.rating}</span> |
| </div> |
| </div> |
| <div class="flex justify-between items-center"> |
| <span class="text-xs text-gray-500">${currentLang === 'zh' ? '难度: ' : 'Difficulty: '}${displayDifficulty}</span> |
| <button class="text-emerald-600 hover:text-emerald-700 text-sm font-medium"> |
| ${currentLang === 'zh' ? '查看食谱' : 'View Recipe'} <i class="fas fa-chevron-right ml-1"></i> |
| </button> |
| </div> |
| </div> |
| `; |
| recipesList.appendChild(recipeEl); |
| }); |
| } |
| |
| function resetToCamera() { |
| ingredientsSection.classList.add('hidden'); |
| preferencesSection.classList.add('hidden'); |
| recipesSection.classList.add('hidden'); |
| loadingSection.classList.add('hidden'); |
| cameraSection.classList.remove('hidden'); |
| selectedPreferences.clear(); |
| |
| |
| preferenceBtns.forEach(btn => { |
| btn.classList.remove('bg-emerald-500', 'text-white'); |
| btn.classList.add('bg-gray-100', 'text-gray-700'); |
| }); |
| } |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=googleman/cgb2025" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |