Spaces:
Runtime error
Runtime error
J.B-Lin commited on
Commit ·
f8bc849
1
Parent(s): c48504c
refactor: remove voice tab, merge chat to home, fix recent records HTML table, clean emoji
Browse files
modules/__pycache__/nutrition_analyzer.cpython-311.pyc
CHANGED
|
Binary files a/modules/__pycache__/nutrition_analyzer.cpython-311.pyc and b/modules/__pycache__/nutrition_analyzer.cpython-311.pyc differ
|
|
|
ui/__pycache__/app_builder.cpython-311.pyc
CHANGED
|
Binary files a/ui/__pycache__/app_builder.cpython-311.pyc and b/ui/__pycache__/app_builder.cpython-311.pyc differ
|
|
|
ui/app_builder.py
CHANGED
|
@@ -1,12 +1,11 @@
|
|
| 1 |
"""
|
| 2 |
PregoPal - Gradio 界面构建
|
| 3 |
===========================
|
| 4 |
-
|
| 5 |
-
1. 🏠 首页(
|
| 6 |
-
2.
|
| 7 |
-
3.
|
| 8 |
-
4.
|
| 9 |
-
5. 📈 营养报告
|
| 10 |
"""
|
| 11 |
|
| 12 |
import datetime
|
|
@@ -19,7 +18,7 @@ from modules.nutrition_analyzer import NutritionAnalyzer
|
|
| 19 |
from modules.family_manager import RecipeManager, PreferenceManager, MemoryManager
|
| 20 |
from modules.diet_extractor import DietExtractor
|
| 21 |
from modules.nutrition_standards import DRIsParser
|
| 22 |
-
from utils import t, ZH, EN,
|
| 23 |
|
| 24 |
# ============================================================
|
| 25 |
# 全局实例(单例)
|
|
@@ -31,183 +30,70 @@ nutrition_analyzer = NutritionAnalyzer()
|
|
| 31 |
|
| 32 |
|
| 33 |
# ============================================================
|
| 34 |
-
#
|
| 35 |
# ============================================================
|
| 36 |
-
def
|
| 37 |
-
"""
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
"
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
# ========== 布局:极简首页 ==========
|
| 79 |
-
with gr.Column(elem_classes=["home-container"]):
|
| 80 |
-
# 标题
|
| 81 |
-
gr.Markdown(T["title"])
|
| 82 |
-
gr.Markdown(T["subtitle"])
|
| 83 |
-
|
| 84 |
-
# 语音启动按钮(居中大按钮)
|
| 85 |
-
with gr.Row():
|
| 86 |
-
with gr.Column(scale=1):
|
| 87 |
-
pass
|
| 88 |
-
with gr.Column(scale=2):
|
| 89 |
-
gr.HTML(f"""
|
| 90 |
-
<div style="text-align: center; padding: 32px 0;">
|
| 91 |
-
<button class="voice-main-btn" onclick="document.querySelectorAll('.tabs button')[1].click()">
|
| 92 |
-
🎙️<br><span style="font-size:14px">{T['tap_speak']}</span>
|
| 93 |
-
</button>
|
| 94 |
-
<p style="margin-top: 16px; color: #888; font-size: 15px;">{T['tap_hint']}</p>
|
| 95 |
-
</div>
|
| 96 |
-
""")
|
| 97 |
-
with gr.Column(scale=1):
|
| 98 |
-
pass
|
| 99 |
-
|
| 100 |
-
# AI 思考状态(实时显示)
|
| 101 |
-
thinking_box = gr.Textbox(
|
| 102 |
-
value=f"{T['thinking_label']}\n{T['thinking_placeholder']}",
|
| 103 |
-
label="",
|
| 104 |
-
interactive=False,
|
| 105 |
-
lines=3,
|
| 106 |
-
elem_classes=["thinking-box"],
|
| 107 |
-
)
|
| 108 |
-
|
| 109 |
-
# 简短最近记录摘要
|
| 110 |
-
with gr.Group(elem_classes=["home-card"]):
|
| 111 |
-
gr.Markdown(f"### {T['recent_log_label']}")
|
| 112 |
-
gr.Markdown(recent_display)
|
| 113 |
-
|
| 114 |
-
return thinking_box
|
| 115 |
|
| 116 |
|
| 117 |
# ============================================================
|
| 118 |
-
#
|
| 119 |
# ============================================================
|
| 120 |
-
def
|
| 121 |
-
"""
|
| 122 |
-
lang = lang_state.value if hasattr(lang_state, 'value') else "zh"
|
| 123 |
-
|
| 124 |
-
# ========== 顶部:全双工状态指示器 ==========
|
| 125 |
-
with gr.Row():
|
| 126 |
-
full_duplex_status = gr.HTML("""
|
| 127 |
-
<div style="padding: 8px 16px; text-align: center;">
|
| 128 |
-
<span class="status-dot idle"></span>
|
| 129 |
-
<span style="color: #666; font-size: 14px;">⏸️ 待命中 — 输入文字或点击下方麦克风开始对话</span>
|
| 130 |
-
</div>
|
| 131 |
-
""")
|
| 132 |
-
|
| 133 |
-
# ========== 中部:Chatbot 对话历史 ==========
|
| 134 |
-
chatbot = gr.Chatbot(
|
| 135 |
-
label=t("chat_label", lang),
|
| 136 |
-
height=420,
|
| 137 |
-
avatar_images=(None, "🌸"),
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
# ========== 底部:输入区域(文字 + 二合一语音按钮) ==========
|
| 141 |
-
with gr.Row():
|
| 142 |
-
msg = gr.Textbox(
|
| 143 |
-
label="",
|
| 144 |
-
placeholder=t("msg_placeholder", lang),
|
| 145 |
-
scale=5,
|
| 146 |
-
show_label=False,
|
| 147 |
-
elem_classes=["chat-input"],
|
| 148 |
-
)
|
| 149 |
-
send_btn = gr.Button(t("btn_send", lang), variant="primary", scale=1)
|
| 150 |
-
|
| 151 |
-
with gr.Row():
|
| 152 |
-
with gr.Column(scale=4):
|
| 153 |
-
audio_input = gr.Audio(
|
| 154 |
-
label="🎤 语音输入",
|
| 155 |
-
type="filepath",
|
| 156 |
-
sources=["microphone", "upload"],
|
| 157 |
-
)
|
| 158 |
-
with gr.Column(scale=1):
|
| 159 |
-
clear_btn = gr.Button(t("btn_clear", lang), size="sm")
|
| 160 |
-
auto_process = gr.Checkbox(label="自动发送", value=True, scale=0)
|
| 161 |
|
| 162 |
-
# ========== 右侧折叠面板:声纹管理 ==========
|
| 163 |
-
with gr.Accordion(t("voice_title", lang) if "###" not in t("voice_title", lang) else "🔊 声纹识别", open=False):
|
| 164 |
-
with gr.Row():
|
| 165 |
-
# 注册区
|
| 166 |
-
with gr.Column(scale=1):
|
| 167 |
-
gr.Markdown("#### 📝 注册新成员")
|
| 168 |
-
member_name = gr.Textbox(label=t("member_name", lang), placeholder="小明")
|
| 169 |
-
member_relation = gr.Dropdown(
|
| 170 |
-
label=t("member_relation", lang),
|
| 171 |
-
choices=ZH["relation_choices"] if lang == "zh" else EN["relation_choices"],
|
| 172 |
-
value=ZH["relation_choices"][0] if lang == "zh" else EN["relation_choices"][0],
|
| 173 |
-
)
|
| 174 |
-
audio_register = gr.Audio(label="录制声纹", type="filepath", sources=["microphone"])
|
| 175 |
-
register_btn = gr.Button(t("btn_register", lang), variant="primary", size="sm")
|
| 176 |
-
register_result = gr.Textbox(label="", interactive=False, lines=1, show_label=False)
|
| 177 |
-
|
| 178 |
-
# 识别区
|
| 179 |
-
with gr.Column(scale=1):
|
| 180 |
-
gr.Markdown("#### 🔍 识别说话人")
|
| 181 |
-
audio_identify = gr.Audio(label="上传语音", type="filepath", sources=["upload", "microphone"])
|
| 182 |
-
identify_btn = gr.Button(t("btn_identify", lang), variant="primary", size="sm")
|
| 183 |
-
identify_result = gr.Textbox(label="", interactive=False, lines=1, show_label=False)
|
| 184 |
-
|
| 185 |
-
# ========== 底部:今日简报 JSON ==========
|
| 186 |
-
with gr.Accordion(t("briefing_title", lang) if "###" not in t("briefing_title", lang) else "📋 今日简报", open=False):
|
| 187 |
-
briefing_display = gr.JSON(value={}, label="")
|
| 188 |
-
|
| 189 |
-
# ========== 对话事件处理 ==========
|
| 190 |
def respond(message, chat_history):
|
| 191 |
-
if not message:
|
| 192 |
-
return "", chat_history
|
| 193 |
-
|
| 194 |
-
# 更新状态为活跃
|
| 195 |
-
active_status = """
|
| 196 |
-
<div style="padding: 8px 16px; text-align: center;">
|
| 197 |
-
<span class="status-dot active"></span>
|
| 198 |
-
<span style="color: #4CAF50; font-size: 14px;">🔄 处理中...</span>
|
| 199 |
-
</div>
|
| 200 |
-
"""
|
| 201 |
|
| 202 |
extracted = DietExtractor.robust_extract(message)
|
| 203 |
response_parts = []
|
| 204 |
|
| 205 |
if lang == "zh":
|
| 206 |
-
greeting = "👋 收到!我来帮你处理..."
|
| 207 |
if extracted["diets"]:
|
| 208 |
for diet in extracted["diets"]:
|
| 209 |
meals_str = "、".join(f"{k}: {v}" for k, v in diet.get("meals", {}).items())
|
| 210 |
-
response_parts.append(f"
|
| 211 |
date_str = diet.get("日期", datetime.date.today().isoformat())
|
| 212 |
diet_logger.add_record(
|
| 213 |
member_name=diet.get("记录人", "用户"),
|
|
@@ -228,7 +114,7 @@ def _build_voice_tab(lang_state):
|
|
| 228 |
ingredients=recipe.get("食材", ""),
|
| 229 |
notes=recipe.get("备注", ""),
|
| 230 |
)
|
| 231 |
-
response_parts.append(f"
|
| 232 |
|
| 233 |
if extracted["preferences"]:
|
| 234 |
for pref in extracted["preferences"]:
|
|
@@ -239,31 +125,19 @@ def _build_voice_tab(lang_state):
|
|
| 239 |
avoid=pref.get("内容", "") if pref.get("类型") == "忌口" else "",
|
| 240 |
allergies=pref.get("内容", "") if pref.get("类型") == "过敏" else "",
|
| 241 |
)
|
| 242 |
-
response_parts.append("
|
| 243 |
|
| 244 |
if extracted["weights"]:
|
| 245 |
for w in extracted["weights"]:
|
| 246 |
-
response_parts.append(f"
|
| 247 |
-
|
| 248 |
-
if
|
| 249 |
-
|
| 250 |
-
next_step = extracted["thinking"].get("下一步", "")
|
| 251 |
-
response_parts.append(f"🤔 当前:{step} → {next_step}")
|
| 252 |
-
|
| 253 |
-
if len(response_parts) == 0:
|
| 254 |
-
response_parts.append(greeting)
|
| 255 |
-
if any(kw in message for kw in ["吃了", "想吃", "午餐", "晚餐", "早餐"]):
|
| 256 |
-
response_parts.append("我来帮你记录!下次可以直接说「我吃了...」我会自动记录。")
|
| 257 |
-
elif any(kw in message for kw in ["菜谱", "会做", "做饭"]):
|
| 258 |
-
response_parts.append("好的,我来记录家庭菜谱!")
|
| 259 |
-
else:
|
| 260 |
-
response_parts.append("有什么我可以帮你的吗?比如记录饮食、推荐食谱、或者聊聊今天的感受。")
|
| 261 |
else:
|
| 262 |
-
greeting = "👋 Got it! Let me help..."
|
| 263 |
if extracted["diets"]:
|
| 264 |
for diet in extracted["diets"]:
|
| 265 |
meals_str = ", ".join(f"{k}: {v}" for k, v in diet.get("meals", {}).items())
|
| 266 |
-
response_parts.append(f"
|
| 267 |
date_str = diet.get("日期", datetime.date.today().isoformat())
|
| 268 |
diet_logger.add_record(
|
| 269 |
member_name=diet.get("记录人", "User"),
|
|
@@ -284,7 +158,7 @@ def _build_voice_tab(lang_state):
|
|
| 284 |
ingredients=recipe.get("食材", ""),
|
| 285 |
notes=recipe.get("备注", ""),
|
| 286 |
)
|
| 287 |
-
response_parts.append(f"
|
| 288 |
|
| 289 |
if extracted["preferences"]:
|
| 290 |
for pref in extracted["preferences"]:
|
|
@@ -295,90 +169,99 @@ def _build_voice_tab(lang_state):
|
|
| 295 |
avoid=pref.get("内容", "") if pref.get("类型") == "忌口" else "",
|
| 296 |
allergies=pref.get("内容", "") if pref.get("类型") == "过敏" else "",
|
| 297 |
)
|
| 298 |
-
response_parts.append("
|
| 299 |
|
| 300 |
if extracted["weights"]:
|
| 301 |
for w in extracted["weights"]:
|
| 302 |
-
response_parts.append(f"
|
| 303 |
-
|
| 304 |
-
if extracted["thinking"]:
|
| 305 |
-
step = extracted["thinking"].get("当前步骤", "")
|
| 306 |
-
next_step = extracted["thinking"].get("下一步", "")
|
| 307 |
-
response_parts.append(f"🤔 Current: {step} → {next_step}")
|
| 308 |
|
| 309 |
-
if
|
| 310 |
-
response_parts.append(
|
| 311 |
-
response_parts.append("How can I help? Record your diet, recommend recipes, or just chat!")
|
| 312 |
|
| 313 |
response = "\n\n".join(response_parts)
|
| 314 |
chat_history.append((message, response))
|
|
|
|
| 315 |
|
| 316 |
-
|
| 317 |
-
idle_status = """
|
| 318 |
-
<div style="padding: 8px 16px; text-align: center;">
|
| 319 |
-
<span class="status-dot idle"></span>
|
| 320 |
-
<span style="color: #666; font-size: 14px;">⏸️ 待命中</span>
|
| 321 |
-
</div>
|
| 322 |
-
"""
|
| 323 |
-
|
| 324 |
-
return "", chat_history, idle_status
|
| 325 |
-
|
| 326 |
-
def process_audio(audio_path, auto_send, chat_history):
|
| 327 |
-
"""处理语音输入(预留 AI 语音识别接口)"""
|
| 328 |
-
if not audio_path:
|
| 329 |
-
return None, chat_history, full_duplex_status.value
|
| 330 |
-
|
| 331 |
-
# TODO: 对接 core/voice_processor.py 的 ASR
|
| 332 |
-
# 当前返回提示信息等待 AI 接入
|
| 333 |
-
active_status = """
|
| 334 |
-
<div style="padding: 8px 16px; text-align: center;">
|
| 335 |
-
<span class="status-dot active"></span>
|
| 336 |
-
<span style="color: #4CAF50; font-size: 14px;">🔊 语音识别中...</span>
|
| 337 |
-
</div>
|
| 338 |
-
"""
|
| 339 |
-
|
| 340 |
-
response = "🎤 语音已接收!(语音识别模块开发中,请暂时使用文字输入)"
|
| 341 |
-
|
| 342 |
-
if auto_send:
|
| 343 |
-
# 模拟:将音频作为消息处理
|
| 344 |
-
pass # 后续接入 ASR 后替换此逻辑
|
| 345 |
-
|
| 346 |
-
idle_status = """
|
| 347 |
-
<div style="padding: 8px 16px; text-align: center;">
|
| 348 |
-
<span class="status-dot idle"></span>
|
| 349 |
-
<span style="color: #666; font-size: 14px;">⏸️ 待命中</span>
|
| 350 |
-
</div>
|
| 351 |
-
"""
|
| 352 |
-
|
| 353 |
-
return None, chat_history, idle_status
|
| 354 |
|
| 355 |
-
# 绑定事件
|
| 356 |
-
send_btn.click(respond, inputs=[msg, chatbot], outputs=[msg, chatbot, full_duplex_status])
|
| 357 |
-
msg.submit(respond, inputs=[msg, chatbot], outputs=[msg, chatbot, full_duplex_status])
|
| 358 |
-
clear_btn.click(lambda: ([], full_duplex_status.value), outputs=[chatbot, full_duplex_status])
|
| 359 |
-
|
| 360 |
-
audio_input.stop_recording(
|
| 361 |
-
fn=process_audio,
|
| 362 |
-
inputs=[audio_input, auto_process, chatbot],
|
| 363 |
-
outputs=[audio_input, chatbot, full_duplex_status],
|
| 364 |
-
)
|
| 365 |
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
|
| 379 |
|
| 380 |
# ============================================================
|
| 381 |
-
# Tab
|
| 382 |
# ============================================================
|
| 383 |
def _build_family_tab(lang_state):
|
| 384 |
"""构建家庭饮食习惯页面(动态语言)"""
|
|
@@ -490,7 +373,7 @@ def _build_family_tab(lang_state):
|
|
| 490 |
|
| 491 |
|
| 492 |
# ============================================================
|
| 493 |
-
# Tab
|
| 494 |
# ============================================================
|
| 495 |
def _build_summary_tab(lang_state):
|
| 496 |
"""构建三天总结页面(动态语言)"""
|
|
@@ -523,13 +406,13 @@ def _build_summary_tab(lang_state):
|
|
| 523 |
|
| 524 |
suggestions = []
|
| 525 |
if deficits:
|
| 526 |
-
suggestions.append("
|
| 527 |
for n in deficits[:3]:
|
| 528 |
foods = DRIsParser.RICH_FOODS.get(n, [])
|
| 529 |
if foods:
|
| 530 |
-
suggestions.append(f"
|
| 531 |
else:
|
| 532 |
-
suggestions.append("
|
| 533 |
|
| 534 |
return report, "\n".join(suggestions)
|
| 535 |
|
|
@@ -541,7 +424,7 @@ def _build_summary_tab(lang_state):
|
|
| 541 |
|
| 542 |
|
| 543 |
# ============================================================
|
| 544 |
-
# Tab
|
| 545 |
# ============================================================
|
| 546 |
def _build_report_tab(lang_state):
|
| 547 |
"""构建营养报告页面(动态语言)"""
|
|
@@ -573,7 +456,8 @@ def _build_report_tab(lang_state):
|
|
| 573 |
records = diet_logger.get_recent_records(days=int(days))
|
| 574 |
analysis = nutrition_analyzer.analyze_diet(records)
|
| 575 |
md_path = nutrition_analyzer.export_report_markdown(analysis)
|
| 576 |
-
|
|
|
|
| 577 |
|
| 578 |
generate_btn.click(fn=generate_report, inputs=[analysis_days], outputs=[report_text, report_plot])
|
| 579 |
export_md_btn.click(fn=export_report, inputs=[analysis_days], outputs=export_result)
|
|
@@ -583,7 +467,7 @@ def _build_report_tab(lang_state):
|
|
| 583 |
# 主入口:创建应用
|
| 584 |
# ============================================================
|
| 585 |
def create_app(loop=None):
|
| 586 |
-
"""创建主应用(
|
| 587 |
|
| 588 |
with gr.Blocks(
|
| 589 |
title="PregoPal - 孕期陪护AI助手",
|
|
@@ -606,7 +490,7 @@ def create_app(loop=None):
|
|
| 606 |
background: var(--card-bg) !important;
|
| 607 |
border: 1px solid var(--card-border) !important;
|
| 608 |
box-shadow: 0 2px 12px rgba(255, 105, 135, 0.08) !important;
|
| 609 |
-
min-height:
|
| 610 |
}
|
| 611 |
.thinking-box textarea {
|
| 612 |
background: linear-gradient(135deg, #F3E5F5, #FFF) !important;
|
|
@@ -633,13 +517,13 @@ def create_app(loop=None):
|
|
| 633 |
with gr.Column(scale=4):
|
| 634 |
gr.Markdown(f"""
|
| 635 |
<div style="text-align: left; padding: 8px 0;">
|
| 636 |
-
<h1 style="margin: 0;">
|
| 637 |
<p style="color: #999; margin: 4px 0 0 0; font-size: 16px;">孕期陪护AI助手 — 温馨的家庭式伴侣</p>
|
| 638 |
</div>
|
| 639 |
""")
|
| 640 |
with gr.Column(scale=1, min_width=120):
|
| 641 |
lang_selector = gr.Radio(
|
| 642 |
-
label="
|
| 643 |
choices=["中文", "English"],
|
| 644 |
value="中文",
|
| 645 |
interactive=True,
|
|
@@ -651,14 +535,11 @@ def create_app(loop=None):
|
|
| 651 |
lang_selector.change(fn=switch_lang, inputs=[lang_selector], outputs=[lang_state])
|
| 652 |
|
| 653 |
# ============================================================
|
| 654 |
-
#
|
| 655 |
# ============================================================
|
| 656 |
with gr.Tabs():
|
| 657 |
-
with gr.Tab("
|
| 658 |
-
_build_home_tab(
|
| 659 |
-
|
| 660 |
-
with gr.Tab(t("tab_voice", "zh"), id="tab_voice"):
|
| 661 |
-
_build_voice_tab(lang_state)
|
| 662 |
|
| 663 |
with gr.Tab(t("tab_family", "zh"), id="tab_family"):
|
| 664 |
_build_family_tab(lang_state)
|
|
|
|
| 1 |
"""
|
| 2 |
PregoPal - Gradio 界面构建
|
| 3 |
===========================
|
| 4 |
+
4 个子页面 + 中英文切换 + 自定义 CSS:
|
| 5 |
+
1. 🏠 首页(对话 + AI 思考 + 最近记录)
|
| 6 |
+
2. 👨👩👧👦 家庭饮食习惯
|
| 7 |
+
3. 📊 三天总结
|
| 8 |
+
4. 📈 营养报告
|
|
|
|
| 9 |
"""
|
| 10 |
|
| 11 |
import datetime
|
|
|
|
| 18 |
from modules.family_manager import RecipeManager, PreferenceManager, MemoryManager
|
| 19 |
from modules.diet_extractor import DietExtractor
|
| 20 |
from modules.nutrition_standards import DRIsParser
|
| 21 |
+
from utils import t, ZH, EN, CUSTOM_CSS
|
| 22 |
|
| 23 |
# ============================================================
|
| 24 |
# 全局实例(单例)
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
# ============================================================
|
| 33 |
+
# 工具:最近记录渲染为 HTML 表格
|
| 34 |
# ============================================================
|
| 35 |
+
def _render_recent_records_html(records, lang="zh"):
|
| 36 |
+
"""用最可靠的 HTML 表格展示最近饮食记录,避免 Markdown 乱换行"""
|
| 37 |
+
if not records:
|
| 38 |
+
no_record = "还没有记录,开始对话吧!" if lang == "zh" else "No records yet. Start a conversation!"
|
| 39 |
+
return f'<div style="color:#999;padding:12px;text-align:center;font-size:15px;">{no_record}</div>'
|
| 40 |
+
|
| 41 |
+
rows = ""
|
| 42 |
+
for r in records[-6:]:
|
| 43 |
+
date = r.get("date", "")[-5:] if r.get("date") else ""
|
| 44 |
+
member = r.get("member_name", "") or ""
|
| 45 |
+
meals = r.get("meals", {})
|
| 46 |
+
meal_parts = []
|
| 47 |
+
for meal_time, food in meals.items():
|
| 48 |
+
meal_parts.append(f"<b>{meal_time}</b> {food}")
|
| 49 |
+
meal_str = " | ".join(meal_parts) if meal_parts else "—"
|
| 50 |
+
rows += f"""
|
| 51 |
+
<tr>
|
| 52 |
+
<td style="white-space:nowrap;color:#E91E63;font-weight:500;">{date}</td>
|
| 53 |
+
<td style="white-space:nowrap;">{member}</td>
|
| 54 |
+
<td>{meal_str}</td>
|
| 55 |
+
</tr>"""
|
| 56 |
+
|
| 57 |
+
label_date = "日期" if lang == "zh" else "Date"
|
| 58 |
+
label_member = "成员" if lang == "zh" else "Member"
|
| 59 |
+
label_meals = "餐食" if lang == "zh" else "Meals"
|
| 60 |
+
|
| 61 |
+
return f"""
|
| 62 |
+
<div style="overflow-x:auto;">
|
| 63 |
+
<table style="width:100%;border-collapse:collapse;font-size:14px;font-family:inherit;">
|
| 64 |
+
<thead>
|
| 65 |
+
<tr style="background:#FCE4EC;color:#880E4F;">
|
| 66 |
+
<th style="padding:10px 12px;text-align:left;border-radius:8px 0 0 0;">{label_date}</th>
|
| 67 |
+
<th style="padding:10px 12px;text-align:left;">{label_member}</th>
|
| 68 |
+
<th style="padding:10px 12px;text-align:left;border-radius:0 8px 0 0;">{label_meals}</th>
|
| 69 |
+
</tr>
|
| 70 |
+
</thead>
|
| 71 |
+
<tbody>
|
| 72 |
+
{rows}
|
| 73 |
+
</tbody>
|
| 74 |
+
</table>
|
| 75 |
+
</div>
|
| 76 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
# ============================================================
|
| 80 |
+
# 工具:对话处理函数(首页共用)
|
| 81 |
# ============================================================
|
| 82 |
+
def _build_respond_fn(lang):
|
| 83 |
+
"""返回对话处理函数"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def respond(message, chat_history):
|
| 86 |
+
if not message or not message.strip():
|
| 87 |
+
return "", chat_history
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
extracted = DietExtractor.robust_extract(message)
|
| 90 |
response_parts = []
|
| 91 |
|
| 92 |
if lang == "zh":
|
|
|
|
| 93 |
if extracted["diets"]:
|
| 94 |
for diet in extracted["diets"]:
|
| 95 |
meals_str = "、".join(f"{k}: {v}" for k, v in diet.get("meals", {}).items())
|
| 96 |
+
response_parts.append(f"已记录饮食:{meals_str}")
|
| 97 |
date_str = diet.get("日期", datetime.date.today().isoformat())
|
| 98 |
diet_logger.add_record(
|
| 99 |
member_name=diet.get("记录人", "用户"),
|
|
|
|
| 114 |
ingredients=recipe.get("食材", ""),
|
| 115 |
notes=recipe.get("备注", ""),
|
| 116 |
)
|
| 117 |
+
response_parts.append(f"已收藏菜谱:{name}")
|
| 118 |
|
| 119 |
if extracted["preferences"]:
|
| 120 |
for pref in extracted["preferences"]:
|
|
|
|
| 125 |
avoid=pref.get("内容", "") if pref.get("类型") == "忌口" else "",
|
| 126 |
allergies=pref.get("内容", "") if pref.get("类型") == "过敏" else "",
|
| 127 |
)
|
| 128 |
+
response_parts.append("已保存饮食偏好")
|
| 129 |
|
| 130 |
if extracted["weights"]:
|
| 131 |
for w in extracted["weights"]:
|
| 132 |
+
response_parts.append(f"已记录体重:{w.get('体重', '')}kg")
|
| 133 |
+
|
| 134 |
+
if not response_parts:
|
| 135 |
+
response_parts.append("收到!有什么我可以帮你的吗?\n比如记录饮食、推荐食谱、或者聊聊今天的感受。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
else:
|
|
|
|
| 137 |
if extracted["diets"]:
|
| 138 |
for diet in extracted["diets"]:
|
| 139 |
meals_str = ", ".join(f"{k}: {v}" for k, v in diet.get("meals", {}).items())
|
| 140 |
+
response_parts.append(f"Logged diet: {meals_str}")
|
| 141 |
date_str = diet.get("日期", datetime.date.today().isoformat())
|
| 142 |
diet_logger.add_record(
|
| 143 |
member_name=diet.get("记录人", "User"),
|
|
|
|
| 158 |
ingredients=recipe.get("食材", ""),
|
| 159 |
notes=recipe.get("备注", ""),
|
| 160 |
)
|
| 161 |
+
response_parts.append(f"Saved recipe: {name}")
|
| 162 |
|
| 163 |
if extracted["preferences"]:
|
| 164 |
for pref in extracted["preferences"]:
|
|
|
|
| 169 |
avoid=pref.get("内容", "") if pref.get("类型") == "忌口" else "",
|
| 170 |
allergies=pref.get("内容", "") if pref.get("类型") == "过敏" else "",
|
| 171 |
)
|
| 172 |
+
response_parts.append("Saved preferences")
|
| 173 |
|
| 174 |
if extracted["weights"]:
|
| 175 |
for w in extracted["weights"]:
|
| 176 |
+
response_parts.append(f"Logged weight: {w.get('体重', '')}kg")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
+
if not response_parts:
|
| 179 |
+
response_parts.append("Got it! How can I help? Record your diet, recommend recipes, or just chat!")
|
|
|
|
| 180 |
|
| 181 |
response = "\n\n".join(response_parts)
|
| 182 |
chat_history.append((message, response))
|
| 183 |
+
return "", chat_history
|
| 184 |
|
| 185 |
+
return respond
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
+
# ============================================================
|
| 189 |
+
# Tab 1: 🏠 首页(对话 + 最近记录)
|
| 190 |
+
# ============================================================
|
| 191 |
+
def _build_home_tab(lang_state):
|
| 192 |
+
"""构建首页:对话输入 + AI 回复 + 最近饮食记录表格"""
|
| 193 |
+
lang = lang_state.value if hasattr(lang_state, 'value') else "zh"
|
| 194 |
+
|
| 195 |
+
_home = {
|
| 196 |
+
"zh": {
|
| 197 |
+
"title": "# PregoPal",
|
| 198 |
+
"subtitle": "你的孕期AI伴侣,用对话记录每一天",
|
| 199 |
+
"chat_label": "对话",
|
| 200 |
+
"msg_placeholder": "说说今天吃了什么、心情怎么样...",
|
| 201 |
+
"btn_send": "发送",
|
| 202 |
+
"btn_clear": "清空对话",
|
| 203 |
+
"recent_log_label": "最近的饮食记录",
|
| 204 |
+
"thinking_placeholder": "等待你的对话...",
|
| 205 |
+
},
|
| 206 |
+
"en": {
|
| 207 |
+
"title": "# PregoPal",
|
| 208 |
+
"subtitle": "Your pregnancy AI companion — just talk naturally",
|
| 209 |
+
"chat_label": "Chat",
|
| 210 |
+
"msg_placeholder": "Tell me what you ate, how you feel...",
|
| 211 |
+
"btn_send": "Send",
|
| 212 |
+
"btn_clear": "Clear",
|
| 213 |
+
"recent_log_label": "Recent Diet Records",
|
| 214 |
+
"thinking_placeholder": "Waiting for you...",
|
| 215 |
+
}
|
| 216 |
+
}
|
| 217 |
+
T = _home[lang]
|
| 218 |
+
|
| 219 |
+
# 最近记录
|
| 220 |
+
records = DietLogger().get_recent_records(days=3)
|
| 221 |
+
recent_html = _render_recent_records_html(records, lang)
|
| 222 |
+
|
| 223 |
+
# 对话处理函数
|
| 224 |
+
respond = _build_respond_fn(lang)
|
| 225 |
+
|
| 226 |
+
with gr.Column(elem_classes=["home-container"]):
|
| 227 |
+
# 标题
|
| 228 |
+
gr.Markdown(T["title"])
|
| 229 |
+
gr.Markdown(T["subtitle"])
|
| 230 |
+
|
| 231 |
+
# Chatbot 对话历史
|
| 232 |
+
chatbot = gr.Chatbot(
|
| 233 |
+
label=T["chat_label"],
|
| 234 |
+
height=380,
|
| 235 |
+
avatar_images=(None, None),
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
# 输入行
|
| 239 |
+
with gr.Row():
|
| 240 |
+
msg = gr.Textbox(
|
| 241 |
+
label="",
|
| 242 |
+
placeholder=T["msg_placeholder"],
|
| 243 |
+
scale=5,
|
| 244 |
+
show_label=False,
|
| 245 |
+
elem_classes=["chat-input"],
|
| 246 |
+
)
|
| 247 |
+
send_btn = gr.Button(T["btn_send"], variant="primary", scale=1)
|
| 248 |
+
clear_btn = gr.Button(T["btn_clear"], size="sm", scale=1)
|
| 249 |
+
|
| 250 |
+
# 最近饮食记录
|
| 251 |
+
with gr.Group(elem_classes=["home-card"]):
|
| 252 |
+
gr.Markdown(f"### {T['recent_log_label']}")
|
| 253 |
+
gr.HTML(recent_html)
|
| 254 |
|
| 255 |
+
# 绑定事件
|
| 256 |
+
send_btn.click(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
|
| 257 |
+
msg.submit(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
|
| 258 |
+
clear_btn.click(lambda: [], outputs=[chatbot])
|
| 259 |
+
|
| 260 |
+
return chatbot, msg, send_btn, clear_btn
|
| 261 |
|
| 262 |
|
| 263 |
# ============================================================
|
| 264 |
+
# Tab 2: 👨👩👧👦 家庭饮食习惯
|
| 265 |
# ============================================================
|
| 266 |
def _build_family_tab(lang_state):
|
| 267 |
"""构建家庭饮食习惯页面(动态语言)"""
|
|
|
|
| 373 |
|
| 374 |
|
| 375 |
# ============================================================
|
| 376 |
+
# Tab 3: 📊 三天总结
|
| 377 |
# ============================================================
|
| 378 |
def _build_summary_tab(lang_state):
|
| 379 |
"""构建三天总结页面(动态语言)"""
|
|
|
|
| 406 |
|
| 407 |
suggestions = []
|
| 408 |
if deficits:
|
| 409 |
+
suggestions.append("建议增加以下食材到家庭菜单:")
|
| 410 |
for n in deficits[:3]:
|
| 411 |
foods = DRIsParser.RICH_FOODS.get(n, [])
|
| 412 |
if foods:
|
| 413 |
+
suggestions.append(f" * {n} -> {'、'.join(foods[:4])}")
|
| 414 |
else:
|
| 415 |
+
suggestions.append("近三天营养状况良好!")
|
| 416 |
|
| 417 |
return report, "\n".join(suggestions)
|
| 418 |
|
|
|
|
| 424 |
|
| 425 |
|
| 426 |
# ============================================================
|
| 427 |
+
# Tab 4: 📈 营养报告
|
| 428 |
# ============================================================
|
| 429 |
def _build_report_tab(lang_state):
|
| 430 |
"""构建营养报告页面(动态语言)"""
|
|
|
|
| 456 |
records = diet_logger.get_recent_records(days=int(days))
|
| 457 |
analysis = nutrition_analyzer.analyze_diet(records)
|
| 458 |
md_path = nutrition_analyzer.export_report_markdown(analysis)
|
| 459 |
+
prefix = "导出成功" if lang == "zh" else "Exported"
|
| 460 |
+
return f"{prefix}: {md_path.name}"
|
| 461 |
|
| 462 |
generate_btn.click(fn=generate_report, inputs=[analysis_days], outputs=[report_text, report_plot])
|
| 463 |
export_md_btn.click(fn=export_report, inputs=[analysis_days], outputs=export_result)
|
|
|
|
| 467 |
# 主入口:创建应用
|
| 468 |
# ============================================================
|
| 469 |
def create_app(loop=None):
|
| 470 |
+
"""创建主应用(4 Tab + 中英文切换 + 自定义 CSS)"""
|
| 471 |
|
| 472 |
with gr.Blocks(
|
| 473 |
title="PregoPal - 孕期陪护AI助手",
|
|
|
|
| 490 |
background: var(--card-bg) !important;
|
| 491 |
border: 1px solid var(--card-border) !important;
|
| 492 |
box-shadow: 0 2px 12px rgba(255, 105, 135, 0.08) !important;
|
| 493 |
+
min-height: 60px;
|
| 494 |
}
|
| 495 |
.thinking-box textarea {
|
| 496 |
background: linear-gradient(135deg, #F3E5F5, #FFF) !important;
|
|
|
|
| 517 |
with gr.Column(scale=4):
|
| 518 |
gr.Markdown(f"""
|
| 519 |
<div style="text-align: left; padding: 8px 0;">
|
| 520 |
+
<h1 style="margin: 0;"> PregoPal</h1>
|
| 521 |
<p style="color: #999; margin: 4px 0 0 0; font-size: 16px;">孕期陪护AI助手 — 温馨的家庭式伴侣</p>
|
| 522 |
</div>
|
| 523 |
""")
|
| 524 |
with gr.Column(scale=1, min_width=120):
|
| 525 |
lang_selector = gr.Radio(
|
| 526 |
+
label=" Language",
|
| 527 |
choices=["中文", "English"],
|
| 528 |
value="中文",
|
| 529 |
interactive=True,
|
|
|
|
| 535 |
lang_selector.change(fn=switch_lang, inputs=[lang_selector], outputs=[lang_state])
|
| 536 |
|
| 537 |
# ============================================================
|
| 538 |
+
# 4 个 Tab
|
| 539 |
# ============================================================
|
| 540 |
with gr.Tabs():
|
| 541 |
+
with gr.Tab(" 首页", id="tab_home"):
|
| 542 |
+
_build_home_tab(lang_state)
|
|
|
|
|
|
|
|
|
|
| 543 |
|
| 544 |
with gr.Tab(t("tab_family", "zh"), id="tab_family"):
|
| 545 |
_build_family_tab(lang_state)
|