Spaces:
Runtime error
Runtime error
J.B-Lin commited on
Commit ·
153f3f8
1
Parent(s): 3e28c81
refactor: 合并三天总结到营养报告,移除独立Tab
Browse files- app.py +1 -1
- docs/UI改进规划_用户反馈.md +159 -0
- ui/app_builder.py +53 -77
app.py
CHANGED
|
@@ -29,7 +29,7 @@ if __name__ == "__main__":
|
|
| 29 |
demo = create_app(loop)
|
| 30 |
demo.launch(
|
| 31 |
server_name="127.0.0.1",
|
| 32 |
-
server_port=
|
| 33 |
share=False,
|
| 34 |
debug=False,
|
| 35 |
css=CUSTOM_CSS,
|
|
|
|
| 29 |
demo = create_app(loop)
|
| 30 |
demo.launch(
|
| 31 |
server_name="127.0.0.1",
|
| 32 |
+
server_port=7880,
|
| 33 |
share=False,
|
| 34 |
debug=False,
|
| 35 |
css=CUSTOM_CSS,
|
docs/UI改进规划_用户反馈.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# PregoPal UI 改进规划 — 用户反馈汇总
|
| 2 |
+
|
| 3 |
+
> 日期:2026-06-09
|
| 4 |
+
> 负责:UI 设计 Cline
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
## 用户原始反馈(逐字保留)
|
| 9 |
+
|
| 10 |
+
> 现在改成卡片式的确实好看一些了,但是卡片的布局不均匀。存在以下问题:
|
| 11 |
+
|
| 12 |
+
### 1. 首页 — 语音提示文案
|
| 13 |
+
> "全双工语音交互,边说边听"请改为直接到提示,用户不是工程师,没必要提示全双工这样的词语
|
| 14 |
+
|
| 15 |
+
- **问题**:`tap_hint` 字段中使用了「全双工语音交互」这种工程师术语
|
| 16 |
+
- **位置**:`utils.py` `_home_texts()` 第 52 行
|
| 17 |
+
- **修改**:改为用户友好的提示,如「点击开始对话」/「Tap to talk」
|
| 18 |
+
|
| 19 |
+
### 2. 首页 — 红色大按钮功能
|
| 20 |
+
> 上面的红色大按钮怎么样才算是启动呢,这部分逻辑有设计吗?
|
| 21 |
+
|
| 22 |
+
- **问题**:红色圆形按钮(`.voice-main-btn`)目前只是一个装饰性 HTML `<button>`,没有任何 `click` 事件绑定
|
| 23 |
+
- **位置**:`ui/app_builder.py` 第 180-187 行
|
| 24 |
+
- **现状**:纯 CSS 样式,没有实际的语音启动逻辑
|
| 25 |
+
- **✅ 已决策**:用户要求真实语音对话启动,先用 `pass` 占位避免报错,设计好函数接口便于后续接入 Llama 大模型
|
| 26 |
+
- **接口设计**:
|
| 27 |
+
```python
|
| 28 |
+
async def start_voice_session(audio_data: bytes, lang: str = "zh") -> dict:
|
| 29 |
+
"""
|
| 30 |
+
语音会话入口(当前 pass 占位,待接入 Llama 模型)
|
| 31 |
+
|
| 32 |
+
Args:
|
| 33 |
+
audio_data: 原始音频数据(bytes)
|
| 34 |
+
lang: 语言标识 "zh" / "en"
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
dict: {
|
| 38 |
+
"text": str, # ASR 转写文本
|
| 39 |
+
"response": str, # AI 回复文本
|
| 40 |
+
"speaker": str, # 识别出的说话人
|
| 41 |
+
"audio_response": bytes | None # TTS 音频(可选)
|
| 42 |
+
}
|
| 43 |
+
"""
|
| 44 |
+
pass
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### 3. 首页 — AI 思考框 UI 问题
|
| 48 |
+
> 下面的"🤔 AI 思考中... 等待对话中..."UI设计也有问题,外面还有个方框写着textbox,下方的所有内容都是两个框包着,是不是哪里逻辑不对;并且文字框内部是灰色,这明显不对
|
| 49 |
+
|
| 50 |
+
- **问题 3a**:`gr.Textbox` 外层有 Gradio 默认的 "textbox" 标签边框
|
| 51 |
+
- **问题 3b**:文本框内部背景是灰色(默认 textarea 样式)
|
| 52 |
+
- **问题 3c**:下方内容区域被两层框包裹(可能是 `.home-card` + `.glass-card` 嵌套)
|
| 53 |
+
- **位置**:`ui/app_builder.py` 第 189-196 行,以及 CSS `.thinking-box` 样式
|
| 54 |
+
- **修改**:
|
| 55 |
+
- 将 `gr.Textbox` 改为 `gr.HTML` 或 `gr.Markdown` 来展示思考状态(去掉 textarea 外壳)
|
| 56 |
+
- 或者通过 CSS 彻底隐藏 textarea 的边框和灰色背景
|
| 57 |
+
|
| 58 |
+
### 4. 顶部标题 — 缺乏英文和趣味字体
|
| 59 |
+
> 页面最顶上的"孕期陪护AI助手 — 温馨的家庭式伴侣"请同时提供英文,并且尝试使用更有趣的字体
|
| 60 |
+
|
| 61 |
+
- **问题**:顶部标题只有中文,没有英文副标题
|
| 62 |
+
- **位置**:`ui/app_builder.py` 第 360-365 行(硬编码 HTML 字符串)
|
| 63 |
+
- **修改**:
|
| 64 |
+
- 添加英文副标题:`Pregnancy Companion AI — Your Cozy Family Partner`
|
| 65 |
+
- 引入趣味字体(Google Fonts 圆体/手写体,如 `Nunito`、`Quicksand`、`Comic Neue` 或中文字体如站酷快乐体)
|
| 66 |
+
|
| 67 |
+
### 5. 家庭饮食习惯 — 子页面默认状态
|
| 68 |
+
> 请默认打开家庭菜谱这样子页面,而不是在最开始的时候三个都不打开,看起来像是卡住了一样
|
| 69 |
+
|
| 70 |
+
- **问题**:三个子 Tab(菜谱/偏好/记忆)进入时全部折叠,看起来像页面卡住了
|
| 71 |
+
- **位置**:`ui/app_builder.py` 第 240-263 行,`gr.Tabs()` 没有设置默认选中
|
| 72 |
+
- **修改**:首次加载时默认显示「🍳 家庭菜谱」Tab
|
| 73 |
+
|
| 74 |
+
### 6. 孕期阶段 — 嵌套边框问题
|
| 75 |
+
> 孕期阶段也是显示不太对,黑色的文本框边缘外面还是黑色的文本框边缘
|
| 76 |
+
|
| 77 |
+
- **问题**:嵌套的 Group 导致双层边框
|
| 78 |
+
- **位置**:`ui/app_builder.py` 第 201-207 行,首页卡片行中的 `card-trimester`
|
| 79 |
+
- **可能原因**:`gr.Group(elem_classes=["home-card", "card-trimester"])` 嵌套在 `gr.Column()` 内,Gradio 给每个 Grroup 都加了边框
|
| 80 |
+
|
| 81 |
+
### 7. 营养报告 — Slider 无法拖动
|
| 82 |
+
> 营养报告中,分析天数的进度条完全没法拖动
|
| 83 |
+
|
| 84 |
+
- **问题**:`gr.Slider` 在 Gradio 中有时渲染为不可拖动的进度条
|
| 85 |
+
- **位置**:`ui/app_builder.py` 第 321-325 行
|
| 86 |
+
- **修改**:改为 `gr.Number` 输入框 + 手动输入天数,或使用 `gr.Slider` 的 `interactive=True`(需确认)
|
| 87 |
+
|
| 88 |
+
### 8. 营养报告 — 右上角"刷新为7天"按钮多余
|
| 89 |
+
> 右上角那个刷新为7天的可以去掉,没啥意义
|
| 90 |
+
|
| 91 |
+
- **问题**:哪里来的"刷新为7天"按钮?可能在 `render_nutrition_report_html` 或 Slider 旁边
|
| 92 |
+
- **待确认**:需要再查看报告 HTML 渲染代码
|
| 93 |
+
|
| 94 |
+
### 9. 营养报告 — 布局问题
|
| 95 |
+
> 此外这个页面UI布局也有问题,生成报告按钮占了页面的一半,下面全是空的,右边则是报告挤在一起
|
| 96 |
+
|
| 97 |
+
- **问题**:`gr.Row()` 中 `scale=1`(左侧按钮+Slider)和 `scale=3`��右侧报告)比例不合理
|
| 98 |
+
- **位置**:`ui/app_builder.py` 第 320-332 行
|
| 99 |
+
- **修改**:
|
| 100 |
+
- 左侧:将 Slider 和按钮上下排列,占用更少宽度(scale=1)
|
| 101 |
+
- 右侧:报告占主导(scale=4 或 5)
|
| 102 |
+
- 或者改为上下布局:上方是 Slider,下方是全宽报告
|
| 103 |
+
|
| 104 |
+
### 10. 营养报告 — 自动生成
|
| 105 |
+
> 此外,没有必要"生成报告"这个按钮,每次分析天数被用户拉动的时候,等待用户不拉动之后就生成报告就是了
|
| 106 |
+
|
| 107 |
+
- **问题**:需要点击「生成报告」按钮才能看到报告
|
| 108 |
+
- **修改**:使用 `gr.Slider` 的 `.change()` 事件(带 debounce 防抖)自动触发报告生成,去掉「生成报告」按钮
|
| 109 |
+
|
| 110 |
+
---
|
| 111 |
+
|
| 112 |
+
## 待实施修改清单
|
| 113 |
+
|
| 114 |
+
- [ ] **P0** 写规划 md 文档(本文档)— 防止上下文压缩丢失用户原话
|
| 115 |
+
- [ ] **P1** 第 1 项:修改首页语音提示文案(去技术术语)
|
| 116 |
+
- [ ] **P1** 第 3 项:修复 AI 思考框 UI(去掉 textarea 外壳+灰色背景)
|
| 117 |
+
- [ ] **P1** 第 5 项:家庭饮食习惯默认打开「家庭菜谱」
|
| 118 |
+
- [ ] **P1** 第 9 项 + 第 10 项:营养报告布局重构 + 自动生成
|
| 119 |
+
- [ ] **P2** 第 4 项:顶部标题添加英文 + 趣味字体
|
| 120 |
+
- [ ] **P2** 第 6 项:修复孕期阶段嵌套边框
|
| 121 |
+
- [ ] **P2** 第 7 项:修复 Slider 拖动问题
|
| 122 |
+
- [ ] **P2** 第 8 项:移除多余的"刷新为7天"
|
| 123 |
+
- [ ] **P3** 第 2 项:红色按钮功能设计(需与用户讨论)
|
| 124 |
+
|
| 125 |
+
---
|
| 126 |
+
|
| 127 |
+
## 技术要点
|
| 128 |
+
|
| 129 |
+
### 关于 gradio Slider 防抖自动生成
|
| 130 |
+
```python
|
| 131 |
+
# 使用 gradio 的 change 事件 + 手动防抖
|
| 132 |
+
import time
|
| 133 |
+
|
| 134 |
+
_last_change = [0]
|
| 135 |
+
|
| 136 |
+
def on_slider_change(days):
|
| 137 |
+
_last_change[0] = time.time()
|
| 138 |
+
# 等待 0.8 秒后执行生成
|
| 139 |
+
...
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
### 关于默认选中 Tab
|
| 143 |
+
```python
|
| 144 |
+
with gr.Tabs(selected=0): # 或 selected="🍳 家庭菜谱"
|
| 145 |
+
```
|
| 146 |
+
|
| 147 |
+
### 关于改用 gr.HTML 代替 gr.Textbox 展示思考状态
|
| 148 |
+
- 去掉 textarea 原生外壳
|
| 149 |
+
- 保留渐变背景色由 HTML inline style 控制
|
| 150 |
+
|
| 151 |
+
---
|
| 152 |
+
|
| 153 |
+
## 文件修改范围
|
| 154 |
+
|
| 155 |
+
| 文件 | 修改内容 |
|
| 156 |
+
|------|----------|
|
| 157 |
+
| `docs/UI改进规划_用户反馈.md` | 本文档(新建) |
|
| 158 |
+
| `ui/app_builder.py` | 主要 UI 逻辑修改 |
|
| 159 |
+
| `utils.py` | 文案修改(`_home_texts`、`CUSTOM_CSS`) |
|
ui/app_builder.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
"""
|
| 2 |
PregoPal - Gradio 界面构建(现代UI版)
|
| 3 |
======================================
|
| 4 |
-
|
| 5 |
1. 🏠 首页(语音启动按钮 + AI 思考 + 简报卡片 + 最近记录)
|
| 6 |
2. 👨👩👧👦 家庭饮食习惯(纯展示卡片)
|
| 7 |
-
3.
|
| 8 |
-
4. 📈 营养报告(纯 HTML Dashboard)
|
| 9 |
|
| 10 |
语言切换设计:
|
| 11 |
-
用唯一一个 @gr.render(inputs=[lang_state]) 包裹全部
|
| 12 |
避免多个 @gr.render 并发触发导致 Gradio 内部字典迭代崩溃。
|
| 13 |
"""
|
| 14 |
|
| 15 |
import datetime
|
| 16 |
import gradio as gr
|
| 17 |
import time
|
|
|
|
| 18 |
|
| 19 |
from modules.voiceprint import VoiceprintManager
|
| 20 |
from modules.meal_recommender import MealRecommender
|
|
@@ -23,12 +23,15 @@ from modules.nutrition_analyzer import NutritionAnalyzer
|
|
| 23 |
from modules.family_manager import RecipeManager, PreferenceManager, MemoryManager
|
| 24 |
from modules.diet_extractor import DietExtractor
|
| 25 |
from modules.nutrition_standards import DRIsParser
|
|
|
|
|
|
|
| 26 |
from utils import (
|
| 27 |
t, ZH, EN, get_home_cards, CUSTOM_CSS,
|
| 28 |
render_family_recipes_html,
|
| 29 |
render_family_preferences_html,
|
| 30 |
render_family_memories_html,
|
| 31 |
render_nutrition_report_html,
|
|
|
|
| 32 |
)
|
| 33 |
|
| 34 |
# ============================================================
|
|
@@ -301,94 +304,73 @@ def _family_content(lang):
|
|
| 301 |
|
| 302 |
|
| 303 |
# ============================================================
|
| 304 |
-
# Tab 3:
|
| 305 |
-
# ============================================================
|
| 306 |
-
def _summary_content(lang):
|
| 307 |
-
"""三天总结组件"""
|
| 308 |
-
with gr.Row():
|
| 309 |
-
with gr.Column(scale=1):
|
| 310 |
-
trimester_select = gr.Radio(
|
| 311 |
-
label=t("trimester_label", lang),
|
| 312 |
-
choices=ZH["trimester_choices"] if lang == "zh" else EN["trimester_choices"],
|
| 313 |
-
value=ZH["trimester_choices"][1] if lang == "zh" else EN["trimester_choices"][1],
|
| 314 |
-
)
|
| 315 |
-
run_3day_btn = gr.Button(t("btn_analyze", lang), variant="primary")
|
| 316 |
-
|
| 317 |
-
with gr.Column(scale=2):
|
| 318 |
-
three_day_result = gr.Textbox(label=t("analysis_result", lang), interactive=False, lines=15)
|
| 319 |
-
menu_suggestion = gr.Textbox(label=t("menu_suggestion", lang), interactive=False, lines=4)
|
| 320 |
-
|
| 321 |
-
def run_three_day_summary(trimester):
|
| 322 |
-
from plugins.three_day_summary import ThreeDaySummaryPlugin
|
| 323 |
-
from plugins.base import LoopContext
|
| 324 |
-
import asyncio
|
| 325 |
-
|
| 326 |
-
ctx = LoopContext()
|
| 327 |
-
ctx.briefing["trimester"] = trimester
|
| 328 |
-
asyncio.run(ThreeDaySummaryPlugin().run(ctx))
|
| 329 |
-
|
| 330 |
-
report = ctx.briefing.get("three_day_summary", {}).get("report", "暂无数据")
|
| 331 |
-
deficits = ctx.briefing.get("three_day_summary", {}).get("persistent_deficits", [])
|
| 332 |
-
|
| 333 |
-
suggestions = []
|
| 334 |
-
if deficits:
|
| 335 |
-
suggestions.append("建议增加以下食材到家庭菜单:")
|
| 336 |
-
for n in deficits[:3]:
|
| 337 |
-
foods = DRIsParser.RICH_FOODS.get(n, [])
|
| 338 |
-
if foods:
|
| 339 |
-
suggestions.append(f" * {n} -> {'、'.join(foods[:4])}")
|
| 340 |
-
else:
|
| 341 |
-
suggestions.append("近三天营养状况良好!")
|
| 342 |
-
|
| 343 |
-
return report, "\n".join(suggestions)
|
| 344 |
-
|
| 345 |
-
run_3day_btn.click(
|
| 346 |
-
fn=run_three_day_summary,
|
| 347 |
-
inputs=[trimester_select],
|
| 348 |
-
outputs=[three_day_result, menu_suggestion],
|
| 349 |
-
)
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
# ============================================================
|
| 353 |
-
# Tab 4: 📈 营养报告(纯 HTML Dashboard,自动生成)
|
| 354 |
# ============================================================
|
| 355 |
def _report_content(lang):
|
| 356 |
-
"""营养报告组件 — 纯 HTML 可视化,Slider 防抖自动生成"""
|
| 357 |
# 防抖时间戳
|
| 358 |
_last_change = [0.0]
|
| 359 |
|
| 360 |
with gr.Column():
|
| 361 |
-
#
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 367 |
|
| 368 |
# 报告全宽
|
| 369 |
report_dashboard = gr.HTML(
|
| 370 |
f'<div style="padding:40px;text-align:center;color:#bbb;font-size:16px;">'
|
| 371 |
-
f'调整上方
|
| 372 |
)
|
| 373 |
|
| 374 |
-
def
|
|
|
|
|
|
|
| 375 |
records = diet_logger.get_recent_records(days=int(days))
|
| 376 |
analysis = nutrition_analyzer.analyze_diet(records)
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
| 381 |
_last_change[0] = time.time()
|
| 382 |
-
# wait for debounce window
|
| 383 |
time.sleep(0.8)
|
| 384 |
-
# if another change happened during sleep, skip
|
| 385 |
if time.time() - _last_change[0] < 0.9:
|
| 386 |
-
return
|
| 387 |
return gr.skip()
|
| 388 |
|
|
|
|
| 389 |
analysis_days.change(
|
| 390 |
fn=on_slider_change,
|
| 391 |
-
inputs=[analysis_days],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
outputs=[report_dashboard],
|
| 393 |
)
|
| 394 |
|
|
@@ -399,7 +381,7 @@ def _report_content(lang):
|
|
| 399 |
|
| 400 |
def create_app(loop=None):
|
| 401 |
"""
|
| 402 |
-
创建主应用(
|
| 403 |
"""
|
| 404 |
with gr.Blocks(
|
| 405 |
title="PregoPal - 孕期陪护AI助手",
|
|
@@ -450,12 +432,6 @@ def create_app(loop=None):
|
|
| 450 |
):
|
| 451 |
_family_content(lang)
|
| 452 |
|
| 453 |
-
with gr.Tab(
|
| 454 |
-
f"📊 {t('tab_summary_suffix', lang)}",
|
| 455 |
-
id="tab_summary"
|
| 456 |
-
):
|
| 457 |
-
_summary_content(lang)
|
| 458 |
-
|
| 459 |
with gr.Tab(
|
| 460 |
f"📈 {t('tab_report_suffix', lang)}",
|
| 461 |
id="tab_report"
|
|
|
|
| 1 |
"""
|
| 2 |
PregoPal - Gradio 界面构建(现代UI版)
|
| 3 |
======================================
|
| 4 |
+
3 个子页面 + 中英文切换:
|
| 5 |
1. 🏠 首页(语音启动按钮 + AI 思考 + 简报卡片 + 最近记录)
|
| 6 |
2. 👨👩👧👦 家庭饮食习惯(纯展示卡片)
|
| 7 |
+
3. 营养报告(含三天缺失深度分析,纯 HTML Dashboard)
|
|
|
|
| 8 |
|
| 9 |
语言切换设计:
|
| 10 |
+
用唯一一个 @gr.render(inputs=[lang_state]) 包裹全部 3 个 Tab,
|
| 11 |
避免多个 @gr.render 并发触发导致 Gradio 内部字典迭代崩溃。
|
| 12 |
"""
|
| 13 |
|
| 14 |
import datetime
|
| 15 |
import gradio as gr
|
| 16 |
import time
|
| 17 |
+
import asyncio
|
| 18 |
|
| 19 |
from modules.voiceprint import VoiceprintManager
|
| 20 |
from modules.meal_recommender import MealRecommender
|
|
|
|
| 23 |
from modules.family_manager import RecipeManager, PreferenceManager, MemoryManager
|
| 24 |
from modules.diet_extractor import DietExtractor
|
| 25 |
from modules.nutrition_standards import DRIsParser
|
| 26 |
+
from plugins.three_day_summary import ThreeDaySummaryPlugin
|
| 27 |
+
from plugins.base import LoopContext
|
| 28 |
from utils import (
|
| 29 |
t, ZH, EN, get_home_cards, CUSTOM_CSS,
|
| 30 |
render_family_recipes_html,
|
| 31 |
render_family_preferences_html,
|
| 32 |
render_family_memories_html,
|
| 33 |
render_nutrition_report_html,
|
| 34 |
+
render_three_day_deficit_section,
|
| 35 |
)
|
| 36 |
|
| 37 |
# ============================================================
|
|
|
|
| 304 |
|
| 305 |
|
| 306 |
# ============================================================
|
| 307 |
+
# Tab 3: � 营养报告(含三天缺失深度分析,纯 HTML Dashboard)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 308 |
# ============================================================
|
| 309 |
def _report_content(lang):
|
| 310 |
+
"""营养报告组件 — 纯 HTML 可视化 + 三天缺失分析,Slider 防抖自动生成"""
|
| 311 |
# 防抖时间戳
|
| 312 |
_last_change = [0.0]
|
| 313 |
|
| 314 |
with gr.Column():
|
| 315 |
+
# 控制栏:天数 + 孕期阶段(紧凑行)
|
| 316 |
+
with gr.Row():
|
| 317 |
+
with gr.Column(scale=2, min_width=200):
|
| 318 |
+
analysis_days = gr.Slider(
|
| 319 |
+
label=t("analysis_days", lang),
|
| 320 |
+
minimum=1, maximum=30, value=7, step=1,
|
| 321 |
+
interactive=True,
|
| 322 |
+
)
|
| 323 |
+
with gr.Column(scale=1, min_width=160):
|
| 324 |
+
trimester_select = gr.Radio(
|
| 325 |
+
label=t("trimester_label", lang),
|
| 326 |
+
choices=ZH["trimester_choices"] if lang == "zh" else EN["trimester_choices"],
|
| 327 |
+
value=ZH["trimester_choices"][1] if lang == "zh" else EN["trimester_choices"][1],
|
| 328 |
+
)
|
| 329 |
|
| 330 |
# 报告全宽
|
| 331 |
report_dashboard = gr.HTML(
|
| 332 |
f'<div style="padding:40px;text-align:center;color:#bbb;font-size:16px;">'
|
| 333 |
+
f'调整上方参数自动生成报告</div>'
|
| 334 |
)
|
| 335 |
|
| 336 |
+
def generate_full_report(days, trimester):
|
| 337 |
+
"""生成营养报告 + 三天缺失深度分析,合并为同一份 HTML"""
|
| 338 |
+
# — 基础营养报告 —
|
| 339 |
records = diet_logger.get_recent_records(days=int(days))
|
| 340 |
analysis = nutrition_analyzer.analyze_diet(records)
|
| 341 |
+
main_html = render_nutrition_report_html(analysis, days=int(days), lang=lang)
|
| 342 |
+
|
| 343 |
+
# — 三天缺失深度分析(调用插件) —
|
| 344 |
+
try:
|
| 345 |
+
ctx = LoopContext()
|
| 346 |
+
ctx.briefing["trimester"] = trimester
|
| 347 |
+
asyncio.run(ThreeDaySummaryPlugin().run(ctx))
|
| 348 |
+
deficit_data = ctx.briefing.get("three_day_summary", {})
|
| 349 |
+
except Exception:
|
| 350 |
+
deficit_data = {}
|
| 351 |
+
|
| 352 |
+
# 添加深度分析板块
|
| 353 |
+
three_day_html = render_three_day_deficit_section(deficit_data, lang=lang)
|
| 354 |
|
| 355 |
+
return main_html + three_day_html
|
| 356 |
+
|
| 357 |
+
def on_slider_change(days, trimester):
|
| 358 |
+
"""Slider/Radio 变化时防抖 0.8s 后生成报告"""
|
| 359 |
_last_change[0] = time.time()
|
|
|
|
| 360 |
time.sleep(0.8)
|
|
|
|
| 361 |
if time.time() - _last_change[0] < 0.9:
|
| 362 |
+
return generate_full_report(days, trimester)
|
| 363 |
return gr.skip()
|
| 364 |
|
| 365 |
+
# 两个输入都触发重新生成
|
| 366 |
analysis_days.change(
|
| 367 |
fn=on_slider_change,
|
| 368 |
+
inputs=[analysis_days, trimester_select],
|
| 369 |
+
outputs=[report_dashboard],
|
| 370 |
+
)
|
| 371 |
+
trimester_select.change(
|
| 372 |
+
fn=on_slider_change,
|
| 373 |
+
inputs=[analysis_days, trimester_select],
|
| 374 |
outputs=[report_dashboard],
|
| 375 |
)
|
| 376 |
|
|
|
|
| 381 |
|
| 382 |
def create_app(loop=None):
|
| 383 |
"""
|
| 384 |
+
创建主应用(3 Tab + 中英文切换 + 自定义 CSS)。
|
| 385 |
"""
|
| 386 |
with gr.Blocks(
|
| 387 |
title="PregoPal - 孕期陪护AI助手",
|
|
|
|
| 432 |
):
|
| 433 |
_family_content(lang)
|
| 434 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
with gr.Tab(
|
| 436 |
f"📈 {t('tab_report_suffix', lang)}",
|
| 437 |
id="tab_report"
|