J.B-Lin commited on
Commit
6cce707
·
2 Parent(s): 56b0dc2a4f0e44

merge: 解决合并冲突

Browse files
Files changed (3) hide show
  1. README.md +91 -18
  2. ui/app_builder.py +4 -8
  3. utils.py +83 -80
README.md CHANGED
@@ -39,14 +39,14 @@ PregoPal 是一款面向孕期家庭的 AI 陪护工具,支持:
39
  | 🍽️ 今日菜品推荐(营养+家庭能力) | 基线可用 | `modules/meal_recommender.py` |
40
  | 📝 饮食记录储存(JSON + Markdown) | 已实现 | `modules/diet_logger.py` + `modules/diet_extractor.py` |
41
  | 📊 营养分析与可视化报告 | 基线可用 | `modules/nutrition_analyzer.py` + `modules/nutrition_standards.py` |
 
 
 
 
 
 
42
 
43
- **技术栈**: Python 3.13 · Gradio 6.16 · MiniCPM-o 4.5 · llama-cpp-python (CUDA) · Modal T4 · Matplotlib · Pandas
44
 
45
- **模型部署**: MiniCPM-o-4_5 GGUF (Q4_K_M) on Modal (T4 GPU, $0.50/hr)
46
- - API 端点: `https://andrew-jiabin--prego-pal-minicpm-serve.modal.run`
47
- - 集成方式: `core/model_loader.py` ←HTTP→ Modal API (OpenAI 兼容)
48
- - 模型数据: Modal Volume `minicpm-o-4_5-models`(持久化,仅需上传一次)
49
- - 详见: `modal_deploy/README.md`
50
 
51
  ---
52
 
@@ -54,7 +54,7 @@ PregoPal 是一款面向孕期家庭的 AI 陪护工具,支持:
54
 
55
  ```
56
  app.py ← Gradio 薄入口
57
- ├── ui/app_builder.py ← 前端 Tab 布局(4 Tab: 首页/家庭/总结/报告)
58
  ├── loop.py ← 每日自动分析循环(状态机)
59
  │ ├── plugins/base.py ← 插件基类(LoopPlugin, PluginRegistry, LoopContext)
60
  │ ├── plugins/family_quiz.py ← 家庭问卷插件(菜谱/体重检查)
@@ -63,7 +63,7 @@ app.py ← Gradio 薄入口
63
  │ ├── plugins/family_memory.py← 家庭记忆处理
64
  │ ├── plugins/dri_analysis.py ← DRIs 营养对比分析
65
  │ ├── plugins/briefing_generator.py ← 今日简报生成
66
- │ ├── plugins/three_day_summary.py ← 三天综合总结
67
  │ └── plugins/preset_writer.py ← 预设/缓存写入
68
  ├── modules/ ← 核心业务逻辑(纯函数/无状态)
69
  │ ├── voiceprint.py ← 声纹识别
@@ -79,7 +79,10 @@ app.py ← Gradio 薄入口
79
  │ ├── vision_processor.py ← 视觉处理
80
  │ └── conversation_manager.py ← 对话管理
81
  ├── config.py ← 全局配置(路径/常量/数据模板)
82
- ├── utils.py ← 工具函数(CSS/翻译/HTML渲染/中文字体设置
 
 
 
83
  └── data/ ← 持久化存储
84
  ├── diet_logs.json ← 结构化饮食记录 JSON
85
  ├── nutrition_db.json ← 营养数据库 JSON
@@ -430,7 +433,7 @@ class LoopPlugin(ABC):
430
  | `FamilyMemoryPlugin` | `SUMMARIZE` | `plugins/family_memory.py` | 提取家庭记忆 |
431
  | `DRIAnalysisPlugin` | `ANALYZE` | `plugins/dri_analysis.py` | DRIs 营养对比 |
432
  | `BriefingGeneratorPlugin` | `BRIEF` | `plugins/briefing_generator.py` | 汇总生成今日简报 |
433
- | `ThreeDaySummaryPlugin` | `THREE_DAY` | `plugins/three_day_summary.py` | 三天综合总结 |
434
  | `PresetWriterPlugin` | `CONSOLIDATE` | `plugins/preset_writer.py` | 写入预设/缓存 |
435
 
436
  **插件向 `ctx.briefing` 写入的 key**(`BriefingGeneratorPlugin` 最终消费):
@@ -446,6 +449,7 @@ ctx.briefing["dri_analysis"] # dict {"focus_nutrients","summary"}
446
  ctx.briefing["recommended_foods"] # list[str]
447
  ctx.briefing["family_memory"] # dict
448
  ctx.briefing["thinking_keywords"] # str
 
449
  ```
450
 
451
  ---
@@ -486,11 +490,15 @@ ctx.briefing["thinking_keywords"] # str
486
 
487
  ```
488
  Cline A: Modules 强化(营养分析对接 DRIs、菜谱推荐 AI 化)
489
- Cline B: UI 界面优化(Gradio 前端增强、报告模板美化) ← 当前 Cline
490
  Cline C: 声纹升级(Whisper encoder 替换频谱特征)
491
  Cline D: 数据处理与插件增强(diet_extractor fallback、新插件)
 
492
  ```
