import React, { useState, useRef, useEffect } from 'react'; import Chart from 'chart.js/auto'; import './AIFoodAnalyzer.css'; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://127.0.0.1:8000'; const defaultProfile = { name: '', age: '', gender: '', height: '', weight: '', activityLevel: 'sedentary', healthGoal: 'maintain', dailyCalories: 0, proteinGoal: 0, fiberGoal: 25, waterGoal: 2000 }; function AIFoodAnalyzer() { // 分頁狀態 const [tab, setTab] = useState('analyzer'); // 個人資料 const [profile, setProfile] = useState(() => { const stored = localStorage.getItem('userProfile'); return stored ? JSON.parse(stored) : { ...defaultProfile }; }); // 食物日記 const [foodDiary, setFoodDiary] = useState([]); // 分析狀態 const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [result, setResult] = useState(null); // 相機/上傳 const videoRef = useRef(null); const canvasRef = useRef(null); const fileInputRef = useRef(null); const [stream, setStream] = useState(null); // Chart const chartRef = useRef(null); // 新增一個 state 來存放預覽圖片的 URL const [previewImage, setPreviewImage] = useState(null); // 初始化日記 useEffect(() => { loadFoodDiary(); // 清理相機 return () => { if (stream) stream.getTracks().forEach(t => t.stop()); }; }, []); // 分頁切換時載入日記 useEffect(() => { if (tab === 'tracking') loadFoodDiary(); }, [tab]); // 分頁切換 const switchTab = (t) => { setTab(t); setError(''); setResult(null); }; // 個人資料儲存 const handleProfileChange = e => { const { name, value } = e.target; setProfile(p => ({ ...p, [name]: value })); }; const saveProfile = e => { e.preventDefault(); // 計算每日建議 const bmr = profile.gender === 'male' ? 88.362 + (13.397 * profile.weight) + (4.799 * profile.height) - (5.677 * profile.age) : 447.593 + (9.247 * profile.weight) + (3.098 * profile.height) - (4.330 * profile.age); const activityMultipliers = { sedentary: 1.2, light: 1.375, moderate: 1.55, active: 1.725, extra: 1.9 }; let calories = bmr * (activityMultipliers[profile.activityLevel] || 1.2); const goalAdjustments = { lose: -300, gain: 300, muscle: 200 }; calories += goalAdjustments[profile.healthGoal] || 0; const dailyCalories = Math.round(calories); const proteinGoal = Math.round(dailyCalories * 0.25 / 4); setProfile(p => { const newP = { ...p, dailyCalories, proteinGoal, fiberGoal: 25, waterGoal: 2000 }; localStorage.setItem('userProfile', JSON.stringify(newP)); return newP; }); alert('個人資料已儲存!'); setTab('analyzer'); }; // 日記 function loadFoodDiary() { const today = new Date().toISOString().slice(0, 10); const stored = localStorage.getItem(`foodDiary_${today}`); setFoodDiary(stored ? JSON.parse(stored) : []); } function saveFoodDiary(diary) { const today = new Date().toISOString().slice(0, 10); localStorage.setItem(`foodDiary_${today}`, JSON.stringify(diary)); setFoodDiary(diary); } function addToFoodDiary() { if (!result) return alert('沒有可加入的分析結果。'); const meal = { ...result, id: Date.now(), timestamp: new Date().toISOString() }; const newDiary = [...foodDiary, meal]; saveFoodDiary(newDiary); alert(`${meal.foodName} 已加入記錄!`); } // 相機/圖片分析 const startCamera = async () => { setError(''); try { const mediaStream = await navigator.mediaDevices.getUserMedia({ video: true }); if (videoRef.current) { videoRef.current.srcObject = mediaStream; setStream(mediaStream); } } catch (err) { setError('無法開啟相機,請檢查權限。'); } }; const capturePhoto = () => { setError(''); const video = videoRef.current; const canvas = canvasRef.current; if (!video || !canvas) return; canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); canvas.toBlob(blob => { if (blob) { setPreviewImage(URL.createObjectURL(blob)); processImage(blob); } else setError('無法擷取圖片,請再試一次'); }, 'image/jpeg'); }; const handleFileUpload = (e) => { const file = e.target.files[0]; if (file) { setPreviewImage(URL.createObjectURL(file)); processImage(file); e.target.value = ''; } }; const processImage = async (imageSource) => { setLoading(true); setResult(null); setError(''); const formData = new FormData(); formData.append('file', imageSource); try { const aiResponse = await fetch(`${API_BASE_URL}/ai/analyze-food-image-with-weight/`, { method: 'POST', body: formData, }); if (!aiResponse.ok) { const errorData = await aiResponse.json(); throw new Error(errorData.detail || `AI辨識失敗 (狀態碼: ${aiResponse.status})`); } const aiData = await aiResponse.json(); const foodName = aiData.food_type; if (!foodName || foodName === 'Unknown') throw new Error('AI無法辨識出食物名稱。'); let analysis = { foodName, description: `AI 辨識結果:${foodName}` + (aiData.note ? `(${aiData.note})` : ''), healthIndex: 75, glycemicIndex: 50, benefits: [`含有 ${foodName} 的營養成分`], nutrition: aiData.nutrition || { calories: 150, protein: 8, carbs: 20, fat: 5, fiber: 3, sugar: 2 }, vitamins: { 'Vitamin C': 15, 'Vitamin A': 10 }, minerals: { 'Iron': 2, 'Calcium': 50 }, estimatedWeight: aiData.estimated_weight, weightConfidence: aiData.weight_confidence, weightErrorRange: aiData.weight_error_range, referenceObject: aiData.reference_object, note: aiData.note }; setResult(analysis); } catch (err) { setError(`分析失敗: ${err.message}`); } finally { setLoading(false); } }; // Chart.js 本週熱量趨勢 useEffect(() => { if (tab !== 'tracking' || !chartRef.current) return; if (chartRef.current._chart) chartRef.current._chart.destroy(); // 取得本週資料 const days = ['日','一','二','三','四','五','六']; const week = []; const now = new Date(); for (let i = 6; i >= 0; i--) { const d = new Date(now); d.setDate(now.getDate() - i); const key = d.toISOString().slice(0,10); const diary = JSON.parse(localStorage.getItem(`foodDiary_${key}`) || '[]'); const total = diary.reduce((sum, m) => sum + (m.nutrition?.calories||0), 0); week.push({ day: days[d.getDay()], calories: total }); } chartRef.current._chart = new Chart(chartRef.current.getContext('2d'), { type: 'line', data: { labels: week.map(d=>d.day), datasets: [{ label: '熱量', data: week.map(d=>d.calories), borderColor: '#667eea', backgroundColor: 'rgba(102, 126, 234, 0.1)', tension: 0.3, pointRadius: 4, fill: true }] }, options: { plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 200 } } }, responsive: true, maintainAspectRatio: false } }); }, [tab, foodDiary]); // UI return (
智能分析您的飲食營養
AI正在分析食物中...
{result.description || `這是 ${result.foodName}`}