health-assistant / frontend /ai_food_analyzer.html
yuting111222's picture
feat: 完成前端互動與後端修正
b54d48b
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI食物營養分析器 - 個人化版本</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* 全局樣式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 追蹤紀錄專用樣式 */
.tracking-container {
padding: 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
/* 統計卡片網格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-3px);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.1);
}
.stat-icon {
font-size: 2rem;
margin-right: 1rem;
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
}
.stat-info {
flex: 1;
}
.stat-value {
font-size: 1.5rem;
font-weight: 700;
color: #2c3e50;
margin-bottom: 0.25rem;
}
.stat-label {
font-size: 0.875rem;
color: #7f8c8d;
}
/* 圖表卡片 */
.chart-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.chart-card.full-width {
grid-column: 1 / -1;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.chart-header h3 {
font-size: 1.25rem;
color: #2c3e50;
margin: 0;
}
/* 進度條樣式 */
.progress-bars {
display: grid;
gap: 1.25rem;
}
.progress-item {
margin-bottom: 1rem;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
.progress-label {
font-weight: 600;
color: #2c3e50;
}
.progress-value {
color: #7f8c8d;
}
.progress-bar-container {
height: 8px;
background: #f0f2f5;
border-radius: 4px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
}
/* 食物記錄 */
.meals-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.meals-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.meals-header h3 {
margin: 0;
font-size: 1.25rem;
color: #2c3e50;
}
.btn-small {
background: #3498db;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-small:hover {
background: #2980b9;
}
.meals-list {
display: grid;
gap: 1rem;
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: #7f8c8d;
}
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.7;
}
.empty-state p {
margin: 0.5rem 0;
}
.text-muted {
color: #bdc3c7;
font-size: 0.875rem;
}
/* 主體樣式 */
body {
font-family: 'Arial', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1rem;
}
.app-container {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 2rem;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
max-width: 500px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
}
.header {
text-align: center;
margin-bottom: 2rem;
}
.title {
color: #333;
font-size: 1.8rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
.subtitle {
color: #666;
font-size: 0.9rem;
}
.tab-nav {
display: flex;
background: rgba(102, 126, 234, 0.1);
border-radius: 15px;
margin-bottom: 2rem;
padding: 0.3rem;
}
.tab-btn {
flex: 1;
padding: 0.8rem;
border: none;
background: transparent;
border-radius: 12px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
color: #666;
}
.tab-btn.active {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* 個人資料表單 */
.profile-form {
display: grid;
gap: 1.5rem;
}
.input-group {
display: flex;
flex-direction: column;
}
.input-group label {
font-weight: bold;
color: #333;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.input-group input, .input-group select {
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 1rem;
transition: border-color 0.3s;
}
.input-group input:focus, .input-group select:focus {
outline: none;
border-color: #667eea;
}
.input-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.save-profile-btn {
background: linear-gradient(45deg, #43cea2, #185a9d);
color: white;
border: none;
padding: 15px;
border-radius: 15px;
font-weight: bold;
cursor: pointer;
font-size: 1rem;
transition: transform 0.2s;
}
.save-profile-btn:hover {
transform: translateY(-2px);
}
/* 食物分析區域 */
.camera-container {
position: relative;
margin-bottom: 1.5rem;
text-align: center;
}
#video {
width: 100%;
max-width: 300px;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
background-color: #000;
}
#canvas {
display: none;
}
.controls {
display: flex;
gap: 1rem;
justify-content: center;
margin: 1.5rem 0;
flex-wrap: wrap;
}
button {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
padding: 12px 20px;
border-radius: 25px;
cursor: pointer;
font-weight: bold;
transition: transform 0.2s;
font-size: 0.9rem;
}
button:hover {
transform: translateY(-2px);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.upload-btn {
background: linear-gradient(45deg, #43cea2, #185a9d);
}
.file-input {
display: none;
}
.loading {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 1rem 0;
text-align: center;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.result {
margin-top: 2rem;
padding: 1.5rem;
background: rgba(102, 126, 234, 0.1);
border-radius: 15px;
display: none;
}
.food-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.food-name {
font-size: 1.3rem;
font-weight: bold;
color: #333;
}
.add-to-diary {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
padding: 8px 16px;
border-radius: 20px;
font-size: 0.8rem;
}
.nutrition-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.8rem;
margin-bottom: 1.5rem;
}
.nutrition-item {
background: white;
padding: 0.8rem;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
text-align: center;
}
.nutrition-label {
font-size: 0.9rem;
color: #666;
margin-bottom: 0.3rem;
}
.nutrition-value {
font-size: 1.1rem;
font-weight: bold;
color: #333;
}
.daily-recommendation {
background: linear-gradient(45deg, rgba(255, 107, 107, 0.1), rgba(238, 90, 36, 0.1));
padding: 1rem;
border-radius: 10px;
border-left: 4px solid #ff6b6b;
margin-top: 1.5rem;
}
.recommendation-title {
font-weight: bold;
color: #333;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.recommendation-text {
font-size: 0.8rem;
color: #666;
line-height: 1.4;
}
/* 健康指數樣式 */
.health-indices {
display: flex;
gap: 1rem;
margin: 1rem 0 1.5rem;
}
.health-index {
flex: 1;
background: white;
padding: 0.8rem;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.index-label {
font-size: 0.8rem;
color: #666;
margin-bottom: 0.5rem;
}
.index-meter {
height: 6px;
background: #f0f0f0;
border-radius: 3px;
margin-bottom: 0.5rem;
overflow: hidden;
}
.meter-fill {
height: 100%;
width: 0%;
border-radius: 3px;
transition: width 0.5s ease;
}
#healthIndexFill {
background: linear-gradient(45deg, #43cea2, #185a9d);
}
#glycemicIndexFill {
background: linear-gradient(45deg, #ff9a9e, #fad0c4);
}
.index-value {
font-size: 0.8rem;
font-weight: bold;
color: #333;
text-align: right;
}
.food-description {
background: rgba(102, 126, 234, 0.1);
padding: 1rem;
border-radius: 10px;
font-size: 0.9rem;
color: #555;
line-height: 1.5;
margin-bottom: 1rem;
}
.benefits-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.benefit-tag {
background: rgba(67, 206, 162, 0.1);
color: #43cea2;
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
display: inline-block;
}
.nutrition-section-title {
color: #333;
font-size: 1rem;
margin: 1.5rem 0 0.8rem;
}
.nutrition-details {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
margin-top: 1.5rem;
}
.nutrition-section {
background: white;
padding: 1rem;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
/* 統計卡片樣式修正 */
.water-card {
background: linear-gradient(135deg, #e0f7fa, #b2ebf2);
}
.water-card .stat-number {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.water-controls {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
justify-content: center;
}
.water-btn {
background: rgba(255, 255, 255, 0.7);
border: none;
border-radius: 5px;
padding: 3px 8px;
font-size: 0.75rem;
color: #0288d1;
cursor: pointer;
transition: all 0.2s;
}
.water-btn:hover {
background: #0288d1;
color: white;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #667eea;
margin-bottom: 0.5rem;
}
/* 圖表容器 */
.chart-container {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.chart-tabs {
display: flex;
background: #f5f5f5;
border-radius: 20px;
padding: 0.3rem;
}
.chart-tab {
background: transparent;
border: none;
padding: 0.5rem 1rem;
border-radius: 15px;
font-size: 0.8rem;
color: #666;
cursor: pointer;
transition: all 0.3s;
}
.chart-tab.active {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.chart-view {
display: none;
height: 250px;
}
.chart-view.active {
display: block;
}
/* 目標達成進度樣式 */
.goals-container {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.goal-items {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.2rem;
}
.goal-item {
background: #f8f9fa;
padding: 1rem;
border-radius: 10px;
}
.goal-info {
display: flex;
justify-content: space-between;
margin-bottom: 0.8rem;
}
.goal-label {
font-weight: 500;
color: #333;
}
.goal-value {
color: #666;
font-size: 0.9rem;
}
.goal-progress {
height: 8px;
background: #e9ecef;
border-radius: 4px;
overflow: hidden;
}
.progress-bar {
height: 100%;
width: 0%;
border-radius: 4px;
transition: width 0.5s ease;
}
#caloriesProgressBar {
background: linear-gradient(45deg, #ff9a9e, #fad0c4);
}
#proteinProgressBar {
background: linear-gradient(45deg, #667eea, #764ba2);
}
#waterProgressBar {
background: linear-gradient(45deg, #4facfe, #00f2fe);
}
#fiberProgressBar {
background: linear-gradient(45deg, #43cea2, #185a9d);
}
.today-meals {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.meal-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #f0f0f0;
}
.meal-item:last-child {
border-bottom: none;
}
.meal-info h4 {
color: #333;
margin-bottom: 0.3rem;
}
.meal-info span {
color: #666;
font-size: 0.8rem;
}
.meal-calories {
font-weight: bold;
color: #667eea;
}
.welcome-message {
background: linear-gradient(45deg, rgba(67, 206, 162, 0.1), rgba(24, 90, 157, 0.1));
padding: 1rem;
border-radius: 10px;
margin-bottom: 2rem;
border-left: 4px solid #43cea2;
}
.no-profile {
text-align: center;
color: #666;
padding: 2rem;
}
/* 每日總結樣式 */
.daily-summary {
background: white;
padding: 1.5rem;
border-radius: 15px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.daily-summary h3 {
color: #333;
margin-bottom: 1.2rem;
font-size: 1.2rem;
}
.nutrition-balance {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #f0f0f0;
}
.balance-chart {
flex: 0 0 150px;
}
.balance-stats {
flex: 1;
padding-left: 1.5rem;
}
.balance-item {
display: flex;
align-items: center;
margin-bottom: 0.8rem;
}
.balance-label {
flex: 1;
color: #666;
font-size: 0.9rem;
}
.balance-value {
font-weight: bold;
color: #333;
margin-right: 1rem;
}
.balance-percent {
background: #f0f0f0;
padding: 0.2rem 0.5rem;
border-radius: 10px;
font-size: 0.8rem;
color: #666;
min-width: 45px;
text-align: center;
}
.nutrition-score {
display: flex;
align-items: center;
}
.score-circle {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(45deg, #667eea, #764ba2);
display: flex;
justify-content: center;
align-items: center;
margin-right: 1.5rem;
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.3);
}
.score-number {
color: white;
font-size: 2rem;
font-weight: bold;
}
.score-details {
flex: 1;
}
.score-details h4 {
color: #333;
margin-bottom: 0.3rem;
font-size: 1.1rem;
}
.score-details p {
color: #666;
font-size: 0.85rem;
line-height: 1.4;
}
.summary-icon {
font-size: 1.5rem;
margin-right: 0.5rem;
}
.summary-item {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
}
.summary-title {
font-size: 1rem;
color: #333;
margin-bottom: 0.2rem;
}
.summary-value {
font-size: 1.2rem;
font-weight: bold;
color: #667eea;
}
.water-input {
display: flex;
flex-direction: column;
align-items: flex-start;
}
/* 響應式設計 */
@media (max-width: 768px) {
.chart-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.meals-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.btn-small {
width: 100%;
justify-content: center;
}
.nutrition-balance {
flex-direction: column;
text-align: center;
}
.nutrition-details {
grid-template-columns: 1fr;
}
.balance-stats {
padding-left: 0;
padding-top: 1rem;
}
}
@media (max-width: 480px) {
.stats-grid {
grid-template-columns: 1fr;
}
.tracking-container {
padding: 1rem;
}
.chart-card, .meals-card, .goals-container {
padding: 1rem;
}
.nutrition-details {
grid-template-columns: 1fr;
}
.goal-items {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="app-container">
<div class="header">
<h1 class="title">🍎 AI食物營養分析器</h1>
<p class="subtitle">智能分析您的飲食營養</p>
</div>
<div class="tab-nav">
<button class="tab-btn active" data-tab="analyzer" onclick="switchTab('analyzer')">分析食物</button>
<button class="tab-btn" data-tab="profile" onclick="switchTab('profile')">個人資料</button>
<button class="tab-btn" data-tab="tracking" onclick="switchTab('tracking')">營養追蹤</button>
</div>
<!-- 食物分析頁面 -->
<div id="analyzer" class="tab-content active">
<div id="profileCheck" class="no-profile" style="display: none;">
<p>請先到「個人資料」頁面完成設定,以獲得個人化營養建議和追蹤功能 👆</p>
</div>
<div id="analyzerContent">
<div class="camera-container">
<video id="video" autoplay playsinline></video>
<canvas id="canvas"></canvas>
</div>
<div class="controls">
<button onclick="startCamera()">📷 開啟相機</button>
<button onclick="capturePhoto()">📸 拍照分析</button>
<button class="upload-btn" onclick="document.getElementById('fileInput').click()">📁 上傳圖片</button>
<input type="file" id="fileInput" class="file-input" accept="image/*" onchange="handleFileUpload(event)">
</div>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>AI正在分析食物中...</p>
</div>
<div class="result" id="result">
<div class="food-info">
<h3 class="food-name" id="foodName">識別的食物</h3>
<button class="add-to-diary" onclick="addToFoodDiary()">加入飲食記錄</button>
</div>
<div class="health-indices">
<div class="health-index">
<div class="index-label">健康指數</div>
<div class="index-meter">
<div class="meter-fill" id="healthIndexFill"></div>
</div>
<div class="index-value" id="healthIndexValue">--/100</div>
</div>
<div class="health-index">
<div class="index-label">升糖指數</div>
<div class="index-meter">
<div class="meter-fill" id="glycemicIndexFill"></div>
</div>
<div class="index-value" id="glycemicIndexValue">--/100</div>
</div>
</div>
<p class="food-description" id="foodDescription">這裡會顯示食物的詳細描述和營養價值。</p>
<div class="benefits-tags" id="benefitsTags"></div>
<div class="nutrition-details">
<div class="nutrition-section">
<h4 class="nutrition-section-title">基本營養素</h4>
<div class="nutrition-grid" id="nutritionGrid"></div>
</div>
<div class="nutrition-section">
<h4 class="nutrition-section-title">維生素</h4>
<div class="nutrition-grid" id="vitaminsGrid"></div>
</div>
<div class="nutrition-section">
<h4 class="nutrition-section-title">礦物質</h4>
<div class="nutrition-grid" id="mineralsGrid"></div>
</div>
</div>
<div class="daily-recommendation" id="recommendation"></div>
</div>
</div>
</div>
<!-- 個人資料頁面 -->
<div id="profile" class="tab-content">
<div class="profile-form">
<div class="input-group">
<label for="userName">姓名</label>
<input type="text" id="userName" placeholder="請輸入您的姓名">
</div>
<div class="input-row">
<div class="input-group">
<label for="userAge">年齡</label>
<input type="number" id="userAge" placeholder="歲" min="1" max="120">
</div>
<div class="input-group">
<label for="userGender">性別</label>
<select id="userGender">
<option value="">請選擇</option>
<option value="male">男性</option>
<option value="female">女性</option>
</select>
</div>
</div>
<div class="input-row">
<div class="input-group">
<label for="userHeight">身高 (cm)</label>
<input type="number" id="userHeight" placeholder="公分" min="100" max="250">
</div>
<div class="input-group">
<label for="userWeight">體重 (kg)</label>
<input type="number" id="userWeight" placeholder="公斤" min="30" max="200">
</div>
</div>
<div class="input-group">
<label for="activityLevel">活動量</label>
<select id="activityLevel">
<option value="sedentary">久坐少動 (辦公室工作)</option>
<option value="light">輕度活動 (每週運動1-3次)</option>
<option value="moderate">中度活動 (每週運動3-5次)</option>
<option value="active">高度活動 (每週運動6-7次)</option>
<option value="extra">超高活動 (體力勞動+運動)</option>
</select>
</div>
<div class="input-group">
<label for="healthGoal">健康目標</label>
<select id="healthGoal">
<option value="lose">減重</option>
<option value="maintain">維持體重</option>
<option value="gain">增重</option>
<option value="muscle">增肌</option>
<option value="health">保持健康</option>
</select>
</div>
<button class="save-profile-btn" onclick="saveUserProfile()">💾 儲存資料</button>
</div>
</div>
<!-- 營養追蹤頁面 -->
<div id="tracking" class="tab-content">
<div id="trackingCheck" class="no-profile" style="display: none;">
<p>請先到「個人資料」頁面完成設定,以獲得個人化營養建議和追蹤功能 👆</p>
</div>
<div class="tracking-container" id="trackingContent">
<div class="welcome-message" id="welcomeMessage"></div>
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<div class="stat-icon">🔥</div>
<div class="stat-info">
<div class="stat-value" id="todayCalories">0</div>
<div class="stat-label">今日攝取熱量</div>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">🎯</div>
<div class="stat-info">
<div class="stat-value" id="calorieGoalText">0</div>
<div class="stat-label">每日目標熱量</div>
</div>
</div>
</div>
<div class="goals-container" id="goalsContainer">
<h3>今日目標進度</h3>
<div class="goal-items" id="goalItems"></div>
</div>
<div class="meals-card">
<div class="meals-header">
<h3>今日食物記錄</h3>
<button class="btn-small" onclick="switchTab('analyzer')">+ 新增餐點</button>
</div>
<div id="mealsList" class="meals-list"></div>
</div>
<div class="chart-card full-width">
<div class="chart-header">
<h3>本週熱量趨勢</h3>
</div>
<div class="chart-container">
<canvas id="weeklyChart"></canvas>
</div>
</div>
</div>
</div>
</div>
<script>
// =================================================================================
// Application State and Data
// =================================================================================
const appState = {
userProfile: null,
foodDiary: [],
currentAnalysis: null,
charts: {}
};
const API_BASE_URL = 'http://127.0.0.1:8000'; // Backend server address
// =================================================================================
// Initialization
// =================================================================================
document.addEventListener('DOMContentLoaded', initializeApp);
function initializeApp() {
loadUserProfile();
loadFoodDiary();
switchTab('analyzer'); // Start on analyzer tab
}
// =================================================================================
// Tab & UI Management
// =================================================================================
function switchTab(tabId) {
// Hide all tab contents
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Deactivate all tab buttons
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
// Activate the selected tab and button
document.getElementById(tabId).classList.add('active');
document.querySelector(`.tab-btn[data-tab="${tabId}"]`).classList.add('active');
// Conditional UI updates based on tab and profile status
const hasProfile = !!appState.userProfile;
document.getElementById('profileCheck').style.display = !hasProfile && tabId === 'analyzer' ? 'block' : 'none';
document.getElementById('analyzerContent').style.display = hasProfile && tabId === 'analyzer' ? 'block' : 'none';
document.getElementById('trackingCheck').style.display = !hasProfile && tabId === 'tracking' ? 'block' : 'none';
document.getElementById('trackingContent').style.display = hasProfile && tabId === 'tracking' ? 'block' : 'none';
if (hasProfile) {
if (tabId === 'profile') {
populateProfileForm();
} else if (tabId === 'tracking') {
updateTrackingPage();
}
}
}
function showLoading(isLoading) {
document.getElementById('loading').style.display = isLoading ? 'flex' : 'none';
}
function showNotification(message, type = 'info') {
alert(`[${type.toUpperCase()}] ${message}`);
}
// =================================================================================
// Profile Management
// =================================================================================
function saveUserProfile() {
const profile = {
name: document.getElementById('userName').value,
age: parseInt(document.getElementById('userAge').value),
gender: document.getElementById('userGender').value,
height: parseInt(document.getElementById('userHeight').value),
weight: parseInt(document.getElementById('userWeight').value),
activityLevel: document.getElementById('activityLevel').value,
healthGoal: document.getElementById('healthGoal').value
};
if (Object.values(profile).some(v => !v)) {
return showNotification('請填寫所有欄位!', 'error');
}
// Calculate BMR and daily calorie needs
const bmr = calculateBMR(profile);
profile.dailyCalories = calculateDailyCalories(bmr, profile.activityLevel, profile.healthGoal);
profile.proteinGoal = Math.round(profile.dailyCalories * 0.25 / 4); // 25% from protein
profile.fiberGoal = 25; // g
profile.waterGoal = 2000; // ml
appState.userProfile = profile;
localStorage.setItem('userProfile', JSON.stringify(profile));
showNotification('個人資料已儲存!', 'success');
switchTab('analyzer'); // Switch to analyzer after saving
}
function loadUserProfile() {
const storedProfile = localStorage.getItem('userProfile');
if (storedProfile) {
appState.userProfile = JSON.parse(storedProfile);
}
}
function populateProfileForm() {
if (!appState.userProfile) return;
for (const key in appState.userProfile) {
const element = document.getElementById(key);
if (element) {
element.value = appState.userProfile[key];
}
}
}
function calculateBMR({ gender, weight, height, age }) {
if (gender === 'male') {
return 88.362 + (13.397 * weight) + (4.799 * height) - (5.677 * age);
}
return 447.593 + (9.247 * weight) + (3.098 * height) - (4.330 * age);
}
function calculateDailyCalories(bmr, activityLevel, healthGoal) {
const activityMultipliers = { sedentary: 1.2, light: 1.375, moderate: 1.55, active: 1.725, extra: 1.9 };
let calories = bmr * (activityMultipliers[activityLevel] || 1.2);
const goalAdjustments = { lose: -300, gain: 300, muscle: 200 };
calories += goalAdjustments[healthGoal] || 0;
return Math.round(calories);
}
// =================================================================================
// Camera and Image Analysis
// =================================================================================
async function startCamera() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
const video = document.getElementById('video');
video.srcObject = stream;
video.style.display = 'block';
} catch (err) {
showNotification('無法開啟相機,請檢查權限。', 'error');
}
}
function capturePhoto() {
const video = document.getElementById('video');
const canvas = document.getElementById('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const context = canvas.getContext('2d');
context.drawImage(video, 0, 0, canvas.width, canvas.height);
canvas.toBlob(blob => {
if (blob) {
processImage(blob);
} else {
showNotification('無法擷取圖片,請再試一次', 'error');
}
}, 'image/jpeg');
}
function handleFileUpload(event) {
const file = event.target.files[0];
if (file) {
processImage(file);
// 重置文件輸入,允許重複上傳相同文件
event.target.value = '';
}
}
async function processImage(imageSource) {
showLoading(true);
document.getElementById('result').style.display = 'none';
const formData = new FormData();
formData.append('file', imageSource);
try {
// 調用 AI 端點獲取食物名稱
const aiResponse = await fetch(`${API_BASE_URL}/ai/analyze-food-image/`, {
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_name;
if (!foodName || foodName === "Unknown") {
throw new Error('AI無法辨識出食物名稱。');
}
// 創建基本分析結果(基於 AI 辨識結果)
// 由於營養端點可能不存在,我們先創建一個基本版本
appState.currentAnalysis = {
foodName: foodName,
description: `AI 辨識結果:${foodName}`,
healthIndex: 75, // 默認值
glycemicIndex: 50, // 默認值
benefits: [`含有 ${foodName} 的營養成分`],
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
}
};
// 嘗試獲取詳細營養信息(可選)
try {
const nutritionResponse = await fetch(`${API_BASE_URL}/api/analyze-nutrition/${encodeURIComponent(foodName)}`);
if (nutritionResponse.ok) {
const nutritionData = await nutritionResponse.json();
if (nutritionData.success) {
// 如果營養端點存在且成功,合併數據
appState.currentAnalysis = {
foodName: foodName,
...nutritionData
};
}
}
} catch (nutritionError) {
console.log('營養信息端點不可用,使用基本信息');
}
displayAnalysisResults(appState.currentAnalysis);
} catch (error) {
showNotification(`分析失敗: ${error.message}`, 'error');
} finally {
showLoading(false);
}
}
function displayAnalysisResults(analysis) {
console.log('顯示分析結果:', analysis); // 調試用
// 顯示結果區域
document.getElementById('result').style.display = 'block';
// 基本信息
document.getElementById('foodName').textContent = analysis.foodName;
document.getElementById('foodDescription').textContent = analysis.description || `這是 ${analysis.foodName}`;
// 健康指數
const healthIndex = analysis.healthIndex || 75;
const glycemicIndex = analysis.glycemicIndex || 50;
document.getElementById('healthIndexValue').textContent = `${healthIndex}/100`;
document.getElementById('healthIndexFill').style.width = `${healthIndex}%`;
document.getElementById('glycemicIndexValue').textContent = `${glycemicIndex}/100`;
document.getElementById('glycemicIndexFill').style.width = `${glycemicIndex}%`;
// 營養益處
const benefitsContainer = document.getElementById('benefitsTags');
benefitsContainer.innerHTML = '';
if (analysis.benefits && Array.isArray(analysis.benefits)) {
analysis.benefits.forEach(tag => {
const tagEl = document.createElement('span');
tagEl.className = 'benefit-tag';
tagEl.textContent = tag;
benefitsContainer.appendChild(tagEl);
});
} else {
const tagEl = document.createElement('span');
tagEl.className = 'benefit-tag';
tagEl.textContent = `${analysis.foodName} 的營養價值`;
benefitsContainer.appendChild(tagEl);
}
// 營養信息
const nutrition = analysis.nutrition || {};
const nutritionData = {
calories: nutrition.calories || 150,
protein: nutrition.protein || 8,
carbs: nutrition.carbs || 20,
fat: nutrition.fat || 5,
fiber: nutrition.fiber || 3,
sugar: nutrition.sugar || 2
};
const labels = {
calories: '熱量',
protein: '蛋白質',
carbs: '碳水化合物',
fat: '脂肪',
fiber: '纖維',
sugar: '糖分'
};
const units = {
calories: '卡',
protein: 'g',
carbs: 'g',
fat: 'g',
fiber: 'g',
sugar: 'g'
};
// 填充營養網格
const nutritionGrid = document.getElementById('nutritionGrid');
nutritionGrid.innerHTML = '';
for (const [key, value] of Object.entries(nutritionData)) {
const itemDiv = document.createElement('div');
itemDiv.className = 'nutrition-item';
itemDiv.innerHTML = `
<div class="nutrition-label">${labels[key]}</div>
<div class="nutrition-value">${value} ${units[key]}</div>
`;
nutritionGrid.appendChild(itemDiv);
}
// 維生素和礦物質
const vitaminsGrid = document.getElementById('vitaminsGrid');
const mineralsGrid = document.getElementById('mineralsGrid');
vitaminsGrid.innerHTML = '';
mineralsGrid.innerHTML = '';
if (analysis.vitamins) {
for (const [key, value] of Object.entries(analysis.vitamins)) {
const itemDiv = document.createElement('div');
itemDiv.className = 'nutrition-item';
itemDiv.innerHTML = `
<div class="nutrition-label">${key}</div>
<div class="nutrition-value">${value} mg</div>
`;
vitaminsGrid.appendChild(itemDiv);
}
}
if (analysis.minerals) {
for (const [key, value] of Object.entries(analysis.minerals)) {
const itemDiv = document.createElement('div');
itemDiv.className = 'nutrition-item';
itemDiv.innerHTML = `
<div class="nutrition-label">${key}</div>
<div class="nutrition-value">${value} mg</div>
`;
mineralsGrid.appendChild(itemDiv);
}
}
// 個人化建議
if (appState.userProfile) {
const recoEl = document.getElementById('recommendation');
const caloriePercentage = ((nutritionData.calories / appState.userProfile.dailyCalories) * 100).toFixed(0);
recoEl.innerHTML = `
<div class="recommendation-title">💡 個人化建議</div>
<div class="recommendation-text">這份食物約佔您每日熱量建議的 ${caloriePercentage}%。</div>
`;
}
}
// =================================================================================
// Food Diary
// =================================================================================
function addToFoodDiary() {
if (!appState.currentAnalysis) {
return showNotification('沒有可加入的分析結果。', 'error');
}
const meal = {
...appState.currentAnalysis,
id: Date.now(),
timestamp: new Date().toISOString()
};
appState.foodDiary.push(meal);
saveFoodDiary();
showNotification(`${meal.foodName} 已加入記錄!`, 'success');
updateTrackingPage(); // Refresh tracking page data
}
function loadFoodDiary() {
const today = new Date().toISOString().slice(0, 10);
const storedDiary = localStorage.getItem(`foodDiary_${today}`);
appState.foodDiary = storedDiary ? JSON.parse(storedDiary) : [];
}
function saveFoodDiary() {
const today = new Date().toISOString().slice(0, 10);
localStorage.setItem(`foodDiary_${today}`, JSON.stringify(appState.foodDiary));
}
// =================================================================================
// Tracking Page
// =================================================================================
function updateTrackingPage() {
if (!appState.userProfile) return;
updateWelcomeMessage();
updateTodayStats();
updateGoalsProgress();
updateMealsList();
renderWeeklyChart();
}
function updateWelcomeMessage() {
document.getElementById('welcomeMessage').innerHTML = `<h3>👋 你好, ${appState.userProfile.name}!</h3><p>這是您今天的營養總覽。</p>`;
}
function updateTodayStats() {
const todayTotals = appState.foodDiary.reduce((totals, meal) => {
totals.calories += meal.nutrition.calories || 0;
totals.protein += meal.nutrition.protein || 0;
totals.fiber += meal.nutrition.fiber || 0;
return totals;
}, { calories: 0, protein: 0, fiber: 0 });
document.getElementById('todayCalories').textContent = Math.round(todayTotals.calories);
document.getElementById('calorieGoalText').textContent = appState.userProfile.dailyCalories;
}
function updateGoalsProgress() {
const goals = {
calories: { label: '熱量', unit: '卡', goal: appState.userProfile.dailyCalories, key: 'calories' },
protein: { label: '蛋白質', unit: 'g', goal: appState.userProfile.proteinGoal, key: 'protein' },
fiber: { label: '纖維', unit: 'g', goal: appState.userProfile.fiberGoal, key: 'fiber' }
};
const todayTotals = appState.foodDiary.reduce((totals, meal) => {
totals.calories += meal.nutrition.calories || 0;
totals.protein += meal.nutrition.protein || 0;
totals.fiber += meal.nutrition.fiber || 0;
return totals;
}, { calories: 0, protein: 0, fiber: 0 });
const container = document.getElementById('goalItems');
container.innerHTML = '';
for (const g in goals) {
const { label, unit, goal, key } = goals[g];
const current = Math.round(todayTotals[key]);
const progress = goal > 0 ? Math.min(100, (current / goal) * 100) : 0;
const item = document.createElement('div');
item.className = 'progress-item';
item.innerHTML = `
<div class="progress-header">
<span class="progress-label">${label}</span>
<span class="progress-value">${current} / ${goal} ${unit}</span>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" style="width: ${progress}%; background-color: #${g === 'calories' ? 'ff9a9e' : g === 'protein' ? '667eea' : '43cea2'};"></div>
</div>`;
container.appendChild(item);
}
}
function updateMealsList() {
const listEl = document.getElementById('mealsList');
if (appState.foodDiary.length === 0) {
listEl.innerHTML = `<div class="empty-state">
<div class="empty-icon">🍽️</div><p>今天尚未記錄任何食物</p>
</div>`;
return;
}
listEl.innerHTML = appState.foodDiary.map(meal => `
<div class="meal-item">
<div class="meal-info">
<h4>${meal.foodName}</h4>
<span>${new Date(meal.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
</div>
<div class="meal-calories">${Math.round(meal.nutrition.calories)} 卡</div>
</div>
`).join('');
}
function renderWeeklyChart() {
const ctx = document.getElementById('weeklyChart').getContext('2d');
if (appState.charts.weekly) {
appState.charts.weekly.destroy();
}
// Mock data for past 6 days + today's actual data
const labels = ['二', '三', '四', '五', '六', '日', '一'].slice(-7); // Last 7 days ending today
const data = Array(6).fill(0).map(() => Math.random() * (2200 - 1500) + 1500);
const todayCalories = appState.foodDiary.reduce((sum, meal) => sum + meal.nutrition.calories, 0);
data.push(todayCalories);
appState.charts.weekly = new Chart(ctx, {
type: 'line',
data: {
labels,
datasets: [{
label: '每日熱量 (卡)',
data,
borderColor: '#667eea',
backgroundColor: 'rgba(102, 126, 234, 0.1)',
fill: true,
tension: 0.3
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: { y: { beginAtZero: true } },
plugins: { legend: { display: false } }
}
});
}
</script>
</body>
</html>