493
 
 
 
 
494
  ### 7.2 关键约定
495
 
496
  1. **改接口前先 grep**:用 `search_files` 搜索方法名找到所有调用方
@@ -545,28 +553,37 @@ chore: 配置/依赖/路径调整
545
 
546
  ### 8.1 页面结构
547
 
548
- 4 Tab 布局,使用 **唯一一个 `@gr.render`** 包裹全部 Tab(避免 Gradio 内部字典迭代崩溃):
549
 
550
  | Tab | ID | 功能 |
551
  |-----|----|------|
552
  | 🏠 首页 | `tab_home` | 语音按钮 + AI 思考 + 信息卡片 + 最近记录 |
553
  | 👨‍👩‍👧‍👦 家庭 | `tab_family` | 家庭菜谱/偏好/记事 HTML 卡片展示 |
554
- | 📊 总结 | `tab_summary` | 三天营养分析 + 菜单建议 |
555
- | 📈 报告 | `tab_report` | 纯 HTML 营养仪表盘(无需 matplotlib) |
 
 
 
 
556
 
557
  ### 8.2 设计风格
558
 
559
  - **Glassmorphism 毛玻璃**:`backdrop-filter: blur(20px)` 半透明卡片
560
  - **粉色暖调主题**:主色 `#E91E63`、渐变 `#FF6B9D → #FFD6E0`
561
- - **纯 HTML 渲染**:家庭信息卡、营养报告均使用 HTML + CSS,无需生成图片
562
  - **中英文切换**:`lang_state` 驱动唯一 `@gr.render`
 
 
 
 
563
 
564
  ### 8.3 关键文件
565
 
566
  | 文件 | 职责 |
567
  |------|------|
568
- | `ui/app_builder.py` | Gradio 界面构建(4 Tab + 语言切换) |
569
- | `utils.py` | CSS 定义 + 翻译字典 + HTML 渲染函数(家庭卡片 + 营养仪表盘) |
 
570
 
571
  ---
572
 
@@ -594,8 +611,64 @@ Model to be Used: MiniCPM-o 4.5
594
 
595
  核心能力层(`core/` 目录)提供模型加载、语音处理、视觉处理、对话管理的底层能力封装。详见各文件 docstring。
596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  ---
598
 
599
- *最后更新: 2026-06-09 | 由 PregoPal Cline 团队维护*
600
 
601
  > **⚠️ 重要提醒**:本 README 是所有并行 Cline 的协作基础,任�� Cline 在修改代码后,若涉及接口、数据格式、配置的变更,必须同步更新本文件。不得随意简化、删除结构性内容。
 
39
  | 🍽️ 今日菜品推荐(营养+家庭能力) | 基线可用 | `modules/meal_recommender.py` |
40
  | 📝 饮食记录储存(JSON + Markdown) | 已实现 | `modules/diet_logger.py` + `modules/diet_extractor.py` |
41
  | 📊 营养分析与可视化报告 | 基线可用 | `modules/nutrition_analyzer.py` + `modules/nutrition_standards.py` |
