From f65abdef0f9661a51439ceeb3aa66260a2134bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B5=B5=E6=9D=B0=20Jie=20Zhao=20=EF=BC=88=E9=9B=84?= =?UTF-8?q?=E7=8B=AE=E6=B1=BD=E8=BD=A6=E7=A7=91=E6=8A=80=EF=BC=89?= <00061074@chery.local> Date: Sun, 2 Nov 2025 22:23:10 +0800 Subject: [PATCH] feat: complete web app features and fix encoding --- static/css/analysis.css | 95 ++++++++ static/css/data_collection.css | 77 +++++++ static/css/recitation.css | 28 +++ static/css/recommendation.css | 99 +++++++++ static/css/style.css | 4 +- static/js/analysis.js | 133 ++++++++++++ static/js/data_collection.js | 193 +++++++++++++++++ static/js/recitation.js | 115 ++++++++++ static/js/recommendation.js | 135 ++++++++++++ templates/analysis.html | 66 ++++++ templates/data_collection.html | 120 ++++++++++ templates/index.html | 21 ++ templates/recitation.html | 5 + templates/recommendation.html | 70 ++++++ web_app.py | 386 ++++++++++++++++++++++++++++++++- 15 files changed, 1542 insertions(+), 5 deletions(-) create mode 100644 static/css/analysis.css create mode 100644 static/css/data_collection.css create mode 100644 static/css/recommendation.css create mode 100644 static/js/analysis.js create mode 100644 static/js/data_collection.js create mode 100644 static/js/recommendation.js create mode 100644 templates/analysis.html create mode 100644 templates/data_collection.html create mode 100644 templates/recommendation.html diff --git a/static/css/analysis.css b/static/css/analysis.css new file mode 100644 index 0000000..01b8955 --- /dev/null +++ b/static/css/analysis.css @@ -0,0 +1,95 @@ +.analysis-container { + max-width: 900px; + margin: 0 auto; +} + +.section { + margin-bottom: 40px; + padding: 30px; + background: #f8f9fa; + border-radius: 15px; +} + +.section h2 { + font-size: 1.8em; + margin-bottom: 25px; + color: #333; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #555; +} + +.form-input, .form-textarea { + width: 100%; + padding: 12px; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1em; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-input:focus, .form-textarea:focus { + outline: none; + border-color: #667eea; +} + +.form-textarea { + resize: vertical; + min-height: 120px; +} + +.analysis-result { + background: white; + padding: 25px; + border-radius: 10px; + border-left: 4px solid #667eea; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.analysis-result h3 { + margin-top: 0; + color: #667eea; +} + +.analysis-result .analysis-section { + margin: 20px 0; + padding: 15px; + background: #f8f9fa; + border-radius: 8px; +} + +.analysis-result .analysis-section h4 { + color: #555; + margin-top: 0; +} + +.message-area { + margin-top: 20px; + padding: 15px; + border-radius: 8px; + display: none; +} + +.message-area.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.message-area.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + diff --git a/static/css/data_collection.css b/static/css/data_collection.css new file mode 100644 index 0000000..b7900ee --- /dev/null +++ b/static/css/data_collection.css @@ -0,0 +1,77 @@ +.data-collection-container { + max-width: 800px; + margin: 0 auto; +} + +.section { + margin-bottom: 40px; + padding: 30px; + background: #f8f9fa; + border-radius: 15px; +} + +.section h2 { + font-size: 1.8em; + margin-bottom: 25px; + color: #333; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #555; +} + +.form-input, .form-textarea { + width: 100%; + padding: 12px; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1em; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-input:focus, .form-textarea:focus { + outline: none; + border-color: #667eea; +} + +.form-textarea { + resize: vertical; + min-height: 100px; +} + +.message-area { + margin-top: 20px; + padding: 15px; + border-radius: 8px; + display: none; +} + +.message-area.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.message-area.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + +.message-area.info { + background: #d1ecf1; + color: #0c5460; + border: 1px solid #bee5eb; + display: block; +} + diff --git a/static/css/recitation.css b/static/css/recitation.css index a67aca2..e4ca83d 100644 --- a/static/css/recitation.css +++ b/static/css/recitation.css @@ -191,6 +191,34 @@ color: #333; } +/* 导出按钮样式 */ +.export-buttons { + display: flex; + gap: 10px; + margin-bottom: 20px; + flex-wrap: wrap; +} + +.btn-export { + padding: 10px 20px; + background: #28a745; + color: white; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 0.9em; + transition: all 0.3s; +} + +.btn-export:hover { + background: #218838; + transform: translateY(-2px); +} + +.btn-export:active { + transform: translateY(0); +} + /* 响应式设计 */ @media (max-width: 768px) { .wheel-container { diff --git a/static/css/recommendation.css b/static/css/recommendation.css new file mode 100644 index 0000000..4f416c5 --- /dev/null +++ b/static/css/recommendation.css @@ -0,0 +1,99 @@ +.recommendation-container { + max-width: 900px; + margin: 0 auto; +} + +.section { + margin-bottom: 40px; + padding: 30px; + background: #f8f9fa; + border-radius: 15px; +} + +.section h2 { + font-size: 1.8em; + margin-bottom: 25px; + color: #333; +} + +.form-group { + margin-bottom: 20px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: bold; + color: #555; +} + +.form-input { + width: 100%; + padding: 12px; + border: 2px solid #e9ecef; + border-radius: 8px; + font-size: 1em; + font-family: inherit; + transition: border-color 0.3s; +} + +.form-input:focus { + outline: none; + border-color: #667eea; +} + +.recommendations-list { + display: grid; + gap: 20px; +} + +.recommendation-card { + background: white; + padding: 20px; + border-radius: 10px; + border-left: 4px solid #667eea; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); +} + +.recommendation-card h3 { + margin-top: 0; + color: #667eea; +} + +.recommendation-card .food-list { + margin: 15px 0; +} + +.recommendation-card .food-item { + padding: 8px; + margin: 5px 0; + background: #f8f9fa; + border-radius: 5px; +} + +.recommendation-card .confidence { + color: #28a745; + font-weight: bold; +} + +.message-area { + margin-top: 20px; + padding: 15px; + border-radius: 8px; + display: none; +} + +.message-area.success { + background: #d4edda; + color: #155724; + border: 1px solid #c3e6cb; + display: block; +} + +.message-area.error { + background: #f8d7da; + color: #721c24; + border: 1px solid #f5c6cb; + display: block; +} + diff --git a/static/css/style.css b/static/css/style.css index de45c28..c4a705b 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -89,8 +89,8 @@ body { .feature-cards { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 30px; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 25px; margin-top: 40px; } diff --git a/static/js/analysis.js b/static/js/analysis.js new file mode 100644 index 0000000..6b368b4 --- /dev/null +++ b/static/js/analysis.js @@ -0,0 +1,133 @@ +// 分析功能脚本 + +let currentUserId = null; + +// DOM元素 +const loginSection = document.getElementById('loginSection'); +const requestSection = document.getElementById('requestSection'); +const analysisSection = document.getElementById('analysisSection'); +const messageArea = document.getElementById('messageArea'); + +const loginBtn = document.getElementById('loginBtn'); +const userIdInput = document.getElementById('userId'); +const analyzeBtn = document.getElementById('analyzeBtn'); +const mealDataTextarea = document.getElementById('mealData'); +const analysisResult = document.getElementById('analysisResult'); + +// 显示消息 +function showMessage(message, type = 'info') { + messageArea.textContent = message; + messageArea.className = `message-area ${type}`; + setTimeout(() => { + messageArea.className = 'message-area'; + messageArea.style.display = 'none'; + }, 3000); +} + +// 用户登录 +loginBtn.addEventListener('click', async () => { + const userId = userIdInput.value.trim(); + + if (!userId) { + showMessage('请输入用户ID', 'error'); + return; + } + + loginBtn.disabled = true; + loginBtn.textContent = '登录中...'; + + try { + const response = await fetch('/api/user/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: userId + }) + }); + + const data = await response.json(); + + if (data.success) { + currentUserId = userId; + loginSection.style.display = 'none'; + requestSection.style.display = 'block'; + showMessage(`欢迎,${data.name || userId}!`, 'success'); + } else { + showMessage(data.message || '登录失败', 'error'); + } + } catch (error) { + console.error('登录失败:', error); + showMessage('登录失败,请检查网络连接', 'error'); + } finally { + loginBtn.disabled = false; + loginBtn.textContent = '登录'; + } +}); + +// 开始分析 +analyzeBtn.addEventListener('click', async () => { + const mealDataText = mealDataTextarea.value.trim(); + + if (!mealDataText) { + showMessage('请输入餐食信息', 'error'); + return; + } + + analyzeBtn.disabled = true; + analyzeBtn.textContent = '分析中...'; + + try { + const response = await fetch('/api/analysis/nutrition', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: currentUserId, + meal_data: { + text: mealDataText + } + }) + }); + + const data = await response.json(); + + if (data.success && data.analysis) { + displayAnalysis(data.analysis); + analysisSection.style.display = 'block'; + showMessage('分析完成!', 'success'); + } else { + showMessage(data.message || '分析失败', 'error'); + } + } catch (error) { + console.error('分析失败:', error); + showMessage('分析失败,请检查网络连接', 'error'); + } finally { + analyzeBtn.disabled = false; + analyzeBtn.textContent = '开始分析'; + } +}); + +// 显示分析结果 +function displayAnalysis(analysis) { + analysisResult.innerHTML = ''; + + if (typeof analysis === 'string') { + // 如果是字符串,直接显示 + analysisResult.innerHTML = `

${analysis.replace(/\n/g, '
')}

`; + } else if (analysis.analysis) { + // 如果包含analysis字段 + analysisResult.innerHTML = `

分析结果

${analysis.analysis.replace(/\n/g, '
')}

`; + } else { + // 如果是对象,格式化显示 + for (const [key, value] of Object.entries(analysis)) { + const section = document.createElement('div'); + section.className = 'analysis-section'; + section.innerHTML = `

${key}

${typeof value === 'string' ? value.replace(/\n/g, '
') : JSON.stringify(value, null, 2)}

`; + analysisResult.appendChild(section); + } + } +} + diff --git a/static/js/data_collection.js b/static/js/data_collection.js new file mode 100644 index 0000000..b1fa46c --- /dev/null +++ b/static/js/data_collection.js @@ -0,0 +1,193 @@ +// 数据采集功能脚本 + +let currentUserId = null; + +// DOM元素 +const loginSection = document.getElementById('loginSection'); +const basicQuestionnaire = document.getElementById('basicQuestionnaire'); +const mealRecord = document.getElementById('mealRecord'); +const messageArea = document.getElementById('messageArea'); + +const loginBtn = document.getElementById('loginBtn'); +const userIdInput = document.getElementById('userId'); +const userNameInput = document.getElementById('userName'); + +const submitBasicBtn = document.getElementById('submitBasicBtn'); +const submitMealBtn = document.getElementById('submitMealBtn'); + +// 显示消息 +function showMessage(message, type = 'info') { + messageArea.textContent = message; + messageArea.className = `message-area ${type}`; + setTimeout(() => { + messageArea.className = 'message-area'; + messageArea.style.display = 'none'; + }, 3000); +} + +// 用户登录 +loginBtn.addEventListener('click', async () => { + const userId = userIdInput.value.trim(); + const userName = userNameInput.value.trim(); + + if (!userId || !userName) { + showMessage('请输入用户ID和姓名', 'error'); + return; + } + + loginBtn.disabled = true; + loginBtn.textContent = '登录中...'; + + try { + const response = await fetch('/api/user/register', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: userId, + name: userName + }) + }); + + const data = await response.json(); + + if (data.success) { + currentUserId = userId; + loginSection.style.display = 'none'; + basicQuestionnaire.style.display = 'block'; + mealRecord.style.display = 'block'; + showMessage('登录成功!', 'success'); + } else { + showMessage(data.message || '登录失败', 'error'); + } + } catch (error) { + console.error('登录失败:', error); + showMessage('登录失败,请检查网络连接', 'error'); + } finally { + loginBtn.disabled = false; + loginBtn.textContent = '登录/注册'; + } +}); + +// 提交基础信息 +submitBasicBtn.addEventListener('click', async () => { + const age = document.getElementById('age').value; + const gender = document.getElementById('gender').value; + const height = document.getElementById('height').value; + const weight = document.getElementById('weight').value; + const activityLevel = document.getElementById('activityLevel').value; + + if (!age || !gender || !height || !weight || !activityLevel) { + showMessage('请填写完整的基础信息', 'error'); + return; + } + + submitBasicBtn.disabled = true; + submitBasicBtn.textContent = '提交中...'; + + try { + const response = await fetch('/api/questionnaire/submit', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: currentUserId, + type: 'basic', + answers: { + age: parseInt(age), + gender: gender, + height: parseFloat(height), + weight: parseFloat(weight), + activity_level: activityLevel + } + }) + }); + + const data = await response.json(); + + if (data.success) { + showMessage('基础信息提交成功!', 'success'); + } else { + showMessage(data.message || '提交失败', 'error'); + } + } catch (error) { + console.error('提交失败:', error); + showMessage('提交失败,请检查网络连接', 'error'); + } finally { + submitBasicBtn.disabled = false; + submitBasicBtn.textContent = '提交基础信息'; + } +}); + +// 记录餐食 +submitMealBtn.addEventListener('click', async () => { + const mealDate = document.getElementById('mealDate').value || new Date().toISOString().split('T')[0]; + const mealType = document.getElementById('mealType').value; + const foodsText = document.getElementById('foods').value; + const calories = document.getElementById('calories').value; + const satisfaction = document.getElementById('satisfaction').value; + + if (!foodsText || !calories) { + showMessage('请填写完整的餐食信息', 'error'); + return; + } + + // 解析食物列表 + const foods = foodsText.split('\n').filter(line => line.trim()); + const quantities = foods.map(f => { + const parts = f.trim().split(/\s+/); + return parts.length > 1 ? parts.slice(1).join(' ') : '适量'; + }); + const foodNames = foods.map(f => f.trim().split(/\s+/)[0]); + + submitMealBtn.disabled = true; + submitMealBtn.textContent = '记录中...'; + + try { + const response = await fetch('/api/meal/record', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: currentUserId, + date: mealDate, + meal_type: mealType, + foods: foodNames, + quantities: quantities, + calories: parseFloat(calories), + satisfaction_score: parseInt(satisfaction), + notes: '' + }) + }); + + const data = await response.json(); + + if (data.success) { + showMessage('餐食记录成功!', 'success'); + // 清空表单 + document.getElementById('foods').value = ''; + document.getElementById('calories').value = ''; + document.getElementById('satisfaction').value = '3'; + } else { + showMessage(data.message || '记录失败', 'error'); + } + } catch (error) { + console.error('记录失败:', error); + showMessage('记录失败,请检查网络连接', 'error'); + } finally { + submitMealBtn.disabled = false; + submitMealBtn.textContent = '记录餐食'; + } +}); + +// 设置默认日期为今天 +document.addEventListener('DOMContentLoaded', () => { + const mealDateInput = document.getElementById('mealDate'); + if (mealDateInput) { + mealDateInput.value = new Date().toISOString().split('T')[0]; + } +}); + diff --git a/static/js/recitation.js b/static/js/recitation.js index bbccc6f..7cfaa49 100644 --- a/static/js/recitation.js +++ b/static/js/recitation.js @@ -26,6 +26,73 @@ const currentItem = document.getElementById('currentItem'); const resultSection = document.getElementById('resultSection'); const sortedList = document.getElementById('sortedList'); const resetBtn = document.getElementById('resetBtn'); +const exportTxtBtn = document.getElementById('exportTxtBtn'); +const exportJsonBtn = document.getElementById('exportJsonBtn'); +const exportCsvBtn = document.getElementById('exportCsvBtn'); + +// 本地存储键名 +const STORAGE_KEY_EXTRACTED = 'recitation_extracted_items'; +const STORAGE_KEY_SORTED = 'recitation_sorted_items'; +const STORAGE_KEY_ORIGINAL_TEXT = 'recitation_original_text'; + +// 页面加载时恢复数据 +window.addEventListener('DOMContentLoaded', () => { + restoreFromStorage(); +}); + +// 保存到本地存储 +function saveToStorage() { + try { + if (extractedItems.length > 0) { + localStorage.setItem(STORAGE_KEY_EXTRACTED, JSON.stringify(extractedItems)); + } + if (sortedItems.length > 0) { + localStorage.setItem(STORAGE_KEY_SORTED, JSON.stringify(sortedItems)); + } + if (textInput.value.trim()) { + localStorage.setItem(STORAGE_KEY_ORIGINAL_TEXT, textInput.value); + } + } catch (e) { + console.error('保存到本地存储失败:', e); + } +} + +// 从本地存储恢复 +function restoreFromStorage() { + try { + const savedExtracted = localStorage.getItem(STORAGE_KEY_EXTRACTED); + const savedSorted = localStorage.getItem(STORAGE_KEY_SORTED); + const savedText = localStorage.getItem(STORAGE_KEY_ORIGINAL_TEXT); + + if (savedText) { + textInput.value = savedText; + } + + if (savedExtracted) { + extractedItems = JSON.parse(savedExtracted); + displayExtractedItems(extractedItems); + extractedSection.style.display = 'block'; + textInput.disabled = true; + } + + if (savedSorted) { + sortedItems = JSON.parse(savedSorted); + displaySortedItems(sortedItems); + createWheel(sortedItems); + wheelSection.style.display = 'block'; + resultSection.style.display = 'block'; + } + } catch (e) { + console.error('从本地存储恢复失败:', e); + } +} + +// 清除本地存储 +function clearStorage() { + localStorage.removeItem(STORAGE_KEY_EXTRACTED); + localStorage.removeItem(STORAGE_KEY_SORTED); + localStorage.removeItem(STORAGE_KEY_ORIGINAL_TEXT); +} // 提取知识点 extractBtn.addEventListener('click', async () => { @@ -55,6 +122,7 @@ extractBtn.addEventListener('click', async () => { displayExtractedItems(extractedItems); extractedSection.style.display = 'block'; textInput.disabled = true; + saveToStorage(); // 保存到本地存储 } else { alert(data.message || '提取失败'); } @@ -108,6 +176,7 @@ sortBtn.addEventListener('click', async () => { wheelSection.style.display = 'block'; resultSection.style.display = 'block'; currentSpinIndex = 0; + saveToStorage(); // 保存到本地存储 } else { alert(data.message || '排序失败'); } @@ -259,6 +328,50 @@ function displaySortedItems(items) { }); } +// 导出功能 +exportTxtBtn.addEventListener('click', () => exportData('txt')); +exportJsonBtn.addEventListener('click', () => exportData('json')); +exportCsvBtn.addEventListener('click', () => exportData('csv')); + +function exportData(format) { + if (sortedItems.length === 0) { + alert('没有可导出的数据'); + return; + } + + fetch('/api/export/sorted', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + items: sortedItems, + format: format + }) + }) + .then(response => { + if (!response.ok) { + throw new Error('导出失败'); + } + return response.blob(); + }) + .then(blob => { + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_'); + a.download = `背诵排序结果_${timestamp}.${format}`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + }) + .catch(error => { + console.error('导出失败:', error); + alert('导出失败,请重试'); + }); +} + // 重置 resetBtn.addEventListener('click', () => { extractedItems = []; @@ -281,5 +394,7 @@ resetBtn.addEventListener('click', () => { isSpinning = false; spinBtn.disabled = false; + + clearStorage(); // 清除本地存储 }); diff --git a/static/js/recommendation.js b/static/js/recommendation.js new file mode 100644 index 0000000..a9fafaa --- /dev/null +++ b/static/js/recommendation.js @@ -0,0 +1,135 @@ +// 推荐功能脚本 + +let currentUserId = null; + +// DOM元素 +const loginSection = document.getElementById('loginSection'); +const requestSection = document.getElementById('requestSection'); +const recommendationsSection = document.getElementById('recommendationsSection'); +const messageArea = document.getElementById('messageArea'); + +const loginBtn = document.getElementById('loginBtn'); +const userIdInput = document.getElementById('userId'); +const getRecommendationBtn = document.getElementById('getRecommendationBtn'); +const mealTypeSelect = document.getElementById('mealType'); +const recommendationsList = document.getElementById('recommendationsList'); + +// 显示消息 +function showMessage(message, type = 'info') { + messageArea.textContent = message; + messageArea.className = `message-area ${type}`; + setTimeout(() => { + messageArea.className = 'message-area'; + messageArea.style.display = 'none'; + }, 3000); +} + +// 用户登录 +loginBtn.addEventListener('click', async () => { + const userId = userIdInput.value.trim(); + + if (!userId) { + showMessage('请输入用户ID', 'error'); + return; + } + + loginBtn.disabled = true; + loginBtn.textContent = '登录中...'; + + try { + const response = await fetch('/api/user/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: userId + }) + }); + + const data = await response.json(); + + if (data.success) { + currentUserId = userId; + loginSection.style.display = 'none'; + requestSection.style.display = 'block'; + showMessage(`欢迎,${data.name || userId}!`, 'success'); + } else { + showMessage(data.message || '登录失败', 'error'); + } + } catch (error) { + console.error('登录失败:', error); + showMessage('登录失败,请检查网络连接', 'error'); + } finally { + loginBtn.disabled = false; + loginBtn.textContent = '登录'; + } +}); + +// 获取推荐 +getRecommendationBtn.addEventListener('click', async () => { + const mealType = mealTypeSelect.value; + + getRecommendationBtn.disabled = true; + getRecommendationBtn.textContent = '获取中...'; + + try { + const response = await fetch('/api/recommendation/get', { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8' + }, + body: JSON.stringify({ + user_id: currentUserId, + meal_type: mealType, + preferences: {}, + context: {} + }) + }); + + const data = await response.json(); + + if (data.success && data.recommendations && data.recommendations.length > 0) { + displayRecommendations(data.recommendations); + recommendationsSection.style.display = 'block'; + showMessage('推荐获取成功!', 'success'); + } else { + showMessage(data.message || '暂无推荐,请先完善个人数据', 'info'); + } + } catch (error) { + console.error('获取推荐失败:', error); + showMessage('获取推荐失败,请检查网络连接', 'error'); + } finally { + getRecommendationBtn.disabled = false; + getRecommendationBtn.textContent = '获取推荐'; + } +}); + +// 显示推荐结果 +function displayRecommendations(recommendations) { + recommendationsList.innerHTML = ''; + + recommendations.forEach((rec, index) => { + const card = document.createElement('div'); + card.className = 'recommendation-card'; + + let foods = []; + if (rec.foods && Array.isArray(rec.foods)) { + foods = rec.foods; + } else if (rec.food) { + foods = [rec.food]; + } + + card.innerHTML = ` +

推荐方案 ${index + 1}

+
+ ${foods.map(food => `
${food}
`).join('')} +
+ ${rec.confidence ? `
置信度: ${(rec.confidence * 100).toFixed(1)}%
` : ''} + ${rec.reason ? `
推荐理由: ${rec.reason}
` : ''} + `; + + recommendationsList.appendChild(card); + }); +} + diff --git a/templates/analysis.html b/templates/analysis.html new file mode 100644 index 0000000..726ef62 --- /dev/null +++ b/templates/analysis.html @@ -0,0 +1,66 @@ + + + + + + 营养分析 - 个性化饮食推荐助手 + + + + +
+
+

? 营养分析

+

AI智能营养分析与建议

+
+ + + +
+
+ +
+

用户登录

+
+ + +
+ +
+ + + + + + + + +
+
+
+ + +
+ + + + + diff --git a/templates/data_collection.html b/templates/data_collection.html new file mode 100644 index 0000000..9e405ab --- /dev/null +++ b/templates/data_collection.html @@ -0,0 +1,120 @@ + + + + + + 数据采集 - 个性化饮食推荐助手 + + + + +
+
+

? 数据采集

+

建立你的个人饮食档案

+
+ + + +
+
+ +
+

用户登录

+
+ + +
+
+ + +
+ +
+ + + + + + + + +
+
+
+ + +
+ + + + + diff --git a/templates/index.html b/templates/index.html index a36e378..a52b7f0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,6 +15,9 @@ @@ -24,6 +27,24 @@

杩欐槸涓涓泦鎴愪簡鏅鸿兘楗鎺ㄨ崘鍜岃儗璇垫帓搴忓姛鑳界殑缃戦〉搴旂敤銆

+
+
馃摑
+

鏁版嵁閲囬泦

+

濉啓鍩虹淇℃伅闂嵎锛岃褰曚綘鐨勯ギ椋熶範鎯拰鍋忓ソ锛屽缓绔嬩釜浜烘。妗堛

+ 寮濮嬩娇鐢 +
+
+
馃
+

鏅鸿兘鎺ㄨ崘

+

鍩轰簬浣犵殑楗涔犳儻鍜屽亸濂斤紝AI涓轰綘鎺ㄨ崘涓у寲鐨勯椋熸惌閰嶃

+ 寮濮嬩娇鐢 +
+
+
馃敩
+

钀ュ吇鍒嗘瀽

+

鍒嗘瀽浣犵殑椁愰钀ュ吇鐘跺喌锛屾彁渚涗笓涓氱殑钀ュ吇寤鸿鍜屽仴搴锋寚瀵笺

+ 寮濮嬩娇鐢 +
馃幆

鑳岃鎺掑簭

diff --git a/templates/recitation.html b/templates/recitation.html index b79729e..a3ccfd2 100644 --- a/templates/recitation.html +++ b/templates/recitation.html @@ -56,6 +56,11 @@ diff --git a/templates/recommendation.html b/templates/recommendation.html new file mode 100644 index 0000000..128b5de --- /dev/null +++ b/templates/recommendation.html @@ -0,0 +1,70 @@ + + + + + + 智能推荐 - 个性化饮食推荐助手 + + + + +
+
+

? 智能推荐

+

基于AI的个性化餐食推荐

+
+ + + +
+
+ +
+

用户登录

+
+ + +
+ +
+ + + + + + + + +
+
+
+ +
+

© 2024 个性化饮食推荐助手

+
+
+ + + + + diff --git a/web_app.py b/web_app.py index 4e2ea0b..657d377 100644 --- a/web_app.py +++ b/web_app.py @@ -3,11 +3,20 @@ 缃戦〉绔簲鐢 - 涓у寲楗鎺ㄨ崘鍔╂墜 + 鑳岃鎺掑簭鍔熻兘 """ -from flask import Flask, render_template, request, jsonify +from flask import Flask, render_template, request, jsonify, session, Response import re import random import logging +import json from pathlib import Path +from datetime import datetime + +# 瀵煎叆涓氬姟妯″潡 +from core.base import BaseConfig, AppCore, ModuleManager, ModuleType, initialize_app +from modules.data_collection import DataCollectionModule +from modules.ai_analysis import AIAnalysisModule +from modules.recommendation_engine import RecommendationEngine +from modules.ocr_calorie_recognition import OCRCalorieRecognitionModule # 閰嶇疆鏃ュ織 logging.basicConfig( @@ -27,6 +36,43 @@ app.config['SECRET_KEY'] = 'your-secret-key-here' app.jinja_env.auto_reload = True app.config['TEMPLATES_AUTO_RELOAD'] = True +# 纭繚鎵鏈夊搷搴斾娇鐢║TF-8缂栫爜 +@app.after_request +def after_request(response): + """纭繚鎵鏈夊搷搴斾娇鐢║TF-8缂栫爜""" + response.headers['Content-Type'] = response.headers.get('Content-Type', 'text/html; charset=utf-8') + if 'charset=' not in response.headers.get('Content-Type', ''): + if response.headers.get('Content-Type', '').startswith('text/html'): + response.headers['Content-Type'] = 'text/html; charset=utf-8' + elif response.headers.get('Content-Type', '').startswith('application/json'): + response.headers['Content-Type'] = 'application/json; charset=utf-8' + return response + +# 鍒濆鍖栧簲鐢ㄦ牳蹇冿紙寤惰繜鍔犺浇锛岄伩鍏嶅湪瀵煎叆鏃跺氨鍒濆鍖栵級 +app_core = None +config = None + +def get_app_core(): + """鑾峰彇搴旂敤鏍稿績瀹炰緥锛堝欢杩熷垵濮嬪寲锛""" + global app_core, config + if app_core is None: + config = BaseConfig() + if initialize_app(config): + app_core = AppCore(config) + # 娉ㄥ唽鎵鏈夋ā鍧 + module_manager = ModuleManager(config) + module_manager.register_module(DataCollectionModule(config)) + module_manager.register_module(AIAnalysisModule(config)) + module_manager.register_module(RecommendationEngine(config)) + module_manager.register_module(OCRCalorieRecognitionModule(config)) + module_manager.initialize_all() + app_core.module_manager = module_manager + app_core.start() + logger.info("搴旂敤鏍稿績鍒濆鍖栧畬鎴") + else: + logger.error("搴旂敤鏍稿績鍒濆鍖栧け璐") + return app_core + class RecitationSorter: """鑳岃鎺掑簭鍣""" @@ -110,13 +156,31 @@ sorter = RecitationSorter() @app.route('/') def index(): """棣栭〉""" - return render_template('index.html') + return render_template('index.html', encoding='utf-8') @app.route('/recitation') def recitation(): """鑳岃鎺掑簭椤甸潰""" - return render_template('recitation.html') + return render_template('recitation.html', encoding='utf-8') + + +@app.route('/data-collection') +def data_collection(): + """鏁版嵁閲囬泦椤甸潰""" + return render_template('data_collection.html', encoding='utf-8') + + +@app.route('/recommendation') +def recommendation(): + """鎺ㄨ崘椤甸潰""" + return render_template('recommendation.html', encoding='utf-8') + + +@app.route('/analysis') +def analysis(): + """鍒嗘瀽椤甸潰""" + return render_template('analysis.html', encoding='utf-8') @app.route('/api/extract', methods=['POST']) @@ -189,6 +253,322 @@ def sort_items(): }), 500 +@app.route('/api/export/sorted', methods=['POST']) +def export_sorted(): + """瀵煎嚭鎺掑簭缁撴灉""" + try: + data = request.get_json() + items = data.get('items', []) + export_format = data.get('format', 'txt') # txt, json, csv + + if not items: + return jsonify({ + 'success': False, + 'message': '娌℃湁鍙鍑虹殑鏁版嵁' + }), 400 + + if export_format == 'json': + content = json.dumps({ + 'items': items, + 'count': len(items), + 'export_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }, ensure_ascii=False, indent=2) + filename = f'鑳岃鎺掑簭缁撴灉_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json' + mimetype = 'application/json; charset=utf-8' + elif export_format == 'csv': + import csv + import io + output = io.StringIO() + writer = csv.writer(output) + writer.writerow(['搴忓彿', '鐭ヨ瘑鐐']) + for i, item in enumerate(items, 1): + writer.writerow([i, item]) + content = output.getvalue() + filename = f'鑳岃鎺掑簭缁撴灉_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv' + mimetype = 'text/csv; charset=utf-8' + else: # txt + content_lines = ['鑳岃鎺掑簭缁撴灉', '=' * 50, ''] + for i, item in enumerate(items, 1): + content_lines.append(f'{i}. {item}') + content_lines.extend(['', '=' * 50, f'瀵煎嚭鏃堕棿: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}']) + content = '\n'.join(content_lines) + filename = f'鑳岃鎺掑簭缁撴灉_{datetime.now().strftime("%Y%m%d_%H%M%S")}.txt' + mimetype = 'text/plain; charset=utf-8' + + response = Response( + content.encode('utf-8'), + mimetype=mimetype, + headers={ + 'Content-Disposition': f'attachment; filename="{filename}"' + } + ) + + return response + + except Exception as e: + logger.error(f"瀵煎嚭澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'瀵煎嚭澶辫触: {str(e)}' + }), 500 + + +# ==================== 涓氬姟鍔熻兘API ==================== + +@app.route('/api/user/login', methods=['POST']) +def user_login(): + """鐢ㄦ埛鐧诲綍""" + try: + data = request.get_json() + user_id = data.get('user_id', '').strip() + + if not user_id: + return jsonify({ + 'success': False, + 'message': '璇疯緭鍏ョ敤鎴稩D' + }), 400 + + # 鑾峰彇鐢ㄦ埛鏁版嵁锛堝鏋滀笉瀛樺湪浼氳嚜鍔ㄥ垱寤猴級 + core = get_app_core() + user_data = core.get_user_data(user_id) + + if user_data: + session['user_id'] = user_id + return jsonify({ + 'success': True, + 'user_id': user_id, + 'name': user_data.profile.get('name', '鏈缃') + }) + else: + return jsonify({ + 'success': False, + 'message': '鐢ㄦ埛鏁版嵁鑾峰彇澶辫触' + }), 500 + + except Exception as e: + logger.error(f"鐢ㄦ埛鐧诲綍澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'鐧诲綍澶辫触: {str(e)}' + }), 500 + + +@app.route('/api/user/register', methods=['POST']) +def user_register(): + """鐢ㄦ埛娉ㄥ唽""" + try: + data = request.get_json() + user_id = data.get('user_id', '').strip() + name = data.get('name', '').strip() + + if not user_id or not name: + return jsonify({ + 'success': False, + 'message': '璇疯緭鍏ョ敤鎴稩D鍜屽鍚' + }), 400 + + core = get_app_core() + user_data = core.get_user_data(user_id) + + # 鏇存柊鐢ㄦ埛鍩烘湰淇℃伅 + user_data.profile['name'] = name + core.data_manager.save_user_data(user_data) + + session['user_id'] = user_id + return jsonify({ + 'success': True, + 'user_id': user_id, + 'name': name + }) + + except Exception as e: + logger.error(f"鐢ㄦ埛娉ㄥ唽澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'娉ㄥ唽澶辫触: {str(e)}' + }), 500 + + +@app.route('/api/questionnaire/submit', methods=['POST']) +def submit_questionnaire(): + """鎻愪氦闂嵎""" + try: + user_id = session.get('user_id') or request.json.get('user_id') + if not user_id: + return jsonify({ + 'success': False, + 'message': '璇峰厛鐧诲綍' + }), 401 + + data = request.get_json() + questionnaire_type = data.get('type', 'basic') # basic, taste, physiological + answers = data.get('answers', {}) + + core = get_app_core() + input_data = { + 'type': 'questionnaire', + 'questionnaire_type': questionnaire_type, + 'answers': answers + } + + result = core.process_user_request(ModuleType.DATA_COLLECTION, input_data, user_id) + + if result and result.result.get('success', False): + return jsonify({ + 'success': True, + 'message': '闂嵎鎻愪氦鎴愬姛' + }) + else: + return jsonify({ + 'success': False, + 'message': result.result.get('message', '闂嵎鎻愪氦澶辫触') if result else '澶勭悊澶辫触' + }), 500 + + except Exception as e: + logger.error(f"鎻愪氦闂嵎澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'鎻愪氦澶辫触: {str(e)}' + }), 500 + + +@app.route('/api/meal/record', methods=['POST']) +def record_meal(): + """璁板綍椁愰""" + try: + user_id = session.get('user_id') or request.json.get('user_id') + if not user_id: + return jsonify({ + 'success': False, + 'message': '璇峰厛鐧诲綍' + }), 401 + + data = request.get_json() + meal_data = { + 'date': data.get('date', datetime.now().strftime('%Y-%m-%d')), + 'meal_type': data.get('meal_type', 'lunch'), # breakfast, lunch, dinner + 'foods': data.get('foods', []), + 'quantities': data.get('quantities', []), + 'calories': data.get('calories', 0), + 'satisfaction_score': data.get('satisfaction_score', 3), + 'notes': data.get('notes', '') + } + + core = get_app_core() + input_data = { + 'type': 'meal_record', + **meal_data + } + + result = core.process_user_request(ModuleType.DATA_COLLECTION, input_data, user_id) + + if result and result.result.get('success', False): + return jsonify({ + 'success': True, + 'message': '椁愰璁板綍鎴愬姛' + }) + else: + return jsonify({ + 'success': False, + 'message': result.result.get('message', '璁板綍澶辫触') if result else '澶勭悊澶辫触' + }), 500 + + except Exception as e: + logger.error(f"璁板綍椁愰澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'璁板綍澶辫触: {str(e)}' + }), 500 + + +@app.route('/api/recommendation/get', methods=['POST']) +def get_recommendations(): + """鑾峰彇椁愰鎺ㄨ崘""" + try: + user_id = session.get('user_id') or request.json.get('user_id') + if not user_id: + return jsonify({ + 'success': False, + 'message': '璇峰厛鐧诲綍' + }), 401 + + data = request.get_json() + meal_type = data.get('meal_type', 'lunch') + preferences = data.get('preferences', {}) + context = data.get('context', {}) + + core = get_app_core() + input_data = { + 'type': 'meal_recommendation', + 'meal_type': meal_type, + 'preferences': preferences, + 'context': context + } + + result = core.process_user_request(ModuleType.RECOMMENDATION, input_data, user_id) + + if result and result.result.get('success', False): + return jsonify({ + 'success': True, + 'recommendations': result.result.get('recommendations', []) + }) + else: + return jsonify({ + 'success': False, + 'message': result.result.get('message', '鎺ㄨ崘澶辫触') if result else '澶勭悊澶辫触', + 'recommendations': [] + }), 500 + + except Exception as e: + logger.error(f"鑾峰彇鎺ㄨ崘澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'鑾峰彇澶辫触: {str(e)}', + 'recommendations': [] + }), 500 + + +@app.route('/api/analysis/nutrition', methods=['POST']) +def analyze_nutrition(): + """钀ュ吇鍒嗘瀽""" + try: + user_id = session.get('user_id') or request.json.get('user_id') + if not user_id: + return jsonify({ + 'success': False, + 'message': '璇峰厛鐧诲綍' + }), 401 + + data = request.get_json() + meal_data = data.get('meal_data', {}) + + core = get_app_core() + input_data = { + 'type': 'nutrition_analysis', + 'meal_data': meal_data + } + + result = core.process_user_request(ModuleType.USER_ANALYSIS, input_data, user_id) + + if result and result.result.get('success', False): + return jsonify({ + 'success': True, + 'analysis': result.result.get('analysis', {}) + }) + else: + return jsonify({ + 'success': False, + 'message': result.result.get('message', '鍒嗘瀽澶辫触') if result else '澶勭悊澶辫触' + }), 500 + + except Exception as e: + logger.error(f"钀ュ吇鍒嗘瀽澶辫触: {e}") + return jsonify({ + 'success': False, + 'message': f'鍒嗘瀽澶辫触: {str(e)}' + }), 500 + + @app.route('/health') def health(): """鍋ュ悍妫鏌"""