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 个子页面 + 中英文切换 + 自定义 CSS:
5
- 1. 🏠 首页(语音启动 + 简报卡片 + AI 思考状态
6
- 2. 🎙️ 语音交互(全双工 Chatbot + 声纹面板)
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, get_home_cards, CUSTOM_CSS
23
 
24
  # ============================================================
25
  # 全局实例(单例)
@@ -31,183 +30,70 @@ nutrition_analyzer = NutritionAnalyzer()
31
 
32
 
33
  # ============================================================
34
- # Tab 1: 🏠 首页
35
  # ============================================================
36
- def _build_home_tab(loop, lang_state):
37
- """构建首页(简洁对话入口 + AI 思考状态)"""
38
- lang = lang_state.value if hasattr(lang_state, 'value') else "zh"
39
-
40
- from modules.family_manager import MemoryManager
41
-
42
- # ========== 语言相关文本 ==========
43
- _home = {
44
- "zh": {
45
- "title": "# 🌸 PregoPal",
46
- "subtitle": "你的孕期AI伴侣,用对话记录每一天",
47
- "tap_speak": "🎙️ 开始对话",
48
- "tap_hint": "说说今天吃了什么、心情怎么样...",
49
- "thinking_label": "🤔 PregoPal 在想...",
50
- "thinking_placeholder": "等待你的对话开启...",
51
- "recent_log_label": "📝 最近的饮食记录",
52
- "no_record": "还没有记录,开始对话吧!",
53
- },
54
- "en": {
55
- "title": "# 🌸 PregoPal",
56
- "subtitle": "Your pregnancy AI companion — just talk naturally",
57
- "tap_speak": "🎙️ Start Talking",
58
- "tap_hint": "Tell me what you ate, how you feel...",
59
- "thinking_label": "🤔 PregoPal is thinking...",
60
- "thinking_placeholder": "Waiting for you...",
61
- "recent_log_label": "📝 Recent Logs",
62
- "no_record": "No records yet. Start a conversation!",
63
- }
64
- }
65
- T = _home[lang]
66
-
67
- # 获取最近饮食摘要
68
- records = DietLogger().get_recent_records(days=3)
69
- if records:
70
- recent_lines = []
71
- for r in records[-4:]:
72
- meals_str = "、".join(f"{k}:{v}" for k, v in r.get("meals", {}).items())
73
- recent_lines.append(f"• {r['date']} {r.get('member_name','')} {meals_str}")
74
- recent_display = "\n".join(recent_lines)
75
- else:
76
- recent_display = T["no_record"]
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
- # Tab 2: 🎙️ 语音交互
119
  # ============================================================
120
- def _build_voice_tab(lang_state):
121
- """构建语音交互页面(全双工 Chatbot + 折叠声纹面板)"""
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, full_duplex_status.value
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"已记录饮食:{meals_str}")
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"已收藏菜谱:{name}")
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"已记录体重:{w.get('体重', '')}kg")
247
-
248
- if extracted["thinking"]:
249
- step = extracted["thinking"].get("当前步骤", "")
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"Logged diet: {meals_str}")
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"Saved recipe: {name}")
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("Saved preferences")
299
 
300
  if extracted["weights"]:
301
  for w in extracted["weights"]:
302
- response_parts.append(f"Logged weight: {w.get('体重', '')}kg")
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 len(response_parts) == 0:
310
- response_parts.append(greeting)
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
- register_btn.click(
367
- fn=lambda name, rel, audio: voiceprint_mgr.register_member(name, rel, audio),
368
- inputs=[member_name, member_relation, audio_register],
369
- outputs=[register_result],
370
- )
371
- identify_btn.click(
372
- fn=lambda audio: voiceprint_mgr.identify_speaker(audio),
373
- inputs=[audio_identify],
374
- outputs=[identify_result],
375
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
- return chatbot, msg, send_btn, clear_btn, briefing_display
 
 
 
 
 
378
 
379
 
380
  # ============================================================
381
- # Tab 3: 👨‍👩‍👧‍👦 家庭饮食习惯(保持原有逻辑,微调配色)
382
  # ============================================================
383
  def _build_family_tab(lang_state):
384
  """构建家庭饮食习惯页面(动态语言)"""
@@ -490,7 +373,7 @@ def _build_family_tab(lang_state):
490
 
491
 
492
  # ============================================================
493
- # Tab 4: 📊 三天总结(保持原有逻辑)
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" {n} {'、'.join(foods[:4])}")
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 5: 📈 营养报告(保持原有逻辑)
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
- return f" {t('btn_export_md', lang) if lang == 'zh' else 'Exported'}: {md_path.name}"
 
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
- """创建主应用(5 Tab + 中英文切换 + 自定义 CSS)"""
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: 140px;
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;">🌸 PregoPal</h1>
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="🌐 Language",
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
- # 5 个 Tab
655
  # ============================================================
656
  with gr.Tabs():
657
- with gr.Tab("🏠 首页", id="tab_home"):
658
- _build_home_tab(loop, lang_state)
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>&nbsp;{food}")
49
+ meal_str = "&nbsp;|&nbsp;".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)