42
+ | 🤖 多模态大模型后端(MiniCPM-o 4.5) | ✅ 已部署 Modal | `modal_deploy/deploy.py` + `modal_deploy/client.py` |
43
+
44
+ **技术栈**: Python 3.13 · Gradio 6.16 · MiniCPM-o 4.5 · Modal · Matplotlib · Pandas
45
+
46
+ **大模型部署状态**:✅ 已完成,详见 [第11节](#11-多模态大模型部署-modal)
47
+
48
 
 
49
 
 
 
 
 
 
50
 
51
  ---
52
 
 
54
 
55
  ```
56
  app.py ← Gradio 薄入口
57
+ ├── ui/app_builder.py ← 前端 Tab 布局(3 Tab: 首页/家庭/营养报告)
58
  ├── loop.py ← 每日自动分析循环(状态机)
59
  │ ├── plugins/base.py ← 插件基类(LoopPlugin, PluginRegistry, LoopContext)
60
  │ ├── plugins/family_quiz.py ← 家庭问卷插件(菜谱/体重检查)
 
63
  │ ├── plugins/family_memory.py← 家庭记忆处理
64
  │ ├── plugins/dri_analysis.py ← DRIs 营养对比分析
65
  │ ├── plugins/briefing_generator.py ← 今日简报生成
66
+ │ ├── plugins/three_day_summary.py ← 三天深度缺失分析(数据由营养报告Tab调用)
67
  │ └── plugins/preset_writer.py ← 预设/缓存写入
68
  ├── modules/ ← 核心业务逻辑(纯函数/无状态)
69
  │ ├── voiceprint.py ← 声纹识别
 
79
  │ ├── vision_processor.py ← 视觉处理
80
  │ └── conversation_manager.py ← 对话管理
81
  ├── config.py ← 全局配置(路径/常量/数据模板)
82
+ ├── utils.py ← 工具函数(CSS/翻译/HTML渲染/营养报告+三天缺失分析
83
+ ├── modal_deploy/ ← Modal 部署层
84
+ │ ├── deploy.py ← Modal ASGI 部署入口(llama-server → FastAPI)
85
+ │ └── client.py ← Python API 客户端(chat / chat_with_image / embed)
86
  └── data/ ← 持久化存储
87
  ├── diet_logs.json ← 结构化饮食记录 JSON
88
  ├── nutrition_db.json ← 营养数据库 JSON
 
433
  | `FamilyMemoryPlugin` | `SUMMARIZE` | `plugins/family_memory.py` | 提取家庭记忆 |
434
  | `DRIAnalysisPlugin` | `ANALYZE` | `plugins/dri_analysis.py` | DRIs 营养对比 |
435
  | `BriefingGeneratorPlugin` | `BRIEF` | `plugins/briefing_generator.py` | 汇总生成今日简报 |
436
+ | `ThreeDaySummaryPlugin` | `THREE_DAY` | `plugins/three_day_summary.py` | 三天深度缺失分析(被营养报告Tab调用) |
437
  | `PresetWriterPlugin` | `CONSOLIDATE` | `plugins/preset_writer.py` | 写入预设/缓存 |
438
 
439
  **插件向 `ctx.briefing` 写入的 key**(`BriefingGeneratorPlugin` 最终消费):
 
449
  ctx.briefing["recommended_foods"] # list[str]
450
  ctx.briefing["family_memory"] # dict
451
  ctx.briefing["thinking_keywords"] # str
452
+ ctx.briefing["three_day_summary"] # dict(由报告Tab选择性地读取)
453
  ```
454
 
455
  ---
 
490
 
491
  ```
492
  Cline A: Modules 强化(营养分析对接 DRIs、菜谱推荐 AI 化)
493
+ Cline B: UI 界面优化(Gradio 前端增强、报告模板美化) ← 当前 Cline
494
  Cline C: 声纹升级(Whisper encoder 替换频谱特征)
495
  Cline D: 数据处理与插件增强(diet_extractor fallback、新插件)
496
+ Cline E: 大模型集成(对接 Modal API,实现全双工语音对话) ← 新增(部署已完成)
497
  ```
498
 
499
+ > 注:MiniCPM-o 4.5 已在 Modal 完成部署,API 客户端 `modal_deploy/client.py` 已就绪。
500
+ > 接下来可并行推进:A) 前端接入大模型 B) 营养分析对接 DRIs C) 声纹升级。
501
+
502
  ### 7.2 关键约定
503
 
504
  1. **改接口前先 grep**:用 `search_files` 搜索方法名找到所有调用方
 
553
 
554
  ### 8.1 页面结构
555
 
556
+ 3 Tab 布局,使用 **唯一一个 `@gr.render`** 包裹全部 Tab(避免 Gradio 内部字典迭代崩溃):
557
 
558
  | Tab | ID | 功能 |
559
  |-----|----|------|
560
  | 🏠 首页 | `tab_home` | 语音按钮 + AI 思考 + 信息卡片 + 最近记录 |
561
  | 👨‍👩‍👧‍👦 家庭 | `tab_family` | 家庭菜谱/偏好/记事 HTML 卡片展示 |
562
+ | 📈 营养报告 | `tab_report` | 纯 HTML 营养仪表盘 + 三天缺失深度分析(已合并) |
563
+
564
+ > **架构变更说明**(v2.0):
565
+ > - 原「三天总结」Tab **已合并到营养报告 Tab** 中,作为一个统一下沉展示区块
566
+ > - 营养报告使用 Slider 防抖自动生成(0.8s),包含基础营养评分 + 三天缺失深度分析
567
+ > - 三天缺失分析页面使用与营养报告一致的设计语言(渐变 Header + 紧凑标签 + 结构化 Summary)
568
 
569
  ### 8.2 设计风格
570
 
571
  - **Glassmorphism 毛玻璃**:`backdrop-filter: blur(20px)` 半透明卡片
572
  - **粉色暖调主题**:主色 `#E91E63`、渐变 `#FF6B9D → #FFD6E0`
573
+ - **纯 HTML 渲染**:家庭信息卡、营养报告、三天缺失分析均使用 HTML + CSS,无需生成图片
574
  - **中英文切换**:`lang_state` 驱动唯一 `@gr.render`
575
+ - **三天缺失分析**:
576
+ - 持续不足营养素采用**紧凑标签式**显示(pill 标签,一行多个)
577
+ - 食材补充建议区域采用 **1:2 扩大布局**(右侧占 2/3 宽度,每行 6 个食物标签)
578
+ - 总结部分采用**结构化 UI 卡片**(红色/橙色/绿色 左侧边框,按严重程度分类显示)
579
 
580
  ### 8.3 关键文件
581
 
582
  | 文件 | 职责 |
583
  |------|------|
584
+ | `ui/app_builder.py` | Gradio 界面构建(3 Tab + 语言切换 + 报告自动生成) |
585
+ | `utils.py` | CSS 定义 + 翻译字典 + HTML 渲染函数(家庭卡片 + 营养仪表盘 + 三天缺失分析) |
586
+ | `plugins/three_day_summary.py` | 三天缺失分析插件(数据层,被报告 Tab 异步调用) |
587
 
588
  ---
589
 
 
611
 
612
  核心能力层(`core/` 目录)提供模型加载、语音处理、视觉处理、对话管理的底层能力封装。详见各文件 docstring。
613
 
614
+ **部署状态**: ✅ 已完成 Modal 部署(见第11节)
615
+
616
+ ---
617
+
618
+ ## 11. 多模态大模型部署 (Modal)
619
+
620
+ ### 11.1 部署状态
621
+
622
+ | 项目 | 状态 |
623
+ |------|------|
624
+ | API URL | `https://andrew-jiabin--prego-pal-minicpm-serve.modal.run` |
625
+ | Modal App | `prego-pal-minicpm` |
626
+ | GPU | T4 (16GB) |
627
+ | 模型 | MiniCPM-o 4.5 Q4_K_M (~12GB) + vision/audio/TTS 投影层 |
628
+ | 冷启动 | ~2-8 分钟(首次请求触发容器启动 + 模型加载) |
629
+ | 闲置缩容 | 300s 无请求自动缩容 |
630
+ | 并发 | `max_inputs=10` |
631
+
632
+ ### 11.2 API 端点
633
+
634
+ | 端点 | 方法 | 功能 |
635
+ |------|------|------|
636
+ | `/v1/chat/completions` | POST | OpenAI 兼容聊天(文本) |
637
+ | `/v1/embeddings` | POST | 文本嵌入 |
638
+ | `/v1/models` | GET | 模型列表 |
639
+ | `/v1/multimodal/chat` | POST | 多模态(图片+文本) |
640
+ | `/health` | GET | 健康检查 |
641
+ | `/` | GET | 服务信息 |
642
+
643
+ ### 11.3 Python 客户端
644
+
645
+ ```python
646
+ from modal_deploy.client import MiniCPMClient
647
+
648
+ client = MiniCPMClient(base_url="https://andrew-jiabin--prego-pal-minicpm-serve.modal.run")
649
+
650
+ # 文本对话
651
+ response = client.ask("今天孕妇可以吃什么?")
652
+
653
+ # 多模态(图片理解)
654
+ response = client.describe_image(image_base64_str)
655
+
656
+ # 嵌入向量
657
+ embeddings = client.embed(["孕期饮食建议", "番茄牛腩"])
658
+ ```
659
+
660
+ 详见 `modal_deploy/client.py` 完整文档。
661
+
662
+ ### 11.4 部署文件
663
+
664
+ | 文件 | 说明 |
665
+ |------|------|
666
+ | `modal_deploy/deploy.py` | Modal ASGI 部署入口(llama-server → FastAPI 包装) |
667
+ | `modal_deploy/client.py` | Python API 客户端(chat / chat_with_image / embed) |
668
+ | `docs/部署经验_Modal_MiniCPM-o.md` | 详细部署经验文档(踩坑记录、成本估算、后续优化) |
669
+
670
  ---
671
 
672
+ *最后更新: 2026-06-10 | 由 PregoPal Cline 团队维护*
673
 
674
  > **⚠️ 重要提醒**:本 README 是所有并行 Cline 的协作基础,任�� Cline 在修改代码后,若涉及接口、数据格式、配置的变更,必须同步更新本文件。不得随意简化、删除结构性内容。
ui/app_builder.py CHANGED
@@ -31,7 +31,6 @@ from utils import (
31
  render_family_preferences_html,
32
  render_family_memories_html,
33
  render_nutrition_report_html,
34
- render_three_day_deficit_section,
35
  )
36
 
37
  # ============================================================
@@ -334,13 +333,12 @@ def _report_content(lang):
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
@@ -349,10 +347,8 @@ def _report_content(lang):
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 后生成报告"""
 
31
  render_family_preferences_html,
32
  render_family_memories_html,
33
  render_nutrition_report_html,
 
34
  )
35
 
36
  # ============================================================
 
333
  )
334
 
335
  def generate_full_report(days, trimester):
336
+ """生成营养报告(含三天缺失深度分析,框架层面合并"""
337
  # — 基础营养报告 —
338
  records = diet_logger.get_recent_records(days=int(days))
339
  analysis = nutrition_analyzer.analyze_diet(records)
 
340
 
341
+ # — 三天缺失深度分析(调用插件,数据传入 render_nutrition_report_html 统一渲染) —
342
  try:
343
  ctx = LoopContext()
344
  ctx.briefing["trimester"] = trimester
 
347
  except Exception:
348
  deficit_data = {}
349
 
350
+ # 所有内容由 render_nutrition_report_html 统一渲染(框架层面合并)
351
+ return render_nutrition_report_html(analysis, days=int(days), lang=lang, deficit_data=deficit_data)
 
 
352
 
353
  def on_slider_change(days, trimester):
354
  """Slider/Radio 变化时防抖 0.8s 后生成报告"""
utils.py CHANGED
@@ -57,7 +57,6 @@ ZH = {
57
  "app_subtitle": "一个温馨的家庭式孕期AI伴侣",
58
  "tab_home_suffix": "首页",
59
  "tab_family_suffix": "家庭饮食习惯",
60
- "tab_summary_suffix": "三天总结",
61
  "tab_report_suffix": "营养报告",
62
  "chat_label": "💬 对话",
63
  "msg_placeholder": "例如:我今天中午吃了番茄牛腩",
@@ -102,20 +101,15 @@ ZH = {
102
  "report_diversity": "饮食多样性",
103
  "report_suggestions": "饮食建议",
104
  "report_days": "近{days}天",
105
- "three_day_title": "📊 三天深度营养分析",
106
- "three_day_subtitle": "基于近日饮食趋势的持续缺失追踪",
 
107
  "three_day_days": "分析了 {days} 天数据",
108
  "three_day_duration": "持续不足的营养素",
109
- "three_day_deficit": "缺失风险",
110
- "three_day_attention": "需关注",
111
- "three_day_good": "摄入达标",
112
  "three_day_suggestions": "🍽️ 食材补充建议",
113
- "three_day_summary": "📋 总结",
114
  "three_day_no_data": "近三天无饮食记录,跳过深度分析",
115
  "three_day_all_good": "✅ 各营养素摄入基本达标,继续保持!",
116
- "three_day_def_critical": "❌",
117
- "three_day_def_warning": "⚠️",
118
- "three_day_def_ok": "✅",
119
  }
120
 
121
  EN = {
@@ -123,7 +117,6 @@ EN = {
123
  "app_subtitle": "A cozy home-oriented AI companion for expectant mothers",
124
  "tab_home_suffix": "Home",
125
  "tab_family_suffix": "Family Diet",
126
- "tab_summary_suffix": "3-Day Summary",
127
  "tab_report_suffix": "Nutrition Report",
128
  "chat_label": "💬 Chat",
129
  "msg_placeholder": "e.g. I had tomato beef brisket for lunch today",
@@ -168,20 +161,15 @@ EN = {
168
  "report_diversity": "Diet Diversity",
169
  "report_suggestions": "Dietary Suggestions",
170
  "report_days": "Last {days} days",
171
- "three_day_title": "📊 3-Day Deep Analysis",
172
- "three_day_subtitle": "Continuous deficiency tracking based on recent diet trends",
 
173
  "three_day_days": "Analyzed {days} days of data",
174
  "three_day_duration": "Persistent Deficits",
175
- "three_day_deficit": "Deficit Risk",
176
- "three_day_attention": "Needs Attention",
177
- "three_day_good": "Adequate Intake",
178
  "three_day_suggestions": "🍽️ Food Suggestions",
179
- "three_day_summary": "📋 Summary",
180
  "three_day_no_data": "No diet records in the last 3 days, skipping deep analysis",
181
  "three_day_all_good": "✅ All nutrients adequate, keep it up!",
182
- "three_day_def_critical": "❌",
183
- "three_day_def_warning": "⚠️",
184
- "three_day_def_ok": "✅",
185
  }
186
 
187
 
@@ -335,11 +323,17 @@ def render_family_memories_html(lang="zh") -> str:
335
 
336
 
337
  # ============================================================
338
- # 营养报告 HTML
 
 
 
 
 
 
339
  # ============================================================
340
 
341
- def render_nutrition_report_html(analysis: dict, days: int, lang="zh") -> str:
342
- """渲染营养报告为现代 Dashboard HTML"""
343
  if not analysis or "error" in analysis:
344
  return f'<div style="padding:40px;text-align:center;color:#aaa;font-size:16px;">{analysis.get("error","暂无数据")}</div>'
345
 
@@ -405,6 +399,11 @@ def render_nutrition_report_html(analysis: dict, days: int, lang="zh") -> str:
405
 
406
  days_str = t("report_days", lang).format(days=days)
407
 
 
 
 
 
 
408
  return f"""
409
  <div style="font-family:system-ui,-apple-system,sans-serif;max-width:100%;">
410
  <!-- Header -->
@@ -459,49 +458,14 @@ def render_nutrition_report_html(analysis: dict, days: int, lang="zh") -> str:
459
  <div style="font-weight:600;font-size:14px;margin-bottom:6px;color:#E65100;">💡 {t("report_suggestions", lang)}</div>
460
  {sug_items if sug_items else '<div style="font-size:13px;color:#888;">暂无建议</div>'}
461
  </div>
462
- </div>"""
463
 
 
 
464
 
465
- # ============================================================
466
- # 营养素中英文对照表(用于三天总结双语展示)
467
- # ============================================================
468
-
469
- NUTRIENT_EN_NAMES = {
470
- "能量(MJ)": "Energy",
471
- "蛋白质(g)": "Protein",
472
- "钙(mg)": "Calcium",
473
- "铁(mg)": "Iron",
474
- "锌(mg)": "Zinc",
475
- "维生素A(μg RAE)": "Vitamin A",
476
- "维生素D(μg)": "Vitamin D",
477
- "维生素E(mg α-TE)": "Vitamin E",
478
- "维生素B1(mg)": "Vitamin B1",
479
- "维生素B2(mg)": "Vitamin B2",
480
- "叶酸(μg DFE)": "Folate",
481
- "碘(μg)": "Iodine",
482
- "镁(mg)": "Magnesium",
483
- "硒(μg)": "Selenium",
484
- "钾(mg)": "Potassium",
485
- "钠(mg)": "Sodium",
486
- "磷(mg)": "Phosphorus",
487
- "维生素C(mg)": "Vitamin C",
488
- "膳食纤维(g)": "Dietary Fiber",
489
- }
490
-
491
-
492
- def _nutrient_display(name_cn: str, lang: str) -> str:
493
- """返回营养素名的展示文本(中文模式显示中文+英文,英文模式显示英文)"""
494
- en_name = NUTRIENT_EN_NAMES.get(name_cn, name_cn)
495
- if lang == "zh":
496
- return f"{name_cn} / {en_name}"
497
- return en_name
498
-
499
 
500
- # ============================================================
501
- # 三天缺失深度分析 HTML(与营养报告统一视觉风格)
502
- # ============================================================
503
 
504
- def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
505
  """
506
  渲染三天缺失深度分析板块,与营养报告使用同一套现代UI视觉语言。
507
 
@@ -513,7 +477,7 @@ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
513
  lang: 语言标识 "zh" / "en"
514
 
515
  Returns:
516
- str: 纯 HTML 字符串,可直接拼接到营养报告之后
517
  """
518
  if not deficit_data:
519
  return ""
@@ -532,7 +496,7 @@ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
532
  {no_data_msg}
533
  </div>"""
534
 
535
- # ——— Persistent Deficits: 紧凑标签式展示(取代大卡片) ———
536
  deficit_tags = ""
537
  for nutrient in persistent_deficits:
538
  nd = _nutrient_display(nutrient, lang)
@@ -554,7 +518,7 @@ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
554
  🎉 {all_good}
555
  </span>"""
556
 
557
- # ——— Food Suggestions: 扩大展示(使用2/3宽度,每行2个食物标签) ———
558
  from modules.nutrition_standards import DRIsParser
559
  suggestion_rows = ""
560
  for nutrient in persistent_deficits[:3]:
@@ -583,11 +547,10 @@ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
583
  🎉 {t("three_day_all_good", lang)}
584
  </div>"""
585
 
586
- # ——— Summary: 覆盖折叠纯文本,解析后用结构化UI展示 ———
587
  summary_sections_html = ""
588
  if report_text:
589
  lines = report_text.strip().split("\n")
590
- # 分类行
591
  critical_items = []
592
  warning_items = []
593
  ok_items = []
@@ -662,31 +625,30 @@ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
662
 
663
  days_label = t("three_day_days", lang).format(days=days_analyzed)
664
 
665
- # ——— 拼装最终 HTML ———
666
- # 布局: 左右比例 1:2 (deficit : suggestions)
667
- # summary 在下方单独一行,结构化展示
668
  return f"""
669
- <div style="margin-top:20px;font-family:system-ui,-apple-system,sans-serif;max-width:100%;">
 
670
 
671
- <!-- ★ Header: 与营养报告统一的渐变风格 -->
672
- <div style="background:linear-gradient(135deg,#E8F5E9,#F1F8E9);border-radius:18px;padding:20px 24px;margin-bottom:16px;">
673
  <div style="display:flex;justify-content:space-between;align-items:center;">
674
  <div>
675
- <div style="font-size:20px;font-weight:700;color:#1B5E20;">{t("three_day_title", lang)}</div>
676
  <div style="color:#888;font-size:13px;margin-top:4px;">{t("three_day_subtitle", lang)}</div>
677
  </div>
678
  <div style="text-align:right;">
679
- <div style="font-size:32px;font-weight:800;color:#4CAF50;">{days_analyzed}</div>
680
- <div style="font-size:12px;color:#888;">天</div>
681
  </div>
682
  </div>
683
  <div style="margin-top:8px;font-size:13px;color:#555;">{days_label}</div>
684
  </div>
685
 
686
- <!-- 双列布局 1:2 → 左侧:紧凑标签式缺失营养素 | 右侧:放大的食材建议 -->
687
  <div style="display:grid;grid-template-columns:1fr 2fr;gap:12px;margin-bottom:16px;">
688
 
689
- <!-- 左侧:持续不足的营养素 — 紧凑标签风格 -->
690
  <div style="background:#fff;border-radius:14px;padding:16px;
691
  box-shadow:0 1px 6px rgba(0,0,0,0.04);border:1px solid #f0f0f0;">
692
  <div style="font-weight:600;font-size:14px;margin-bottom:12px;color:#333;">
@@ -697,7 +659,7 @@ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
697
  </div>
698
  </div>
699
 
700
- <!-- 右侧:食材补充建议 — 放大展示 -->
701
  <div style="background:linear-gradient(135deg,#FFF8E1,#FFFDE7);border-radius:14px;padding:16px 20px;
702
  border:1px solid #FFE082;">
703
  <div style="font-weight:600;font-size:15px;margin-bottom:12px;color:#E65100;">
@@ -708,7 +670,7 @@ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
708
 
709
  </div>
710
 
711
- <!-- 总结部分:结构化UI展示(替代纯文本折叠) -->
712
  {f'''
713
  <div style="background:#fff;border-radius:14px;padding:16px 20px;
714
  box-shadow:0 1px 6px rgba(0,0,0,0.04);border:1px solid #f0f0f0;">
@@ -722,6 +684,47 @@ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
722
  </div>"""
723
 
724
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
725
  # ============================================================
726
  # 自定义 CSS 样式
727
  # ============================================================
 
57
  "app_subtitle": "一个温馨的家庭式孕期AI伴侣",
58
  "tab_home_suffix": "首页",
59
  "tab_family_suffix": "家庭饮食习惯",
 
60
  "tab_report_suffix": "营养报告",
61
  "chat_label": "💬 对话",
62
  "msg_placeholder": "例如:我今天中午吃了番茄牛腩",
 
101
  "report_diversity": "饮食多样性",
102
  "report_suggestions": "饮食建议",
103
  "report_days": "近{days}天",
104
+ # 三天深度分析(已合并到营养报告中)
105
+ "three_day_title": "📊 天深度营养趋势",
106
+ "three_day_subtitle": "基于近三日饮食数据的持续缺失追踪",
107
  "three_day_days": "分析了 {days} 天数据",
108
  "three_day_duration": "持续不足的营养素",
 
 
 
109
  "three_day_suggestions": "🍽️ 食材补充建议",
110
+ "three_day_summary": "📋 趋势总结",
111
  "three_day_no_data": "近三天无饮食记录,跳过深度分析",
112
  "three_day_all_good": "✅ 各营养素摄入基本达标,继续保持!",
 
 
 
113
  }
114
 
115
  EN = {
 
117
  "app_subtitle": "A cozy home-oriented AI companion for expectant mothers",
118
  "tab_home_suffix": "Home",
119
  "tab_family_suffix": "Family Diet",
 
120
  "tab_report_suffix": "Nutrition Report",
121
  "chat_label": "💬 Chat",
122
  "msg_placeholder": "e.g. I had tomato beef brisket for lunch today",
 
161
  "report_diversity": "Diet Diversity",
162
  "report_suggestions": "Dietary Suggestions",
163
  "report_days": "Last {days} days",
164
+ # 三天深度分析(合并到营养报告中)
165
+ "three_day_title": "📊 3-Day Nutrient Trends",
166
+ "three_day_subtitle": "Continuous deficiency tracking based on recent diet data",
167
  "three_day_days": "Analyzed {days} days of data",
168
  "three_day_duration": "Persistent Deficits",
 
 
 
169
  "three_day_suggestions": "🍽️ Food Suggestions",
170
+ "three_day_summary": "📋 Trend Summary",
171
  "three_day_no_data": "No diet records in the last 3 days, skipping deep analysis",
172
  "three_day_all_good": "✅ All nutrients adequate, keep it up!",
 
 
 
173
  }
174
 
175
 
 
323
 
324
 
325
  # ============================================================
326
+ # 营养报告 HTML(含三天深度分析)
327
+ # ============================================================
328
+ # 统一的设计令牌(Design Tokens)
329
+ # Header: 粉色渐变 background:linear-gradient(135deg,#FCE4EC,#FFF0F2)
330
+ # 卡片: background:#fff; border-radius:14px; box-shadow + border
331
+ # 建议: background:linear-gradient(135deg,#FFF8E1,#FFFDE7); border:1px solid #FFE082
332
+ # 强调色: #880E4F (深粉红), #E91E63 (粉红), #E65100 (橙)
333
  # ============================================================
334
 
335
+ def render_nutrition_report_html(analysis: dict, days: int, lang="zh", deficit_data: dict = None) -> str:
336
+ """渲染营养报告为现代 Dashboard HTML(含三天深度趋势分析)"""
337
  if not analysis or "error" in analysis:
338
  return f'<div style="padding:40px;text-align:center;color:#aaa;font-size:16px;">{analysis.get("error","暂无数据")}</div>'
339
 
 
399
 
400
  days_str = t("report_days", lang).format(days=days)
401
 
402
+ # ============================================================
403
+ # 三天深度趋势分析(合并到营养报告内)
404
+ # ============================================================
405
+ three_day_html = _render_deficit_section(deficit_data, lang)
406
+
407
  return f"""
408
  <div style="font-family:system-ui,-apple-system,sans-serif;max-width:100%;">
409
  <!-- Header -->
 
458
  <div style="font-weight:600;font-size:14px;margin-bottom:6px;color:#E65100;">💡 {t("report_suggestions", lang)}</div>
459
  {sug_items if sug_items else '<div style="font-size:13px;color:#888;">暂无建议</div>'}
460
  </div>
 
461
 
462
+ <!-- ★ 三天深度趋势分析 — 使用与营养报告统一的UI设计 -->
463
+ {three_day_html}
464
 
465
+ </div>"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
 
 
 
467
 
468
+ def _render_deficit_section(deficit_data: dict, lang="zh") -> str:
469
  """
470
  渲染三天缺失深度分析板块,与营养报告使用同一套现代UI视觉语言。
471
 
 
477
  lang: 语言标识 "zh" / "en"
478
 
479
  Returns:
480
+ str: 纯 HTML 字符串,使用与营养报告统一的风格
481
  """
482
  if not deficit_data:
483
  return ""
 
496
  {no_data_msg}
497
  </div>"""
498
 
499
+ # ——— Persistent Deficits: 紧凑标签式展示 ———
500
  deficit_tags = ""
501
  for nutrient in persistent_deficits:
502
  nd = _nutrient_display(nutrient, lang)
 
518
  🎉 {all_good}
519
  </span>"""
520
 
521
+ # ——— Food Suggestions ———
522
  from modules.nutrition_standards import DRIsParser
523
  suggestion_rows = ""
524
  for nutrient in persistent_deficits[:3]:
 
547
  🎉 {t("three_day_all_good", lang)}
548
  </div>"""
549
 
550
+ # ——— Summary: 结构化UI展示 ———
551
  summary_sections_html = ""
552
  if report_text:
553
  lines = report_text.strip().split("\n")
 
554
  critical_items = []
555
  warning_items = []
556
  ok_items = []
 
625
 
626
  days_label = t("three_day_days", lang).format(days=days_analyzed)
627
 
628
+ # ——— 拼装最终 HTML(使用与营养报告统一的设计语言) ———
 
 
629
  return f"""
630
+ <!-- 三天深度趋势分析 — 使用与营养报告一致的粉色渐变主题 -->
631
+ <div style="margin-top:20px;">
632
 
633
+ <!-- 头部:与营养报告Header统一的粉色渐变 -->
634
+ <div style="background:linear-gradient(135deg,#FCE4EC,#FFF0F2);border-radius:18px;padding:20px 24px;margin-bottom:16px;">
635
  <div style="display:flex;justify-content:space-between;align-items:center;">
636
  <div>
637
+ <div style="font-size:20px;font-weight:700;color:#880E4F;">{t("three_day_title", lang)}</div>
638
  <div style="color:#888;font-size:13px;margin-top:4px;">{t("three_day_subtitle", lang)}</div>
639
  </div>
640
  <div style="text-align:right;">
641
+ <div style="font-size:32px;font-weight:800;color:#E91E63;">{days_analyzed}</div>
642
+ <div style="font-size:12px;color:#888;">{'' if lang == 'zh' else 'days'}</div>
643
  </div>
644
  </div>
645
  <div style="margin-top:8px;font-size:13px;color:#555;">{days_label}</div>
646
  </div>
647
 
648
+ <!-- 双列布局 1:2 → 左侧:缺失营养素标签 | 右侧:食材建议 -->
649
  <div style="display:grid;grid-template-columns:1fr 2fr;gap:12px;margin-bottom:16px;">
650
 
651
+ <!-- 左侧:持续不足的营养素 — 与报告一致的白色卡片 -->
652
  <div style="background:#fff;border-radius:14px;padding:16px;
653
  box-shadow:0 1px 6px rgba(0,0,0,0.04);border:1px solid #f0f0f0;">
654
  <div style="font-weight:600;font-size:14px;margin-bottom:12px;color:#333;">
 
659
  </div>
660
  </div>
661
 
662
+ <!-- 右侧:食材补充建议 — 与报告建议区一致的金色渐变 -->
663
  <div style="background:linear-gradient(135deg,#FFF8E1,#FFFDE7);border-radius:14px;padding:16px 20px;
664
  border:1px solid #FFE082;">
665
  <div style="font-weight:600;font-size:15px;margin-bottom:12px;color:#E65100;">
 
670
 
671
  </div>
672
 
673
+ <!-- 总结部分:结构化UI展示 -->
674
  {f'''
675
  <div style="background:#fff;border-radius:14px;padding:16px 20px;
676
  box-shadow:0 1px 6px rgba(0,0,0,0.04);border:1px solid #f0f0f0;">
 
684
  </div>"""
685
 
686
 
687
+ # 保留旧函数名作为代理(兼容外部调用),标记为 deprecated
688
+ def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str:
689
+ """已弃用:三天分析已合并到 render_nutrition_report_html 中。保留此函数保持兼容。"""
690
+ return _render_deficit_section(deficit_data, lang)
691
+
692
+
693
+ # ============================================================
694
+ # 营养素中英文对照表
695
+ # ============================================================
696
+
697
+ NUTRIENT_EN_NAMES = {
698
+ "能量(MJ)": "Energy",
699
+ "蛋白质(g)": "Protein",
700
+ "钙(mg)": "Calcium",
701
+ "铁(mg)": "Iron",
702
+ "锌(mg)": "Zinc",
703
+ "维生素A(μg RAE)": "Vitamin A",
704
+ "维生素D(μg)": "Vitamin D",
705
+ "维生素E(mg α-TE)": "Vitamin E",
706
+ "维生素B1(mg)": "Vitamin B1",
707
+ "维生素B2(mg)": "Vitamin B2",
708
+ "叶酸(μg DFE)": "Folate",
709
+ "碘(μg)": "Iodine",
710
+ "镁(mg)": "Magnesium",
711
+ "硒(μg)": "Selenium",
712
+ "钾(mg)": "Potassium",
713
+ "钠(mg)": "Sodium",
714
+ "磷(mg)": "Phosphorus",
715
+ "维生素C(mg)": "Vitamin C",
716
+ "膳食纤维(g)": "Dietary Fiber",
717
+ }
718
+
719
+
720
+ def _nutrient_display(name_cn: str, lang: str) -> str:
721
+ """返回营养素名的展示文本(中文模式显示中文+英文,英文模式显示英文)"""
722
+ en_name = NUTRIENT_EN_NAMES.get(name_cn, name_cn)
723
+ if lang == "zh":
724
+ return f"{name_cn} / {en_name}"
725
+ return en_name
726
+
727
+
728
  # ============================================================
729
  # 自定义 CSS 样式
730
  # ============================================================