J.B-Lin commited on
Commit
2ddf335
·
1 Parent(s): ec87820

feat(ui): modern UI redesign with 4 tabs, glassmorphism cards, and pure HTML nutrition dashboard

Browse files
Files changed (5) hide show
  1. README.md +79 -564
  2. app.py +1 -1
  3. modules/nutrition_analyzer.py +18 -8
  4. ui/app_builder.py +53 -142
  5. utils.py +363 -178
README.md CHANGED
@@ -1,579 +1,94 @@
1
- ---
2
- title: PregoPal
3
- emoji: 📈
4
- colorFrom: pink
5
- colorTo: green
6
- sdk: gradio
7
- sdk_version: 6.16.0
8
- python_version: '3.13'
9
- app_file: app.py
10
- pinned: false
11
- license: mit
12
- short_description: Voice-based Pregnant Meal & Nutrition Tracker
13
- ---
14
-
15
- # PregoPal — 孕期陪护 AI 助手
16
-
17
- > **并行 Cline 协作接口文档**
18
- > 每个 Cline 在修改模块前,先 `git pull --rebase`;修改完成后立即 `git commit && git push`。
19
-
20
- ---
21
-
22
- ## 1. 项目概述
23
-
24
- PregoPal 是一款面向孕期家庭的 AI 陪护工具,支持:
25
-
26
- | 功能 | 状态 | 负责模块 |
27
- |------|------|----------|
28
- | 🎤 声纹识别与家庭成员区分 | 基线可用 | `modules/voiceprint.py` |
29
- | 🍽️ 今日菜品推荐(营养+家庭能力 | 基线可用 | `modules/meal_recommender.py` |
30
- | 📝 饮食记录储存(JSON + Markdown) | 已实现 | `modules/diet_logger.py` + `modules/diet_extractor.py` |
31
- | 📊 营养分析与可视化报告 | 基线可用 | `modules/nutrition_analyzer.py` + `modules/nutrition_standards.py` |
32
-
33
- **技术栈**: Python 3.13 · Gradio 6.16 · MiniCPM-o 4.5 · Matplotlib · Pandas
34
-
35
- ---
36
-
37
- ## 2. 架构总览
38
-
39
- ```
40
- app.py ← Gradio 薄入口
41
- ├── ui/app_builder.py ← 前端 Tab 布局(饮食记录/营养分析/家庭管理/声纹/菜品推荐/环境配置)
42
- ├── loop.py ← 每日自动分析循环(状态机)
43
- │ ├── plugins/base.py ← 插件基类(LoopPlugin, PluginRegistry, LoopContext)
44
- │ ├── plugins/family_quiz.py ← 家庭问卷插件(菜谱/体重检查)
45
- │ ├── plugins/diet_summary.py ← 昨日饮食总结
46
- │ ├── plugins/weight_check.py ← 体重检查插件
47
- │ ├── plugins/family_memory.py← 家庭记忆处理
48
- │ ├── plugins/dri_analysis.py ← DRIs 营养对比分析
49
- │ ├── plugins/briefing_generator.py ← 今日简报生成
50
- │ ├── plugins/three_day_summary.py ← 每三天综合总结
51
- │ └── plugins/preset_writer.py ← 预设/缓存写入
52
- ├── modules/ ← 核心业务逻辑(纯函数/无状态)
53
- │ ├── voiceprint.py ← 声纹识别
54
- │ ├── meal_recommender.py ← 菜品推荐
55
- │ ├── diet_extractor.py ← AI 回复数据提取
56
- │ ├── diet_logger.py ← 饮食记录存储
57
- │ ├── family_manager.py ← 家庭信息管理
58
- │ ├── nutrition_standards.py ← 中国官方营养标准
59
- │ ├── nutrition_analyzer.py ← 营养分析与可视化
60
- │ └── core/ ← 核心能力层
61
- │ ├── model_loader.py ← MiniCPM-o 模型加载
62
- │ ├── voice_processor.py ← 语音处理
63
- │ ├── vision_processor.py ← 视觉处理
64
- │ └── conversation_manager.py ← 对话管理
65
- ├── config.py ← 全局配置(路径/常量/数据模板)
66
- ├── utils.py ← 工具函数(中文字体设置等)
67
- └── data/ ← 持久化存储
68
- ├── diet_logs.json ← 结构化饮食记录 JSON
69
- ├── nutrition_db.json ← 营养数据库 JSON
70
- ├── family.json ← 家庭成员声纹 JSON
71
- ├── family/ ← 家庭信息 Markdown(recipes/preferences/memory)
72
- ├── logs/ ← 每日饮食日志 Markdown
73
- ├── presets/ ← 预设/缓存 + .daily_status.json
74
- ├── reports/ ← 营养报告 Markdown
75
- ├── voices/ ← 声纹音频文件
76
- └── nutrition/ ← 营养标准原始文档
77
- ```
78
-
79
- ---
80
-
81
- ## 3. 核心模块接口说明
82
-
83
- > **约定**:模块间通过函数调用传递数据。所有返回 dict 的接口,其 key 约定在下方列出。
84
- > **修改清单**:改任何模块的输入/输出签名时,必须同步更新本 README 对应条目。
85
-
86
- ### 3.1 声纹识别 — `modules/voiceprint.py`
87
-
88
- 类:`VoiceprintManager`
89
-
90
- | 方法 | 输入 | 输出 | 说明 |
91
- |------|------|------|------|
92
- | `register_member(name, relation, audio_path)` | `name: str`, `relation: str`, `audio_path: str` (Path) | `(member_info: dict\|None, msg: str)` | 注册新成员声纹,返回 `{"id","name","relation","registered_at","audio_path","features"}` |
93
- | `identify_speaker(audio_path)` | `audio_path: str` (Path) | `(member_info: dict\|None, msg: str)` | 识别说话人,需声纹库非空 |
94
- | `get_members_list()` | — | `str` (格式化文本) | 获取已注册成员列表 |
95
- | `delete_member(member_id)` | `member_id: str` | `str` (结果消息) | 删除指定成员 |
96
-
97
- **依赖**:
98
- - `VOICE_DIR`(`data/voices/`)从 `config.py` 导入
99
- - `FAMILY_FILE`(`data/family.json`)— JSON 格式 `{"members": [...], "voiceprints": {...}}`
100
- - `config.VOICEPRINT_SIMILARITY_THRESHOLD`(默认 0.7)
101
-
102
- **内部数据结构**(`data/family.json`):
103
- ```json
104
- {
105
- "members": [
106
- {
107
- "id": "abc12345",
108
- "name": "小红",
109
- "relation": "孕妇",
110
- "registered_at": "2026-06-09T07:00:00",
111
- "audio_path": "data/voices/abc12345.wav",
112
- "features": {"mean": 0.01, "std": 0.05, "max": 0.5, "min": -0.5, "zero_crossing_rate": 0.3, "energy": 0.001, "duration": 3.5}
113
- }
114
- ],
115
- "voiceprints": {
116
- "abc12345": {"mean": 0.01, ...}
117
- }
118
- }
119
- ```
120
-
121
- > ⚠️ **风险等级:中** — 当前使用频谱统计特征(baseline),后续升级 Whisper encoder 需保持 `register_member`/`identify_speaker` 签名不变。
122
-
123
- ---
124
-
125
- ### 3.2 菜品推荐 — `modules/meal_recommender.py`
126
-
127
- 类:`MealRecommender`
128
-
129
- | 方法 | 输入 | 输出 | 说明 |
130
- |------|------|------|------|
131
- | `get_recommendation(preference, trimester, restrictions)` | `preference: str=""`, `trimester: str="孕中期"`, `restrictions: str=""` | `dict` | 返回今日推荐食谱 |
132
- | `format_meal_plan(recommendation)` | `recommendation: dict`(上一个方法的返回值) | `str` | 格式化为可读文本 |
133
-
134
- **输出 dict schema**(`get_recommendation` 返回):
135
- ```json
136
- {
137
- "date": "2026-06-09",
138
- "trimester": "孕中期",
139
- "preference": "想吃清淡的",
140
- "focus": "补充蛋白质、钙、铁",
141
- "meals": {
142
- "早餐": "小米粥+包子+煮鸡蛋",
143
- "午餐": "番茄牛腩+杂粮饭+凉拌黄瓜",
144
- "晚餐": "蒸蛋羹+小米粥+炒青菜",
145
- "加餐": "酸奶+坚果"
146
- },
147
- "tips": ["🌿 孕中期建议:...", "💡 建议每天饮水..."]
148
- }
149
- ```
150
-
151
- **依赖**:
152
- - `config.MEAL_TEMPLATES`(4 餐次 × 4 选项的食谱模板 dict)
153
- - `config.TRIMESTER_ADJUSTMENTS`(各孕期阶段的 focus/avoid 建议)
154
- - `config.TRIMESTER_TIPS`(各孕期阶段的饮食提示字符串)
155
-
156
- > ⚠️ **风险等级:中** — 当前为随机模板推荐。后续对接 AI 对话推荐时需保持 `get_recommendation` 签名,内部逻辑可任意替换。
157
-
158
- ---
159
-
160
- ### 3.3 AI 对话数据提取 — `modules/diet_extractor.py`
161
-
162
- 类:`DietExtractor`(全静态方法)
163
-
164
- | 方法 | 输入 | 输出 | 说明 |
165
- |------|------|------|------|
166
- | `extract_all(text)` | `text: str` (AI 回复全文) | `dict` | 正则提取所有结构化数据 |
167
- | `fallback_extract_diet(text)` | `text: str` | `dict\|None` | 关键词 fallback 提取饮食 |
168
- | `fallback_extract_thinking(text)` | `text: str` | `dict\|None` | 关键词 fallback 提取思考 |
169
- | `robust_extract(text)` | `text: str` | `dict` | 正则优先 → fallback 兜底 |
170
-
171
- **`extract_all` 返回 dict schema**:
172
- ```json
173
- {
174
- "diets": [
175
- {
176
- "meals": {"早餐": "全麦面包+鸡蛋+牛奶", "午餐": "清蒸鱼", "晚餐": "", "加餐": ""},
177
- "日期": "2026-06-09",
178
- "记录人": "孕妇",
179
- "备注": "孕妇吃了"
180
- }
181
- ],
182
- "recipes": [{"菜名": "清蒸鱼", "制作人": "丈夫", "难度": "简单", "食材": "鱼、姜", "备注": ""}],
183
- "preferences": [{"人员": "孕妇", "类型": "偏好", "内容": "爱吃酸"}],
184
- "weights": [{"日期": "2026-06-09", "体重": "65", "记录人": "孕妇自己"}],
185
- "memories": [{"类型": "事件", "内容": "婆婆今天来家里"}],
186
- "thinking": {"当前步骤": "记录饮食", "下一步": "分析营养"} // or None
187
- }
188
- ```
189
-
190
- **Markdown 提取标记格式**(AI 回复中需含):
191
- ```
192
- [EXTRACT_DIET]...[/EXTRACT_DIET]
193
- [EXTRACT_RECIPE]...[/EXTRACT_RECIPE]
194
- [EXTRACT_PREFERENCE]...[/EXTRACT_PREFERENCE]
195
- [EXTRACT_WEIGHT]...[/EXTRACT_WEIGHT]
196
- [EXTRACT_MEMORY]...[/EXTRACT_MEMORY]
197
- [THINKING]...[/THINKING]
198
- ```
199
-
200
- **辅助函数**:
201
- - `get_extract_prompt(date_str=None) -> str` — 返回含日期占位符的 System Prompt 模板
202
-
203
- > ✅ **风险等级:低** — 接口稳定,已存在完整测试(`tests/test_diet_extractor.py`)。
204
-
205
- ---
206
-
207
- ### 3.4 饮食记录存储 — `modules/diet_logger.py`
208
-
209
- 类:`DietLogger`
210
-
211
- | 方法 | 输入 | 输出 | 说明 |
212
- |------|------|------|------|
213
- | `add_record(member_name, member_relation, date, meals, mood, notes)` | `member_name: str`, `member_relation: str`, `date: str (ISO)`, `meals: dict`, `mood: str=""`, `notes: str=""` | `(record: dict, md_path: Path)` | 添加记录 → JSON + MD 双写 |
214
- | `get_recent_records(days=7)` | `days: int` | `list[dict]` | 获取近 N 天记录 |
215
- | `get_all_markdown_files()` | — | `list[Path]` | 所有 MD 日志文件 |
216
- | `parse_diet_record(text) — static` | `text: str` | `dict\|None` | [DIET_RECORD] 标记解析(后续 AI 版本用) |
217
-
218
- **`add_record` 返回的 record dict**:
219
- ```json
220
- {
221
- "id": "a1b2c3d4",
222
- "member_name": "小红",
223
- "member_relation": "孕妇",
224
- "date": "2026-06-09",
225
- "meals": {"早餐": "燕麦粥+坚果", "午餐": "清蒸鱼+米饭"},
226
- "mood": "挺好",
227
- "notes": "今天胃口不错",
228
- "extensions": {},
229
- "created_at": "2026-06-09T12:00:00"
230
- }
231
- ```
232
-
233
- **依赖**:
234
- - `config.DIET_LOG_FILE`(`data/diet_logs.json`)— JSON 格式
235
- - `config.LOGS_DIR`(`data/logs/`)— Markdown 日志目录
236
- - `config.DIET_LOG_SCHEMA_VERSION`(当前 "1.0")
237
-
238
- **Markdown 日志文��格式**(`data/logs/饮食日志_YYYY-MM-DD.md`):
239
- ```markdown
240
- # 🥗 孕期饮食日志
241
-
242
- ## 📋 基本信息
243
- - **日期**: 2026-06-09
244
- - **记录人**: 孕妇 - 小红
245
- - **记录时间**: 2026-06-09T12:00:00
246
-
247
- ## 🍽️ 今日饮食记录
248
-
249
- ### 早餐
250
- - 燕麦粥+坚果
251
-
252
- ### 午餐
253
- - 清蒸鱼+米饭
254
-
255
- ---
256
- *由 PregoPal 自动生成*
257
- ```
258
-
259
- > ✅ **风险等级:低** — schema 已加版本号,`extensions: {}` 字段可向后兼容扩展。
260
-
261
- ---
262
-
263
- ### 3.5 家庭信息管理 — `modules/family_manager.py`
264
-
265
- 三个管理器类,全部使用 `@classmethod`(无需实例化):
266
-
267
- #### 3.5.1 RecipeManager — 家庭菜谱
268
- | 方法 | 输入 | 输出 | 说明 |
269
- |------|------|------|------|
270
- | `load_all()` | — | `list[dict]` | 读取所有菜谱 |
271
- | `add_recipe(name, cook, difficulty, ingredients, notes)` | 五个 `str` 参数 | `str` (结果消息) | 添加菜谱 |
272
- | `get_names()` | — | `list[str]` | 所有菜名 |
273
- | `format_for_prompt()` | — | `str` | System Prompt 格式 |
274
-
275
- 菜谱 dict: `{"name", "cook", "difficulty", "ingredients", "notes"}`
276
-
277
- #### 3.5.2 PreferenceManager — 饮食偏好
278
- | 方法 | 输入 | 输出 | 说明 |
279
- |------|------|------|------|
280
- | `load_all()` | — | `list[dict]` | 所有成员偏好 |
281
- | `add_member(name, role, preferences, avoid, allergies, notes)` | 六个 `str` 参数 | `str` (结果消息) | 添加/更新(覆盖同名) |
282
- | `format_for_prompt()` | — | `str` | System Prompt 格式 |
283
-
284
- 偏好 dict: `{"name", "role", "preferences", "avoid", "allergies", "notes"}`
285
-
286
- #### 3.5.3 MemoryManager — 家庭记忆
287
- | 方法 | 输入 | 输出 | 说明 |
288
- |------|------|------|------|
289
- | `load_all()` | — | `dict` (relationships/events/daily) | 所有记忆 |
290
- | `add_event(description)` | `str` | `str` (结果消息) | 添加重要事件 |
291
- | `add_daily(content)` | `str` | `str` (结果消息) | 添加日常记录 |
292
- | `format_for_prompt()` | — | `str` | System Prompt 格式 |
293
-
294
- **顶层统一接口**:
295
- - `load_all_family_info() -> dict` — 一次性加载所有家庭信息
296
- - `format_all_for_prompt() -> str` — 全量 System Prompt 文本
297
-
298
- **数据文件**(`data/family/`):
299
- - `recipes.md` — Markdown 格式表
300
- - `preferences.md` — Markdown 格式表
301
- - `memory.md` — Markdown 格式表
302
-
303
- > ✅ **风险等级:低** — 接口稳定,有完整测试(`tests/test_family_manager.py`)。更新 `add_member` 覆盖逻辑时注意保留已有数据。
304
-
305
- ---
306
-
307
- ### 3.6 营养标准 — `modules/nutrition_standards.py`
308
-
309
- 类:`BMIStandards`, `DietaryGuideStandards`, `DRIsParser`(全部只读数据+静态方法)
310
-
311
- | 类 | 关键方法 | 说明 |
312
- |----|---------|------|
313
- | `BMIStandards` | `get_bmi_range(pre_preg_bmi) → dict`, `get_recommended_gain(pre_preg_bmi) → dict`, `calculate_bmi(weight_kg, height_m) → float` | 孕前 BMI 标准(中国标准) |
314
- | `DietaryGuideStandards` | `get_daily_guideline(trimester) → dict`, `get_food_groups_info() → dict` | 膳食指南推荐 |
315
- | `DRIsParser` | `get_dri_for_trimester(trimester) → dict`, `get_all_dris() → dict` | DRIs 2023 数据 |
316
-
317
- **DRIsParser.get_dri_for_trimester 返回示例**:
318
- ```json
319
- {
320
- "蛋白质": {"value": 70, "unit": "g", "note": "孕中期+15g"},
321
- "钙": {"value": 1000, "unit": "mg"},
322
- "铁": {"value": 27, "unit": "mg"},
323
- "叶酸": {"value": 600, "unit": "mcg"},
324
- ...
325
- }
326
- ```
327
-
328
- > ✅ **风险等级:低** — 只读数据类,有完整测试(`tests/test_nutrition_standards.py`)。
329
-
330
- ---
331
-
332
- ### 3.7 营养分析与可视化 — `modules/nutrition_analyzer.py`
333
-
334
- 类:`NutritionAnalyzer`
335
-
336
- | 方法 | 输入 | 输出 | 说明 |
337
- |------|------|------|------|
338
- | `analyze_diet(records)` | `records: list[dict]` (diet_logger 格式) | `dict` | 营养覆盖分析 |
339
- | `generate_report_chart(analysis)` | `analysis: dict`(上一个方法返回) | `matplotlib.figure.Figure` | 4 面板可视化图表 |
340
- | `generate_report_text(analysis)` | `analysis: dict` | `str` | 文本格式报告 |
341
- | `export_report_markdown(analysis, filename)` | `analysis: dict`, `filename: str\|None` | `Path` | 导出 Markdown 报告 |
342
-
343
- **`analyze_diet` 返回 dict schema**:
344
- ```json
345
- {
346
- "total_days": 7,
347
- "total_records": 21,
348
- "meal_counts": {"早餐": 5, "午餐": 6, "晚餐": 6, "加餐": 4},
349
- "food_items": ["全麦面包", "鸡蛋", "牛奶", "清蒸鱼", ...],
350
- "nutrition_coverage": {
351
- "叶酸": {
352
- "matched_foods": ["菠菜"],
353
- "covered": true,
354
- "recommended_foods": ["菠菜", "西兰花", "芦笋"],
355
- "benefit": "预防胎儿神经管畸形",
356
- "daily_recommend": "0.4",
357
- "unit": "mg"
358
- },
359
- ...
360
- },
361
- "diversity_score": {"score": 75, "details": ["✅ 早餐: 6/7天 (86%)", ...]},
362
- "suggestions": ["⚠️ 以下营养素摄入不足: 铁, 钙", ...]
363
- }
364
- ```
365
-
366
- **图表输出**(`generate_report_chart`):
367
- 1. 左上:营养覆盖雷达图(最多 8 种营养素)
368
- 2. 右上:各餐次频率柱状图
369
- 3. 左下:饮食多样性评分环形图
370
- 4. 右下:营养建议文本框
371
-
372
- **依赖**:
373
- - `config.NUTRITION_DB_FILE`(`data/nutrition_db.json`)— 可自定义
374
- - `config.DEFAULT_NUTRITION_DB`(10 种营养素的内置推荐)
375
- - `config.REPORTS_DIR`(`data/reports/`)
376
- - `utils.setup_chinese_font()` — matplotlib 中文字体
377
-
378
- > 🔴 **风险等级:高(待改造)** — 当前使用通用 `DEFAULT_NUTRITION_DB` 做营养覆盖分析,**尚未对接 `modules/nutrition_standards.py` 的 DRIs 数据**。改造时需:
379
- > 1. 将 `_calculate_nutrition_coverage` 中的 `self.nutrition_db` 替换为 `DRIsParser.get_dri_for_trimester()`
380
- > 2. 保持 `analyze_diet(records) -> dict` 签名不变
381
- > 3. 保持 `nutrition_coverage` 的 dict key 不变(nutrient name 为 key)
382
-
383
- ---
384
-
385
- ### 3.8 插件基类 — `plugins/base.py`
386
-
387
- 核心类型(供所有插件和 loop 使用):
388
-
389
- ```python
390
- # 阶段枚举
391
- class LoopStage(Enum):
392
- FAMILY_QUIZ = "family_quiz"
393
- SUMMARIZE = "summarize"
394
- ANALYZE = "analyze"
395
- BRIEF = "brief"
396
- THREE_DAY = "three_day"
397
- CONSOLIDATE = "consolidate"
398
-
399
- # 上下文容器(插件间共享)
400
- @dataclass
401
- class LoopContext:
402
- briefing: dict = {} # 累积的简报数据
403
- weight_data: dict = {}
404
- diet_records: list = []
405
- family_recipes: list = [] # family_manager.RecipeManager.load_all() 格式
406
- family_memory: dict = {} # family_manager.MemoryManager.load_all() 格式
407
- analysis_results: dict = {}
408
- errors: list = []
409
-
410
- # 插件结果
411
- @dataclass
412
- class PluginResult:
413
- success: bool = True
414
- data: dict = {}
415
- message: str = ""
416
-
417
- # 插件基类
418
- class LoopPlugin(ABC):
419
- @abstractmethod
420
- def stage(self) -> LoopStage: ...
421
- @abstractmethod
422
- def name(self) -> str: ...
423
- @abstractmethod
424
- async def run(self, ctx: LoopContext) -> PluginResult: ...
425
- ```
426
-
427
- > ✅ **风险等级:低** — 核心架构稳定。新增插件:实现 `LoopPlugin`,在 `loop.py` 的 `_register_default_plugins` 中注册即可。
428
-
429
- ---
430
-
431
- ## 4. 插件管线一览
432
-
433
- | 插件名 | 阶段 | 文件 | 职责 |
434
- |--------|------|------|------|
435
- | `FamilyRecipeQuizPlugin` | `FAMILY_QUIZ` | `plugins/family_quiz.py` | 检查是否需要询问家庭菜谱 |
436
- | `WeightQuizPlugin` | `FAMILY_QUIZ` | `plugins/family_quiz.py` | 检查是否需要询问体重 |
437
- | `DietSummaryPlugin` | `SUMMARIZE` | `plugins/diet_summary.py` | 读取昨日饮食日志 |
438
- | `WeightCheckPlugin` | `SUMMARIZE` | `plugins/weight_check.py` | 体重变化分析 |
439
- | `FamilyMemoryPlugin` | `SUMMARIZE` | `plugins/family_memory.py` | 提取家庭记忆 |
440
- | `DRIAnalysisPlugin` | `ANALYZE` | `plugins/dri_analysis.py` | DRIs 营养对比 |
441
- | `BriefingGeneratorPlugin` | `BRIEF` | `plugins/briefing_generator.py` | 汇总生成今日简报 |
442
- | `ThreeDaySummaryPlugin` | `THREE_DAY` | `plugins/three_day_summary.py` | 三天综合总结 |
443
- | `PresetWriterPlugin` | `CONSOLIDATE` | `plugins/preset_writer.py` | 写入预设/缓存 |
444
-
445
- **插件向 `ctx.briefing` 写入的 key**(`BriefingGeneratorPlugin` 最终消费):
446
- ```
447
- ctx.briefing["trimester"] # str
448
- ctx.briefing["need_ask_weight"] # bool
449
- ctx.briefing["weight_quiz_message"] # str
450
- ctx.briefing["weight_evaluation"] # dict
451
- ctx.briefing["need_ask_recipe"] # bool
452
- ctx.briefing["recipe_quiz_message"] # str
453
- ctx.briefing["yesterday_diet"] # dict {"status","summary","meal_count"}
454
- ctx.briefing["dri_analysis"] # dict {"focus_nutrients","summary"}
455
- ctx.briefing["recommended_foods"] # list[str]
456
- ctx.briefing["family_memory"] # dict
457
- ctx.briefing["thinking_keywords"] # str
458
- ```
459
-
460
- ---
461
-
462
- ## 5. 配置模块 — `config.py`
463
-
464
- 每个 Cline 如需新增全局常量,**追加到同类型区域末尾**,并在 commit message 中注明。
465
-
466
- | 配置区 | 主要内容 | 修改风险 |
467
- |--------|---------|----------|
468
- | 目录路径 | `DATA_DIR`, `VOICE_DIR`, `LOGS_DIR`, `REPORTS_DIR`, `FAMILY_FILE`, `DIET_LOG_FILE`, `NUTRITION_DB_FILE` | ⚠️ 中 |
469
- | 常量枚举 | `FAMILY_ROLES`, `TRIMESTERS` | ✅ 低 |
470
- | 数据模板 | `DEFAULT_NUTRITION_DB` (10种营养素), `MEAL_TEMPLATES` (4×4), `TRIMESTER_ADJUSTMENTS`, `TRIMESTER_TIPS` | ⚠️ 中 |
471
- | Schema 版本 | `DIET_LOG_SCHEMA_VERSION = "1.0"` | ⚠️(改版本号前需评估向后兼容) |
472
- | 声纹阈值 | `VOICEPRINT_SIMILARITY_THRESHOLD = 0.7` | ✅ 低 |
473
-
474
- ---
475
-
476
- ## 6. 数据文件格式规范
477
-
478
- | 文件 | 格式 | Schema | 读模块 | 写模块 |
479
- |------|------|--------|--------|--------|
480
- | `data/diet_logs.json` | JSON | `{"schema_version": "1.0", "records": [...]}` | `diet_logger`, `nutrition_analyzer` | `diet_logger` |
481
- | `data/nutrition_db.json` | JSON | `{"营养素名": {"category", "daily_recommend_mg/g/mcg", "foods": [...], "benefit"}}` | `nutrition_analyzer` | `nutrition_analyzer` (首次初始化) |
482
- | `data/family.json` | JSON | `{"members": [...], "voiceprints": {...}}` | `voiceprint` | `voiceprint` |
483
- | `data/family/recipes.md` | Markdown | `### 菜名\n- **制作人**: ...` | `family_manager.RecipeManager` | `family_manager.RecipeManager` |
484
- | `data/family/preferences.md` | Markdown | `### 成员:姓名\n- **偏好**: ...` | `family_manager.PreferenceManager` | `family_manager.PreferenceManager` |
485
- | `data/family/memory.md` | Markdown | `## 重要事件\n- **日期**: ...` | `family_manager.MemoryManager` | `family_manager.MemoryManager` |
486
- | `data/logs/饮食日志_*.md` | Markdown | 见 3.4 节 | `plugins/diet_summary` | `diet_logger` |
487
- | `data/reports/营养报告_*.md` | Markdown | 见 3.7 节 | 用户 | `nutrition_analyzer` |
488
- | `data/presets/.daily_status.json` | JSON | `{"YYYY-MM-DD": {"summary_done": bool, "day_ended": bool}}` | `loop.DailyStatus` | `loop.DailyStatus` |
489
-
490
- ---
491
-
492
- ## 7. 并行 Cline 协作规范
493
-
494
- ### 7.1 分工建议
495
-
496
- ```
497
- Cline A: Modules 强化(营养分析对接 DRIs、菜谱推荐 AI 化)
498
- Cline B: UI 界面优化(Gradio 前端增强、报告模板美化)
499
- Cline C: 声纹升级(Whisper encoder 替换频谱特征)
500
- Cline D: 数据处理与插件增强(diet_extractor fallback、新插件)
501
- ```
502
-
503
- ### 7.2 关键约定
504
-
505
- 1. **改接口前先 grep**:用 `search_files` 搜索方法名找到所有调用方
506
- 2. **config.py 修改需沟通**:新增常量追加到同类型区域,不删改已有常量名
507
- 3. **数据文件向后兼容**:新增字段优先用 `extensions: {}` 或新增可选 key,不删除已有 key
508
- 4. **测试保持通过**:`python tests/run_tests.py` 零失败
509
- 5. **README 同步更新**:接口签名变更 → 更新本 README 对应章节
510
-
511
- ### 7.3 Git 工作流(强制执行)
512
 
513
  ```bash
514
- # 工作前
515
- git pull --rebase origin main
516
 
517
- # 开发中:每完成一个独立功能点就提交
518
- git add -A
519
- git commit -m "feat: 描述做了什么"
520
 
521
- # 推送前再次 pull
522
- git pull --rebase origin main
523
-
524
- # 测试通过后推送
525
- python tests/run_tests.py
526
- git push origin main
527
  ```
528
 
529
- ### 7.4 Commit Message 规范
530
 
531
  ```
532
- feat: 新功能/新模块/新接口
533
- fix: 修复 bug
534
- test: 添加或修改测试
535
- refactor: 重构(不改功能)
536
- docs: 文档更新(含本 README
537
- chore: 配置/依赖/路径调整
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  ```
539
 
540
- ### 7.5 冲突处理策略
541
-
542
- | 冲突类型 | 处理方式 |
543
- |---------|---------|
544
- | `config.py` 常量冲突 | 取并集,双方新增常量都保留 |
545
- | 同函数不同实现 | 保留逻辑更完整的一方,另一方改动如果无冲突则合并到合适位置 |
546
- | 数据文件(JSON/MD)冲突 | 保留两个版本共有的条目 + 各自独有条目(去重) |
547
- | 测试文件冲突 | **取并集**:保留所有测试用例 |
548
- | 本 README 冲突 | **取行数更长的一方**��手动整合 |
549
- | `.gitignore` 冲突 | 取并集 |
550
 
551
- ---
 
 
 
 
552
 
553
- ## 8. 测试
554
-
555
- ```bash
556
- # 运行全部测试
557
- python tests/run_tests.py
558
- ```
559
-
560
- 当前测试覆盖:
561
- - `test_diet_extractor.py` — ✅ 通过(正则解析 + fallback 提取)
562
- - `test_family_manager.py` — ✅ 通过(菜谱/偏好/记忆 CRUD)
563
- - `test_nutrition_standards.py` — ✅ 通过(BMI/膳食指南/DRIs 数据)
564
- - `test_loop.py` — 循环执行测试
565
- - `test_plugins.py` — 插件集成测试
566
-
567
- ---
568
-
569
- ## 9. Model: MiniCPM-o 4.5
570
-
571
- ```
572
- Model to be Used: MiniCPM-o 4.5
573
- ```
574
 
575
- 核心能力层(`core/` 目录)提供模型加载、语音处理、视觉处理、对话管理的底层能力封装。详见各文件 docstring。
 
 
 
 
576
 
577
- ---
578
 
579
- *最后更新: 2026-06-09 | 由 PregoPal Cline 团队维护*
 
1
+ # 🌸 PregoPal - 孕期陪护AI助手
2
+
3
+ 一个温馨的家庭式孕期AI伴侣,基于语音交互与智能分析,为孕妇提供全方位的饮食营养陪伴。
4
+
5
+ ## ✨ 核心功能
6
+
7
+ ### 1. 🏠 现代仪表盘首页
8
+ - **全双工语音交互**:大圆形语音按钮,点击即可说话
9
+ - **AI 实时思考**:显示AI当前的思维状态
10
+ - **信息卡片**:
11
+ - 📅 孕期阶段展示
12
+ - 🥗 营养关注与推荐食物
13
+ - 🍽️ 昨日饮食回顾
14
+ - 🍳 家庭菜谱概览
15
+ - ⚖️ 体重管理状态
16
+ - **最近饮食记录**:以表格形式展示近3天饮食
17
+
18
+ ### 2. 👨‍👩‍👧‍👦 家庭饮食习惯(纯展示)
19
+ - **🍳 家庭菜谱**:自动收藏的菜谱卡片网格
20
+ - **🥗 饮食偏好**:每位家庭成员的偏好/忌口标签
21
+ - **📝 家庭记事**:AI记忆的时间线展示
22
+ - 支持手动刷新
23
+
24
+ ### 3. 📊 三天营养总结
25
+ - 选择孕期阶段(早/中/晚期)
26
+ - 分析近三天饮食数据
27
+ - 智能菜单改进建议
28
+
29
+ ### 4. 📈 营养报告(纯HTML仪表盘
30
+ - **综合营养评分**:渐变进度条 + 状态标签
31
+ - **餐次完成率**:早餐/午餐/晚餐/加餐的柱状统计
32
+ - **关键营养覆盖**:各营养素百分比进度条
33
+ - **饮食多样性**:分数 + 食材种类统计
34
+ - **AI 饮食建议**:根据分析结果智能提示
35
+
36
+ ### 5. 🌐 中英文切换
37
+ 一键切换中/英文界面
38
+
39
+ ## 🚀 快速启动
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
  ```bash
42
+ # 1. 安装依赖
43
+ pip install -r requirements.txt
44
 
45
+ # 2. 运行
46
+ python app.py
 
47
 
48
+ # 3. 打开浏览器访问
49
+ # http://127.0.0.1:7860
 
 
 
 
50
  ```
51
 
52
+ ## 🏗️ 项目结构
53
 
54
  ```
55
+ PregoPal/
56
+ ├── app.py # 应用入口
57
+ ├── config.py # 全局配置
58
+ ├── loop.py # 核心循环引擎
59
+ ├── utils.py # 工具函数(CSS/翻译/HTML渲染
60
+ ├── requirements.txt # 依赖清单
61
+ ├── ui/
62
+ │ └── app_builder.py # Gradio界面构建(4 Tab)
63
+ ├── modules/
64
+ │ ├── voiceprint.py # 声纹识别
65
+ │ ├── diet_logger.py # 饮食日志
66
+ │ ├── diet_extractor.py # 饮食信息提取
67
+ │ ├── nutrition_analyzer.py # 营养分析
68
+ │ ├── meal_recommender.py # 菜品推荐
69
+ │ ├── family_manager.py # 家庭管理
70
+ │ └── nutrition_standards.py # 营养标准
71
+ ├── plugins/ # Loop插件
72
+ ├── core/ # 核心基础设施
73
+ └── data/ # 数据存储
74
  ```
75
 
76
+ ## 🎨 UI 设计特色
 
 
 
 
 
 
 
 
 
77
 
78
+ - **Glassmorphism 毛玻璃卡片**:现代、轻盈的视觉风格
79
+ - **粉色主题**:温暖、舒适的孕期配色
80
+ - **纯HTML渲染**:营养报告无需matplotlib,实时生成
81
+ - **响应式布局**:桌面与移动端自适应
82
+ - **Tab永不折叠**:所有Tab始终可见
83
 
84
+ ## 🔧 技术栈
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
+ - **前端**:Gradio + 自定义CSS/HTML
87
+ - **后端**:Python
88
+ - **语音**:声纹识别 + 全双工语音交互
89
+ - **AI**:大语言模型驱动分析
90
+ - **数据**:JSON本地存储
91
 
92
+ ## 📝 许可
93
 
94
+ MIT License
app.py CHANGED
@@ -28,7 +28,7 @@ if __name__ == "__main__":
28
  # 创建并启动 Gradio 应用
29
  demo = create_app(loop)
30
  demo.launch(
31
- server_name="0.0.0.0",
32
  server_port=7860,
33
  share=False,
34
  debug=False,
 
28
  # 创建并启动 Gradio 应用
29
  demo = create_app(loop)
30
  demo.launch(
31
+ server_name="127.0.0.1",
32
  server_port=7860,
33
  share=False,
34
  debug=False,
modules/nutrition_analyzer.py CHANGED
@@ -68,18 +68,27 @@ class NutritionAnalyzer:
68
  nutrition_coverage = self._calculate_nutrition_coverage(food_items)
69
 
70
  # 计算饮食多样性评分
71
- diversity_score = self._calculate_diversity_score(meal_counts, total_days)
72
 
73
  # 生成建议
74
- suggestions = self._generate_suggestions(nutrition_coverage, diversity_score)
 
 
 
 
 
 
 
75
 
76
  return {
 
77
  "total_days": total_days,
78
  "total_records": len(records),
79
  "meal_counts": meal_counts,
80
  "food_items": list(set(food_items)),
81
  "nutrition_coverage": nutrition_coverage,
82
- "diversity_score": diversity_score,
 
83
  "suggestions": suggestions
84
  }
85
 
@@ -192,7 +201,8 @@ class NutritionAnalyzer:
192
 
193
  # 3. 饮食多样性评分仪表盘
194
  ax3 = fig.add_subplot(2, 2, 3)
195
- score = analysis["diversity_score"]["score"]
 
196
  ax3.pie([score, 100 - score], startangle=90,
197
  colors=['#FF6B9D', '#F0F0F0'],
198
  wedgeprops={'width': 0.3, 'edgecolor': 'white'})
@@ -228,12 +238,12 @@ class NutritionAnalyzer:
228
  f"分析周期: {analysis['total_days']} 天",
229
  f"记录总数: {analysis['total_records']} 条",
230
  "",
231
- "饮食多样性评分: {}/100".format(analysis['diversity_score']['score']),
232
  ]
233
 
234
  lines.append("")
235
  lines.append("各餐次记录情况:")
236
- for detail in analysis['diversity_score']['details']:
237
  lines.append(f" {detail}")
238
 
239
  lines.append("")
@@ -270,12 +280,12 @@ class NutritionAnalyzer:
270
  - **生成时间**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}
271
 
272
  ## 饮食多样性评分
273
- **评分: {analysis.get('diversity_score', {}).get('score', 0)}/100**
274
 
275
  | 餐次 | 记录天数 | 覆盖率 |
276
  |------|---------|--------|
277
  """
278
- for detail in analysis.get('diversity_score', {}).get('details', []):
279
  parts = detail.split(': ', 1)
280
  if len(parts) == 2:
281
  content += f"| {parts[0]} | {parts[1]} |\n"
 
68
  nutrition_coverage = self._calculate_nutrition_coverage(food_items)
69
 
70
  # 计算饮食多样性评分
71
+ diversity_result = self._calculate_diversity_score(meal_counts, total_days)
72
 
73
  # 生成建议
74
+ suggestions = self._generate_suggestions(nutrition_coverage, diversity_result)
75
+
76
+ # 综合评分:多样性(50%) + 营养覆盖(50%)
77
+ diversity_num = diversity_result.get("score", 0) if isinstance(diversity_result, dict) else 0
78
+ covered_nutrients = sum(1 for v in nutrition_coverage.values() if v["covered"])
79
+ total_nutrients = max(len(nutrition_coverage), 1)
80
+ nutrition_score = int(covered_nutrients / total_nutrients * 100)
81
+ overall_score = int(diversity_num * 0.5 + nutrition_score * 0.5)
82
 
83
  return {
84
+ "score": overall_score,
85
  "total_days": total_days,
86
  "total_records": len(records),
87
  "meal_counts": meal_counts,
88
  "food_items": list(set(food_items)),
89
  "nutrition_coverage": nutrition_coverage,
90
+ "diversity_score": diversity_num,
91
+ "diversity_details": diversity_result.get("details", []),
92
  "suggestions": suggestions
93
  }
94
 
 
201
 
202
  # 3. 饮食多样性评分仪表盘
203
  ax3 = fig.add_subplot(2, 2, 3)
204
+ score = analysis.get("diversity_score", 0)
205
+ if isinstance(score, dict): score = score.get("score", 0)
206
  ax3.pie([score, 100 - score], startangle=90,
207
  colors=['#FF6B9D', '#F0F0F0'],
208
  wedgeprops={'width': 0.3, 'edgecolor': 'white'})
 
238
  f"分析周期: {analysis['total_days']} 天",
239
  f"记录总数: {analysis['total_records']} 条",
240
  "",
241
+ "饮食多样性评分: {}/100".format(analysis.get('diversity_score', 0) if not isinstance(analysis.get('diversity_score'), dict) else analysis['diversity_score'].get('score', 0)),
242
  ]
243
 
244
  lines.append("")
245
  lines.append("各餐次记录情况:")
246
+ for detail in analysis.get('diversity_details', []):
247
  lines.append(f" {detail}")
248
 
249
  lines.append("")
 
280
  - **生成时间**: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}
281
 
282
  ## 饮食多样性评分
283
+ **评分: {analysis.get('diversity_score', 0) if not isinstance(analysis.get('diversity_score'), dict) else analysis['diversity_score'].get('score', 0)}/100**
284
 
285
  | 餐次 | 记录天数 | 覆盖率 |
286
  |------|---------|--------|
287
  """
288
+ for detail in analysis.get('diversity_details', []):
289
  parts = detail.split(': ', 1)
290
  if len(parts) == 2:
291
  content += f"| {parts[0]} | {parts[1]} |\n"
ui/app_builder.py CHANGED
@@ -1,11 +1,11 @@
1
  """
2
- PregoPal - Gradio 界面构建
3
- ===========================
4
- 4 个子页面 + 中英文切换 + 自定义 CSS
5
  1. 🏠 首页(语音启动按钮 + AI 思考 + 简报卡片 + 最近记录)
6
- 2. 👨‍👩‍👧‍👦 家庭饮食习惯
7
  3. 📊 三天总结
8
- 4. 📈 营养报告
9
 
10
  语言切换设计:
11
  用唯一一个 @gr.render(inputs=[lang_state]) 包裹全部 4 个 Tab,
@@ -22,7 +22,13 @@ from modules.nutrition_analyzer import NutritionAnalyzer
22
  from modules.family_manager import RecipeManager, PreferenceManager, MemoryManager
23
  from modules.diet_extractor import DietExtractor
24
  from modules.nutrition_standards import DRIsParser
25
- from utils import t, ZH, EN, get_home_cards, CUSTOM_CSS
 
 
 
 
 
 
26
 
27
  # ============================================================
28
  # 全局实例(单例)
@@ -173,18 +179,7 @@ def _home_content(loop, lang):
173
  with gr.Column(scale=2):
174
  gr.HTML(f"""
175
  <div style="text-align: center; padding: 24px 0;">
176
- <button class="voice-main-btn" style="
177
- width:160px;height:160px;border-radius:50%;
178
- background:linear-gradient(135deg,#FF6B8A 0%,#FF4081 100%);
179
- border:4px solid #FFF;
180
- box-shadow:0 8px 32px rgba(255,64,129,0.3);
181
- cursor:pointer;font-size:48px;color:white;
182
- transition:all 0.3s ease;"
183
- onmouseover="this.style.transform='scale(1.05)';this.style.boxShadow='0 12px 40px rgba(255,64,129,0.4)'"
184
- onmouseout="this.style.transform='scale(1)';this.style.boxShadow='0 8px 32px rgba(255,64,129,0.3)'"
185
- >
186
- 🎙️
187
- </button>
188
  <p style="margin-top: 8px; color: #999; font-size: 14px;">{T['tap_hint']}</p>
189
  </div>
190
  """)
@@ -233,107 +228,39 @@ def _home_content(loop, lang):
233
 
234
 
235
  # ============================================================
236
- # Tab 2: 👨‍👩‍👧‍👦 家庭饮食习惯
237
  # ============================================================
238
  def _family_content(lang):
239
- """家庭饮食习惯组件"""
240
- with gr.Tabs():
241
- with gr.Tab(t("tab_preferences", lang)):
242
- with gr.Row():
243
- with gr.Column():
244
- pref_name = gr.Textbox(label=t("pref_name", lang), placeholder="小明")
245
- pref_role = gr.Dropdown(
246
- label=t("pref_role", lang),
247
- choices=ZH["relation_choices"] if lang == "zh" else EN["relation_choices"],
248
- value=ZH["relation_choices"][0] if lang == "zh" else EN["relation_choices"][0],
249
- )
250
- pref_likes = gr.Textbox(
251
- label=t("pref_likes", lang),
252
- placeholder=t("pref_likes_placeholder", lang),
253
- lines=1,
254
- )
255
- pref_avoid = gr.Textbox(
256
- label=t("pref_avoid", lang),
257
- placeholder=t("pref_avoid_placeholder", lang),
258
- lines=1,
259
- )
260
- pref_allergy = gr.Textbox(
261
- label=t("pref_allergy", lang),
262
- placeholder=t("pref_allergy_placeholder", lang),
263
- lines=1,
264
- )
265
- save_pref_btn = gr.Button(t("btn_save", lang), variant="primary", size="sm")
266
- with gr.Column():
267
- refresh_pref_btn = gr.Button(t("btn_refresh", lang), size="sm")
268
- pref_display = gr.Textbox(label="", interactive=False, lines=12)
269
-
270
- save_pref_btn.click(
271
- fn=lambda n, r, p, a, al: PreferenceManager.add_member(n, r, p, a, al, ""),
272
- inputs=[pref_name, pref_role, pref_likes, pref_avoid, pref_allergy],
273
- outputs=[gr.Textbox()],
274
- ).then(fn=lambda: PreferenceManager.format_for_prompt(), outputs=pref_display)
275
- refresh_pref_btn.click(fn=lambda: PreferenceManager.format_for_prompt(), outputs=pref_display)
276
 
 
277
  with gr.Tab(t("tab_recipes", lang)):
278
- with gr.Row():
279
- with gr.Column():
280
- recipe_name = gr.Textbox(
281
- label=t("recipe_name", lang),
282
- placeholder=t("recipe_name_placeholder", lang),
283
- )
284
- recipe_cook = gr.Textbox(
285
- label=t("recipe_cook", lang),
286
- placeholder=t("recipe_cook_placeholder", lang),
287
- )
288
- recipe_diff = gr.Dropdown(
289
- label=t("recipe_diff", lang),
290
- choices=ZH["recipe_diff_choices"] if lang == "zh" else EN["recipe_diff_choices"],
291
- value=ZH["recipe_diff_choices"][1] if lang == "zh" else EN["recipe_diff_choices"][1],
292
- )
293
- recipe_ingredients = gr.Textbox(
294
- label=t("recipe_ingredients", lang),
295
- placeholder=t("recipe_ingredients_placeholder", lang),
296
- lines=1,
297
- )
298
- save_recipe_btn = gr.Button(t("btn_add", lang), variant="primary", size="sm")
299
- with gr.Column():
300
- refresh_recipe_btn = gr.Button(t("btn_refresh", lang), size="sm")
301
- recipe_display = gr.Textbox(label="", interactive=False, lines=12)
302
-
303
- save_recipe_btn.click(
304
- fn=lambda n, c, d, i: RecipeManager.add_recipe(n, c, d, i, ""),
305
- inputs=[recipe_name, recipe_cook, recipe_diff, recipe_ingredients],
306
- outputs=[gr.Textbox()],
307
- ).then(fn=lambda: RecipeManager.format_for_prompt(), outputs=recipe_display)
308
- refresh_recipe_btn.click(fn=lambda: RecipeManager.format_for_prompt(), outputs=recipe_display)
309
 
310
  with gr.Tab(t("tab_memory", lang)):
311
- with gr.Row():
312
- with gr.Column():
313
- memory_type = gr.Dropdown(
314
- label=t("memory_type", lang),
315
- choices=ZH["memory_type_choices"] if lang == "zh" else EN["memory_type_choices"],
316
- value=ZH["memory_type_choices"][0] if lang == "zh" else EN["memory_type_choices"][0],
317
- )
318
- memory_content = gr.Textbox(
319
- label=t("memory_content", lang),
320
- placeholder=t("memory_content_placeholder", lang),
321
- lines=2,
322
- )
323
- save_memory_btn = gr.Button(t("btn_save_memory", lang), variant="primary", size="sm")
324
- with gr.Column():
325
- refresh_memory_btn = gr.Button(t("btn_refresh", lang), size="sm")
326
- memory_display = gr.Textbox(label="", interactive=False, lines=12)
327
-
328
- def save_memory(m_type, content):
329
- if m_type in ["事件", "Event"]:
330
- return MemoryManager.add_event(content)
331
- return MemoryManager.add_daily(f"[{m_type}] {content}")
332
-
333
- save_memory_btn.click(
334
- fn=save_memory, inputs=[memory_type, memory_content], outputs=[gr.Textbox()]
335
- ).then(fn=lambda: MemoryManager.format_for_prompt(), outputs=memory_display)
336
- refresh_memory_btn.click(fn=lambda: MemoryManager.format_for_prompt(), outputs=memory_display)
337
 
338
 
339
  # ============================================================
@@ -386,40 +313,30 @@ def _summary_content(lang):
386
 
387
 
388
  # ============================================================
389
- # Tab 4: 📈 营养报告
390
  # ============================================================
391
  def _report_content(lang):
392
- """营养报告组件"""
393
  with gr.Row():
394
  with gr.Column(scale=1):
395
- analysis_days = gr.Slider(label=t("analysis_days", lang), minimum=1, maximum=30, value=7, step=1)
 
 
 
396
  generate_btn = gr.Button(t("btn_generate", lang), variant="primary")
397
- with gr.Column(scale=2):
398
- report_text = gr.Textbox(label=t("report_text", lang), interactive=False, lines=12)
399
-
400
- with gr.Row():
401
- report_plot = gr.Plot(label=t("report_chart", lang))
402
 
403
- with gr.Row():
404
- export_md_btn = gr.Button(t("btn_export_md", lang), size="sm")
405
- export_result = gr.Textbox(label="", interactive=False, lines=1)
 
 
406
 
407
  def generate_report(days):
408
  records = diet_logger.get_recent_records(days=int(days))
409
  analysis = nutrition_analyzer.analyze_diet(records)
410
- text_report = nutrition_analyzer.generate_report_text(analysis)
411
- chart = nutrition_analyzer.generate_report_chart(analysis)
412
- return text_report, chart
413
 
414
- def export_report(days):
415
- records = diet_logger.get_recent_records(days=int(days))
416
- analysis = nutrition_analyzer.analyze_diet(records)
417
- md_path = nutrition_analyzer.export_report_markdown(analysis)
418
- prefix = "导出成功" if lang == "zh" else "Exported"
419
- return f"{prefix}: {md_path.name}"
420
-
421
- generate_btn.click(fn=generate_report, inputs=[analysis_days], outputs=[report_text, report_plot])
422
- export_md_btn.click(fn=export_report, inputs=[analysis_days], outputs=export_result)
423
 
424
 
425
  # ============================================================
@@ -429,12 +346,6 @@ def _report_content(lang):
429
  def create_app(loop=None):
430
  """
431
  创建主应用(4 Tab + 中英文切换 + 自定义 CSS)。
432
-
433
- 设计要点:
434
- - 只使用 **一个** @gr.render 包裹所有 Tab,避免多个 @gr.render
435
- 并发触发导致 RuntimeError(dictionary changed size during iteration)
436
- - 每个 Tab 的标题通过 t() 函数实现中英文切换
437
- - theme / css / head 传入 demo.launch() 以兼容 Gradio 6.0
438
  """
439
  with gr.Blocks(
440
  title="PregoPal - 孕期陪护AI助手",
 
1
  """
2
+ PregoPal - Gradio 界面构建(现代UI版)
3
+ ======================================
4
+ 4 个子页面 + 中英文切换:
5
  1. 🏠 首页(语音启动按钮 + AI 思考 + 简报卡片 + 最近记录)
6
+ 2. 👨‍👩‍👧‍👦 家庭饮食习惯(纯展示卡片)
7
  3. 📊 三天总结
8
+ 4. 📈 营养报告(纯 HTML Dashboard)
9
 
10
  语言切换设计:
11
  用唯一一个 @gr.render(inputs=[lang_state]) 包裹全部 4 个 Tab,
 
22
  from modules.family_manager import RecipeManager, PreferenceManager, MemoryManager
23
  from modules.diet_extractor import DietExtractor
24
  from modules.nutrition_standards import DRIsParser
25
+ from utils import (
26
+ t, ZH, EN, get_home_cards, CUSTOM_CSS,
27
+ render_family_recipes_html,
28
+ render_family_preferences_html,
29
+ render_family_memories_html,
30
+ render_nutrition_report_html,
31
+ )
32
 
33
  # ============================================================
34
  # 全局实例(单例)
 
179
  with gr.Column(scale=2):
180
  gr.HTML(f"""
181
  <div style="text-align: center; padding: 24px 0;">
182
+ <button class="voice-main-btn"></button>
 
 
 
 
 
 
 
 
 
 
 
183
  <p style="margin-top: 8px; color: #999; font-size: 14px;">{T['tap_hint']}</p>
184
  </div>
185
  """)
 
228
 
229
 
230
  # ============================================================
231
+ # Tab 2: 👨‍👩‍👧‍👦 家庭饮食习惯(纯展示,3 张现代卡片)
232
  # ============================================================
233
  def _family_content(lang):
234
+ """家庭饮食习惯组件 — 纯展示模式"""
235
+ # Refresh state to force re-render
236
+ with gr.Column(elem_classes=["glass-card"]):
237
+ gr.Markdown(f"## {t('family_title', lang)}")
238
+ gr.Markdown(f"*{t('family_subtitle', lang)}*")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
+ with gr.Tabs():
241
  with gr.Tab(t("tab_recipes", lang)):
242
+ recipes_html = gr.HTML(render_family_recipes_html(lang))
243
+ refresh_recipe_btn = gr.Button(t("btn_refresh", lang), size="sm")
244
+ refresh_recipe_btn.click(
245
+ fn=lambda: render_family_recipes_html(lang),
246
+ outputs=recipes_html,
247
+ )
248
+
249
+ with gr.Tab(t("tab_preferences", lang)):
250
+ prefs_html = gr.HTML(render_family_preferences_html(lang))
251
+ refresh_pref_btn = gr.Button(t("btn_refresh", lang), size="sm")
252
+ refresh_pref_btn.click(
253
+ fn=lambda: render_family_preferences_html(lang),
254
+ outputs=prefs_html,
255
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
 
257
  with gr.Tab(t("tab_memory", lang)):
258
+ mem_html = gr.HTML(render_family_memories_html(lang))
259
+ refresh_mem_btn = gr.Button(t("btn_refresh", lang), size="sm")
260
+ refresh_mem_btn.click(
261
+ fn=lambda: render_family_memories_html(lang),
262
+ outputs=mem_html,
263
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
 
265
 
266
  # ============================================================
 
313
 
314
 
315
  # ============================================================
316
+ # Tab 4: 📈 营养报告(纯 HTML Dashboard,无 matplotlib 图片)
317
  # ============================================================
318
  def _report_content(lang):
319
+ """营养报告组件 — 纯 HTML 可视化"""
320
  with gr.Row():
321
  with gr.Column(scale=1):
322
+ analysis_days = gr.Slider(
323
+ label=t("analysis_days", lang),
324
+ minimum=1, maximum=30, value=7, step=1,
325
+ )
326
  generate_btn = gr.Button(t("btn_generate", lang), variant="primary")
 
 
 
 
 
327
 
328
+ with gr.Column(scale=3):
329
+ report_dashboard = gr.HTML(
330
+ f'<div style="padding:40px;text-align:center;color:#bbb;font-size:16px;">'
331
+ f'点击左侧「生成报告」按钮查看分析</div>'
332
+ )
333
 
334
  def generate_report(days):
335
  records = diet_logger.get_recent_records(days=int(days))
336
  analysis = nutrition_analyzer.analyze_diet(records)
337
+ return render_nutrition_report_html(analysis, days=int(days), lang=lang)
 
 
338
 
339
+ generate_btn.click(fn=generate_report, inputs=[analysis_days], outputs=report_dashboard)
 
 
 
 
 
 
 
 
340
 
341
 
342
  # ============================================================
 
346
  def create_app(loop=None):
347
  """
348
  创建主应用(4 Tab + 中英文切换 + 自定义 CSS)。
 
 
 
 
 
 
349
  """
350
  with gr.Blocks(
351
  title="PregoPal - 孕期陪护AI助手",
utils.py CHANGED
@@ -17,20 +17,10 @@ from pathlib import Path
17
 
18
  def setup_chinese_font():
19
  """尝试设置中文字体,返回使用的字体名称"""
20
- # 优先使用 Windows 系统字体
21
  font_candidates = [
22
- 'SimHei', # 黑体(Windows 自带)
23
- 'Microsoft YaHei', # 微软雅黑
24
- 'SimSun', # 宋体
25
- 'KaiTi', # 楷体
26
- 'FangSong', # 仿宋
27
- 'WenQuanYi Micro Hei',
28
- 'Noto Sans CJK SC',
29
- 'Noto Sans SC',
30
- 'Source Han Sans SC',
31
- 'PingFang SC',
32
- 'Hiragino Sans GB',
33
- 'STHeiti',
34
  ]
35
  available = [f.name for f in fm.fontManager.ttflist]
36
  for font in font_candidates:
@@ -38,8 +28,6 @@ def setup_chinese_font():
38
  plt.rcParams['font.sans-serif'] = [font, 'DejaVu Sans']
39
  plt.rcParams['axes.unicode_minus'] = False
40
  return font
41
-
42
- # fallback: 尝试直接加载系统字体文件
43
  system_fonts = [
44
  Path("C:/Windows/Fonts/simhei.ttf"),
45
  Path("C:/Windows/Fonts/msyh.ttc"),
@@ -55,8 +43,6 @@ def setup_chinese_font():
55
  return font_name
56
  except Exception:
57
  continue
58
-
59
- # 最终 fallback
60
  plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
61
  plt.rcParams['axes.unicode_minus'] = False
62
  return None
@@ -66,19 +52,13 @@ def setup_chinese_font():
66
  # 国际化(i18n)
67
  # ============================================================
68
 
69
- # 中文字典
70
  ZH = {
71
- # 应用标题
72
  "app_title": "🌸 PregoPal - 孕期陪护AI助手",
73
  "app_subtitle": "一个温馨的家庭式孕期AI伴侣",
74
-
75
- # Tab 名称(后缀,用于 @gr.render 拼接)
76
  "tab_home_suffix": "首页",
77
  "tab_family_suffix": "家庭饮食习惯",
78
  "tab_summary_suffix": "三天总结",
79
  "tab_report_suffix": "营养报告",
80
-
81
- # 语音交互页
82
  "chat_label": "💬 对话",
83
  "msg_placeholder": "例如:我今天中午吃了番茄牛腩",
84
  "btn_send": "发送",
@@ -92,66 +72,45 @@ ZH = {
92
  "btn_register": "📝 注册",
93
  "btn_identify": "🔊 识别",
94
  "briefing_title": "#### 📋 今日简报",
95
-
96
- # 家庭饮食习惯页
97
  "tab_preferences": "🥗 饮食偏好",
98
  "tab_recipes": "🍳 家庭菜谱",
99
- "tab_memory": "📖 家庭记",
100
- "pref_name": "姓名",
101
- "pref_role": "身份",
102
- "pref_likes": "偏好",
103
- "pref_likes_placeholder": "喜欢吃什么?",
104
- "pref_avoid": "忌口",
105
- "pref_avoid_placeholder": "不吃什么?",
106
- "pref_allergy": "过敏",
107
- "pref_allergy_placeholder": "对什么过敏?",
108
- "btn_save": "💾 保存",
109
  "btn_refresh": "🔄 刷新",
110
- "recipe_name": "菜名",
111
- "recipe_name_placeholder": "番茄牛腩",
112
- "recipe_cook": "制作人",
113
- "recipe_cook_placeholder": "谁会做?",
114
- "recipe_diff": "难度",
115
- "recipe_diff_choices": ["简单", "中等", "困难"],
116
- "recipe_ingredients": "食材",
117
- "recipe_ingredients_placeholder": "主要食材",
118
- "btn_add": "💾 添加",
119
- "memory_type": "类型",
120
- "memory_type_choices": ["关系", "事件", "日常"],
121
- "memory_content": "内容",
122
- "memory_content_placeholder": "记录家庭关系、重要事件或日常点滴",
123
- "btn_save_memory": "💾 保存",
124
-
125
- # 三天总结页
126
  "trimester_label": "孕期阶段",
127
  "trimester_choices": ["孕早期", "孕中期", "孕晚期"],
128
  "btn_analyze": "📊 运行分析",
129
  "analysis_result": "📋 分析结果",
130
  "menu_suggestion": "💡 菜单改进建议",
131
-
132
- # 营养报告页
133
  "analysis_days": "分析天数",
134
  "btn_generate": "📊 生成报告",
135
  "report_text": "📋 文本报告",
136
- "report_chart": "📊 可视化图表",
137
- "btn_export_md": "📥 导出 Markdown",
138
-
139
- # 通用
140
  "lang_label": "🌐 语言",
141
  "lang_zh": "中文",
142
  "lang_en": "English",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  }
144
 
145
- # 英文字典
146
  EN = {
147
  "app_title": "🌸 PregoPal - Pregnancy Companion AI",
148
  "app_subtitle": "A cozy home-oriented AI companion for expectant mothers",
149
-
150
  "tab_home_suffix": "Home",
151
  "tab_family_suffix": "Family Diet",
152
  "tab_summary_suffix": "3-Day Summary",
153
  "tab_report_suffix": "Nutrition Report",
154
-
155
  "chat_label": "💬 Chat",
156
  "msg_placeholder": "e.g. I had tomato beef brisket for lunch today",
157
  "btn_send": "Send",
@@ -165,50 +124,36 @@ EN = {
165
  "btn_register": "📝 Register",
166
  "btn_identify": "🔊 Identify",
167
  "briefing_title": "#### 📋 Today's Briefing",
168
-
169
  "tab_preferences": "🥗 Preferences",
170
  "tab_recipes": "🍳 Recipes",
171
- "tab_memory": "📖 Family Memory",
172
- "pref_name": "Name",
173
- "pref_role": "Role",
174
- "pref_likes": "Likes",
175
- "pref_likes_placeholder": "What do they like?",
176
- "pref_avoid": "Dislikes",
177
- "pref_avoid_placeholder": "What don't they eat?",
178
- "pref_allergy": "Allergies",
179
- "pref_allergy_placeholder": "Any allergies?",
180
- "btn_save": "💾 Save",
181
  "btn_refresh": "🔄 Refresh",
182
- "recipe_name": "Dish Name",
183
- "recipe_name_placeholder": "Tomato Beef Brisket",
184
- "recipe_cook": "Cook",
185
- "recipe_cook_placeholder": "Who makes it?",
186
- "recipe_diff": "Difficulty",
187
- "recipe_diff_choices": ["Easy", "Medium", "Hard"],
188
- "recipe_ingredients": "Ingredients",
189
- "recipe_ingredients_placeholder": "Main ingredients",
190
- "btn_add": "💾 Add",
191
- "memory_type": "Type",
192
- "memory_type_choices": ["Relation", "Event", "Daily"],
193
- "memory_content": "Content",
194
- "memory_content_placeholder": "Record family relations, events or daily moments",
195
- "btn_save_memory": "💾 Save",
196
-
197
  "trimester_label": "Trimester",
198
  "trimester_choices": ["1st Trimester", "2nd Trimester", "3rd Trimester"],
199
  "btn_analyze": "📊 Analyze",
200
  "analysis_result": "📋 Analysis Result",
201
  "menu_suggestion": "💡 Menu Suggestions",
202
-
203
  "analysis_days": "Analysis Days",
204
  "btn_generate": "📊 Generate Report",
205
  "report_text": "📋 Text Report",
206
- "report_chart": "📊 Chart",
207
- "btn_export_md": "📥 Export Markdown",
208
-
209
  "lang_label": "🌐 Language",
210
  "lang_zh": "中文",
211
  "lang_en": "English",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  }
213
 
214
 
@@ -225,33 +170,20 @@ def t(key: str, lang: str = "zh") -> str:
225
  def get_home_cards(loop=None):
226
  """从 Loop 上下文提取首页卡片数据"""
227
  briefing = loop.get_briefing() if loop else {}
228
-
229
- # 孕期阶段
230
  trimester = briefing.get("trimester", "孕中期")
231
-
232
- # 最近营养关注点
233
  focus_nutrients = briefing.get("dri_analysis", {}).get("focus_nutrients", [])
234
  recommended_foods = briefing.get("recommended_foods", [])
235
-
236
- # 今日推荐菜品(取前3)
237
  from modules.family_manager import RecipeManager
238
  all_recipes = RecipeManager.load_all()
239
  recipe_count = len(all_recipes)
240
  recipe_names = [r.get("name", "") for r in all_recipes[:3]]
241
-
242
- # 昨日饮食摘要
243
  yesterday = briefing.get("yesterday_diet", {})
244
  yesterday_summary = yesterday.get("summary", "暂无记录")
245
  meal_count = yesterday.get("meal_count", 0)
246
-
247
- # 体重评估
248
  weight_eval = briefing.get("weight_evaluation", {})
249
  weight_status = weight_eval.get("status", "暂无数据")
250
  weight_trend = weight_eval.get("trend", "")
251
-
252
- # AI 思考关键词
253
  thinking_keywords = briefing.get("thinking_keywords", "")
254
-
255
  return {
256
  "trimester": trimester,
257
  "focus_nutrients": focus_nutrients,
@@ -266,29 +198,333 @@ def get_home_cards(loop=None):
266
  }
267
 
268
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  # ============================================================
270
  # 自定义 CSS 样式
271
  # ============================================================
272
 
273
  CUSTOM_CSS = """
274
- /* ============ 全局圆角与阴影 ============ */
 
 
 
 
275
  .gradio-container {
276
  max-width: 1200px !important;
277
  margin: 0 auto !important;
 
 
 
 
 
 
 
 
278
  }
279
 
280
- /* 卡片样式 */
281
- .card-container {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  border-radius: 16px !important;
283
- padding: 16px !important;
284
- background: linear-gradient(135deg, #FFF5F7 0%, #FFFFFF 100%) !important;
285
- border: 1px solid #FCE4EC !important;
286
- box-shadow: 0 2px 12px rgba(255, 105, 135, 0.08) !important;
287
- transition: all 0.3s ease;
288
  }
289
- .card-container:hover {
290
- box-shadow: 0 4px 20px rgba(255, 105, 135, 0.15) !important;
291
- transform: translateY(-1px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
 
294
  /* 大圆形按钮(语音启动) */
@@ -301,8 +537,11 @@ CUSTOM_CSS = """
301
  box-shadow: 0 8px 32px rgba(255, 64, 129, 0.3) !important;
302
  cursor: pointer !important;
303
  transition: all 0.3s ease !important;
304
- font-size: 20px !important;
305
  color: white !important;
 
 
 
306
  }
307
  .voice-main-btn:hover {
308
  transform: scale(1.05) !important;
@@ -312,83 +551,29 @@ CUSTOM_CSS = """
312
  transform: scale(0.95) !important;
313
  }
314
 
315
- /* 状态指示灯 */
316
- .status-dot {
317
- width: 12px;
318
- height: 12px;
319
- border-radius: 50%;
320
- display: inline-block;
321
- margin-right: 8px;
322
- }
323
- .status-dot.active {
324
- background: #4CAF50;
325
- animation: pulse 1.5s infinite;
326
- }
327
- .status-dot.idle {
328
- background: #BDBDBD;
329
- }
330
- @keyframes pulse {
331
- 0%, 100% { opacity: 1; }
332
- 50% { opacity: 0.4; }
333
- }
334
-
335
- /* 聊天气泡 */
336
- .chat-bubble {
337
- border-radius: 18px !important;
338
- padding: 12px 16px !important;
339
- margin: 8px 0 !important;
340
- max-width: 85% !important;
341
- }
342
- .chat-bubble.user {
343
- background: #FCE4EC !important;
344
- margin-left: auto !important;
345
- }
346
- .chat-bubble.ai {
347
- background: #E8F5E9 !important;
348
- margin-right: auto !important;
349
- }
350
-
351
- /* Tab 按钮美化 */
352
- .tabs > .tab-nav > button {
353
- border-radius: 12px 12px 0 0 !important;
354
- font-size: 15px !important;
355
- padding: 12px 20px !important;
356
- transition: all 0.2s ease !important;
357
- }
358
- .tabs > .tab-nav > button.selected {
359
- background: linear-gradient(135deg, #FCE4EC, #FFF) !important;
360
- font-weight: 600 !important;
361
- }
362
-
363
- /* 按钮统一圆角 */
364
  button, .gr-button {
365
  border-radius: 12px !important;
366
  font-weight: 500 !important;
 
367
  }
368
-
369
- /* 输入框圆角 */
370
  input, textarea, .gr-textbox {
371
  border-radius: 12px !important;
372
  }
373
 
374
- /* 折叠面板 */
375
- .accordion {
376
- border-radius: 16px !important;
377
- border: 1px solid #FCE4EC !important;
378
- }
379
- .accordion > .label-wrap {
380
- border-radius: 16px 16px 0 0 !important;
381
- }
382
-
383
  /* 响应式调整 */
384
  @media (max-width: 768px) {
385
  .voice-main-btn {
386
  width: 120px !important;
387
  height: 120px !important;
388
- font-size: 16px !important;
389
  }
390
  .gradio-container {
391
- padding: 8px !important;
 
 
 
 
392
  }
393
  }
394
- """
 
17
 
18
  def setup_chinese_font():
19
  """尝试设置中文字体,返回使用的字体名称"""
 
20
  font_candidates = [
21
+ 'SimHei', 'Microsoft YaHei', 'SimSun', 'KaiTi', 'FangSong',
22
+ 'WenQuanYi Micro Hei', 'Noto Sans CJK SC', 'Noto Sans SC',
23
+ 'Source Han Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'STHeiti',
 
 
 
 
 
 
 
 
 
24
  ]
25
  available = [f.name for f in fm.fontManager.ttflist]
26
  for font in font_candidates:
 
28
  plt.rcParams['font.sans-serif'] = [font, 'DejaVu Sans']
29
  plt.rcParams['axes.unicode_minus'] = False
30
  return font
 
 
31
  system_fonts = [
32
  Path("C:/Windows/Fonts/simhei.ttf"),
33
  Path("C:/Windows/Fonts/msyh.ttc"),
 
43
  return font_name
44
  except Exception:
45
  continue
 
 
46
  plt.rcParams['font.sans-serif'] = ['DejaVu Sans']
47
  plt.rcParams['axes.unicode_minus'] = False
48
  return None
 
52
  # 国际化(i18n)
53
  # ============================================================
54
 
 
55
  ZH = {
 
56
  "app_title": "🌸 PregoPal - 孕期陪护AI助手",
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": "例如:我今天中午吃了番茄牛腩",
64
  "btn_send": "发送",
 
72
  "btn_register": "📝 注册",
73
  "btn_identify": "🔊 识别",
74
  "briefing_title": "#### 📋 今日简报",
 
 
75
  "tab_preferences": "🥗 饮食偏好",
76
  "tab_recipes": "🍳 家庭菜谱",
77
+ "tab_memory": "📝 家庭记",
 
 
 
 
 
 
 
 
 
78
  "btn_refresh": "🔄 刷新",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  "trimester_label": "孕期阶段",
80
  "trimester_choices": ["孕早期", "孕中期", "孕晚期"],
81
  "btn_analyze": "📊 运行分析",
82
  "analysis_result": "📋 分析结果",
83
  "menu_suggestion": "💡 菜单改进建议",
 
 
84
  "analysis_days": "分析天数",
85
  "btn_generate": "📊 生成报告",
86
  "report_text": "📋 文本报告",
 
 
 
 
87
  "lang_label": "🌐 语言",
88
  "lang_zh": "中文",
89
  "lang_en": "English",
90
+ "family_title": "👨‍👩‍👧‍👦 家庭饮食档案",
91
+ "family_subtitle": "AI 自动记录的家庭饮食习惯",
92
+ "no_recipes": "暂无菜谱记录 — AI 将在对话中自动学习",
93
+ "no_preferences": "暂无饮食偏好记录 — AI 将在对话中自动学习",
94
+ "no_memories": "暂无家庭记忆 — AI 将在对话中自动学习",
95
+ "report_title": "📈 营养分析报告",
96
+ "report_subtitle": "基于近期饮食数据的 AI 分析",
97
+ "report_overall": "综合营养评分",
98
+ "report_good": "良好",
99
+ "report_need_attention": "需关注",
100
+ "report_meal_completion": "餐次完成率",
101
+ "report_nutrient_coverage": "关键营养覆盖",
102
+ "report_diversity": "饮食多样性",
103
+ "report_suggestions": "饮食建议",
104
+ "report_days": "近{days}天",
105
  }
106
 
 
107
  EN = {
108
  "app_title": "🌸 PregoPal - Pregnancy Companion AI",
109
  "app_subtitle": "A cozy home-oriented AI companion for expectant mothers",
 
110
  "tab_home_suffix": "Home",
111
  "tab_family_suffix": "Family Diet",
112
  "tab_summary_suffix": "3-Day Summary",
113
  "tab_report_suffix": "Nutrition Report",
 
114
  "chat_label": "💬 Chat",
115
  "msg_placeholder": "e.g. I had tomato beef brisket for lunch today",
116
  "btn_send": "Send",
 
124
  "btn_register": "📝 Register",
125
  "btn_identify": "🔊 Identify",
126
  "briefing_title": "#### 📋 Today's Briefing",
 
127
  "tab_preferences": "🥗 Preferences",
128
  "tab_recipes": "🍳 Recipes",
129
+ "tab_memory": "📝 Memories",
 
 
 
 
 
 
 
 
 
130
  "btn_refresh": "🔄 Refresh",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  "trimester_label": "Trimester",
132
  "trimester_choices": ["1st Trimester", "2nd Trimester", "3rd Trimester"],
133
  "btn_analyze": "📊 Analyze",
134
  "analysis_result": "📋 Analysis Result",
135
  "menu_suggestion": "💡 Menu Suggestions",
 
136
  "analysis_days": "Analysis Days",
137
  "btn_generate": "📊 Generate Report",
138
  "report_text": "📋 Text Report",
 
 
 
139
  "lang_label": "🌐 Language",
140
  "lang_zh": "中文",
141
  "lang_en": "English",
142
+ "family_title": "👨‍👩‍👧‍👦 Family Diet Profile",
143
+ "family_subtitle": "AI-automated family diet memory",
144
+ "no_recipes": "No recipes yet — AI will learn from conversations",
145
+ "no_preferences": "No preferences yet — AI will learn from conversations",
146
+ "no_memories": "No memories yet — AI will learn from conversations",
147
+ "report_title": "📈 Nutrition Report",
148
+ "report_subtitle": "AI analysis based on recent diet data",
149
+ "report_overall": "Overall Score",
150
+ "report_good": "Good",
151
+ "report_need_attention": "Needs Attention",
152
+ "report_meal_completion": "Meal Completion",
153
+ "report_nutrient_coverage": "Key Nutrients",
154
+ "report_diversity": "Diet Diversity",
155
+ "report_suggestions": "Dietary Suggestions",
156
+ "report_days": "Last {days} days",
157
  }
158
 
159
 
 
170
  def get_home_cards(loop=None):
171
  """从 Loop 上下文提取首页卡片数据"""
172
  briefing = loop.get_briefing() if loop else {}
 
 
173
  trimester = briefing.get("trimester", "孕中期")
 
 
174
  focus_nutrients = briefing.get("dri_analysis", {}).get("focus_nutrients", [])
175
  recommended_foods = briefing.get("recommended_foods", [])
 
 
176
  from modules.family_manager import RecipeManager
177
  all_recipes = RecipeManager.load_all()
178
  recipe_count = len(all_recipes)
179
  recipe_names = [r.get("name", "") for r in all_recipes[:3]]
 
 
180
  yesterday = briefing.get("yesterday_diet", {})
181
  yesterday_summary = yesterday.get("summary", "暂无记录")
182
  meal_count = yesterday.get("meal_count", 0)
 
 
183
  weight_eval = briefing.get("weight_evaluation", {})
184
  weight_status = weight_eval.get("status", "暂无数据")
185
  weight_trend = weight_eval.get("trend", "")
 
 
186
  thinking_keywords = briefing.get("thinking_keywords", "")
 
187
  return {
188
  "trimester": trimester,
189
  "focus_nutrients": focus_nutrients,
 
198
  }
199
 
200
 
201
+ # ============================================================
202
+ # Helper: 家庭数据 → HTML 卡片
203
+ # ============================================================
204
+
205
+ def render_family_recipes_html(lang="zh") -> str:
206
+ """渲染家庭菜谱为现代卡片"""
207
+ from modules.family_manager import RecipeManager
208
+ recipes = RecipeManager.load_all()
209
+ if not recipes:
210
+ no = t("no_recipes", lang)
211
+ return f'<div style="padding:24px;text-align:center;color:#aaa;font-size:15px;">{no}</div>'
212
+ cards = ""
213
+ for r in recipes:
214
+ name = r.get("name", "")
215
+ cook = r.get("cook", "")
216
+ diff = r.get("difficulty", "")
217
+ diff_stars = {"��单": "⭐", "中等": "⭐⭐", "困难": "⭐⭐⭐", "Easy": "⭐", "Medium": "⭐⭐", "Hard": "⭐⭐⭐"}
218
+ stars = diff_stars.get(diff, "")
219
+ cards += f"""
220
+ <div style="background:#fff;border-radius:14px;padding:14px 16px;box-shadow:0 1px 6px rgba(0,0,0,0.04);display:flex;flex-direction:column;gap:4px;border:1px solid #f0f0f0;transition:all 0.2s;">
221
+ <div style="font-weight:600;font-size:15px;color:#E91E63;">{name}</div>
222
+ <div style="font-size:13px;color:#888;">
223
+ <span>👨‍🍳 {cook}</span>
224
+ <span style="margin-left:12px;">{stars}</span>
225
+ </div>
226
+ </div>"""
227
+ return f"""
228
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:10px;">
229
+ {cards}
230
+ </div>"""
231
+
232
+
233
+ def render_family_preferences_html(lang="zh") -> str:
234
+ """渲染饮食偏好为现代卡片"""
235
+ from modules.family_manager import PreferenceManager
236
+ members = PreferenceManager.load_all()
237
+ if not members:
238
+ no = t("no_preferences", lang)
239
+ return f'<div style="padding:24px;text-align:center;color:#aaa;font-size:15px;">{no}</div>'
240
+ cards = ""
241
+ for m in members:
242
+ name = m.get("name", "")
243
+ role = m.get("role", "")
244
+ pref = m.get("preferences", "")
245
+ avoid = m.get("avoid", "")
246
+ allergies = m.get("allergies", "")
247
+ tags = []
248
+ if pref: tags.append(f'<span style="background:#E8F5E9;color:#2E7D32;padding:2px 10px;border-radius:99px;font-size:12px;">✅ {pref}</span>')
249
+ if avoid: tags.append(f'<span style="background:#FFF3E0;color:#E65100;padding:2px 10px;border-radius:99px;font-size:12px;">❌ {avoid}</span>')
250
+ if allergies: tags.append(f'<span style="background:#FFEBEE;color:#C62828;padding:2px 10px;border-radius:99px;font-size:12px;">⚠ {allergies}</span>')
251
+ if not tags: tags.append(f'<span style="color:#bbb;font-size:12px;">暂无记录</span>')
252
+ role_icons = {"孕妇":"🤰","丈夫":"👨","婆婆":"👩","妈妈":"👩","爸爸":"👨","其他家人":"👤",
253
+ "Pregnant":"🤰","Husband":"👨","Mother-in-law":"👩","Mom":"👩","Dad":"👨","Other":"👤"}
254
+ icon = role_icons.get(role, "👤")
255
+ role_str = f" {role}" if role else ""
256
+ cards += f"""
257
+ <div style="background:#fff;border-radius:14px;padding:16px;box-shadow:0 1px 6px rgba(0,0,0,0.04);border:1px solid #f0f0f0;">
258
+ <div style="font-weight:600;font-size:16px;margin-bottom:8px;">
259
+ {icon}<span style="margin-left:4px;">{name}{role_str}</span>
260
+ </div>
261
+ <div style="display:flex;flex-wrap:wrap;gap:6px;">
262
+ {"".join(tags)}
263
+ </div>
264
+ </div>"""
265
+ return f"""
266
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px;">
267
+ {cards}
268
+ </div>"""
269
+
270
+
271
+ def render_family_memories_html(lang="zh") -> str:
272
+ """渲染家庭记忆为时间线"""
273
+ from modules.family_manager import MemoryManager
274
+ memories = MemoryManager.load_all()
275
+ if not memories:
276
+ no = t("no_memories", lang)
277
+ return f'<div style="padding:24px;text-align:center;color:#aaa;font-size:15px;">{no}</div>'
278
+ items = ""
279
+ for m in memories[-10:]:
280
+ date = m.get("date", "")[-5:] if m.get("date") else ""
281
+ content = m.get("content", "") or m.get("title", "") or m.get("event", "")
282
+ if not content: continue
283
+ items += f"""
284
+ <div style="display:flex;gap:12px;padding:8px 0;">
285
+ <div style="min-width:50px;font-size:13px;color:#E91E63;font-weight:500;">{date}</div>
286
+ <div style="flex:1;background:#fff;border-radius:10px;padding:10px 14px;border:1px solid #f0f0f0;font-size:14px;color:#333;">{content}</div>
287
+ </div>"""
288
+ return f"""
289
+ <div style="max-height:320px;overflow-y:auto;padding:4px 0;">
290
+ {items}
291
+ </div>"""
292
+
293
+
294
+ def render_nutrition_report_html(analysis: dict, days: int, lang="zh") -> str:
295
+ """渲染营养报告为现代 Dashboard HTML"""
296
+ if not analysis or "error" in analysis:
297
+ return f'<div style="padding:40px;text-align:center;color:#aaa;font-size:16px;">{analysis.get("error","暂无数据")}</div>'
298
+
299
+ score = analysis.get("score", 0)
300
+ meal_counts = analysis.get("meal_counts", {})
301
+ total_days = analysis.get("total_days", 0)
302
+ nutrition_coverage = analysis.get("nutrition_coverage", {})
303
+ suggestions = analysis.get("suggestions", [])
304
+ food_items = analysis.get("food_items", [])
305
+ diversity_score = analysis.get("diversity_score", 0)
306
+
307
+ # Score bar color
308
+ bar_color = "#4CAF50" if score >= 70 else "#FF9800" if score >= 50 else "#F44336"
309
+ bar_status = t("report_good", lang) if score >= 70 else t("report_need_attention", lang)
310
+
311
+ # Meal completion bars
312
+ meal_labels = {"早餐":"🌅", "午餐":"☀️", "晚餐":"🌙", "加餐":"🍪"}
313
+ meal_rows = ""
314
+ for mt, label in {"早餐":"Breakfast","午餐":"Lunch","晚餐":"Dinner","��餐":"Snack"}.items():
315
+ cnt = meal_counts.get(mt, 0)
316
+ pct = min(100, round(cnt / max(total_days, 1) * 100))
317
+ icon = meal_labels.get(mt, "🍽️")
318
+ mcolor = "#4CAF50" if pct >= 80 else "#FF9800" if pct >= 50 else "#F44336"
319
+ meal_rows += f"""
320
+ <div style="margin-bottom:10px;">
321
+ <div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:3px;">
322
+ <span>{icon} {mt}</span><span style="font-weight:600;">{pct}%</span>
323
+ </div>
324
+ <div style="background:#f0f0f0;border-radius:99px;height:8px;overflow:hidden;">
325
+ <div style="width:{pct}%;background:{mcolor};height:100%;border-radius:99px;transition:width 0.6s;"></div>
326
+ </div>
327
+ </div>"""
328
+
329
+ # Nutrient coverage
330
+ n_rows = ""
331
+ for nut, info in nutrition_coverage.items():
332
+ pct = info.get("percentage", 0) if isinstance(info, dict) else info
333
+ try: pct = min(100, int(pct))
334
+ except: pct = 0
335
+ ncolor = "#4CAF50" if pct >= 70 else "#FF9800" if pct >= 40 else "#F44336"
336
+ n_rows += f"""
337
+ <div style="margin-bottom:8px;">
338
+ <div style="display:flex;justify-content:space-between;font-size:13px;margin-bottom:2px;">
339
+ <span>{nut}</span><span style="font-weight:600;">{pct}%</span>
340
+ </div>
341
+ <div style="background:#f0f0f0;border-radius:99px;height:6px;overflow:hidden;">
342
+ <div style="width:{pct}%;background:{ncolor};height:100%;border-radius:99px;"></div>
343
+ </div>
344
+ </div>"""
345
+
346
+ # Suggestions
347
+ sug_items = ""
348
+ for s in suggestions[:5]:
349
+ color = "#F44336" if "⚠" in s or "不足" in s or "不够" in s or "缺乏" in s else "#FF9800"
350
+ sug_items += f'<div style="padding:6px 0;font-size:13px;color:{color};">• {s}</div>'
351
+
352
+ # Diversity
353
+ div_color = "#4CAF50" if diversity_score >= 60 else "#FF9800" if diversity_score >= 30 else "#F44336"
354
+ food_count = len(food_items)
355
+ food_str = "、".join(food_items[:8])
356
+ if len(food_items) > 8: food_str += "⋯"
357
+
358
+ days_str = t("report_days", lang).format(days=days)
359
+
360
+ return f"""
361
+ <div style="font-family:system-ui,-apple-system,sans-serif;max-width:100%;">
362
+ <!-- Header -->
363
+ <div style="background:linear-gradient(135deg,#FCE4EC,#FFF0F2);border-radius:18px;padding:20px 24px;margin-bottom:16px;">
364
+ <div style="display:flex;justify-content:space-between;align-items:center;">
365
+ <div>
366
+ <div style="font-size:20px;font-weight:700;color:#880E4F;">📊 {t("report_overall", lang)}</div>
367
+ <div style="color:#888;font-size:13px;margin-top:4px;">{days_str} · {total_days}t("report_days","").split("近")[0] if lang=="zh" else ""</div>
368
+ </div>
369
+ <div style="text-align:right;">
370
+ <div style="font-size:36px;font-weight:800;color:{bar_color};">{score}</div>
371
+ <div style="font-size:14px;color:#555;">/ 100</div>
372
+ </div>
373
+ </div>
374
+ <div style="background:#f0f0f0;border-radius:99px;height:10px;margin-top:12px;overflow:hidden;">
375
+ <div style="width:{min(100,score)}%;background:linear-gradient(90deg,#F44336,#FF9800,#4CAF50);height:100%;border-radius:99px;transition:width 0.8s;"></div>
376
+ </div>
377
+ <div style="margin-top:6px;font-size:14px;color:#555;text-align:center;">{bar_status}</div>
378
+ </div>
379
+
380
+ <!-- 3 cards row -->
381
+ <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:16px;">
382
+ <!-- Meal Completion -->
383
+ <div style="background:#fff;border-radius:14px;padding:14px 16px;box-shadow:0 1px 6px rgba(0,0,0,0.04);border:1px solid #f0f0f0;">
384
+ <div style="font-weight:600;font-size:14px;margin-bottom:10px;color:#333;">🍽️ {t("report_meal_completion", lang)}</div>
385
+ {meal_rows}
386
+ </div>
387
+ <!-- Nutrient Coverage -->
388
+ <div style="background:#fff;border-radius:14px;padding:14px 16px;box-shadow:0 1px 6px rgba(0,0,0,0.04);border:1px solid #f0f0f0;">
389
+ <div style="font-weight:600;font-size:14px;margin-bottom:10px;color:#333;">🥗 {t("report_nutrient_coverage", lang)}</div>
390
+ {n_rows if n_rows else '<div style="color:#bbb;font-size:13px;">暂无数据</div>'}
391
+ </div>
392
+ <!-- Diversity -->
393
+ <div style="background:#fff;border-radius:14px;padding:14px 16px;box-shadow:0 1px 6px rgba(0,0,0,0.04);border:1px solid #f0f0f0;">
394
+ <div style="font-weight:600;font-size:14px;margin-bottom:10px;color:#333;">🌿 {t("report_diversity", lang)}</div>
395
+ <div style="text-align:center;padding:8px 0;">
396
+ <div style="font-size:32px;font-weight:700;color:{div_color};">{diversity_score}</div>
397
+ <div style="font-size:13px;color:#888;">/ 100</div>
398
+ </div>
399
+ <div style="background:#f0f0f0;border-radius:99px;height:6px;overflow:hidden;margin:6px 0;">
400
+ <div style="width:{min(100,diversity_score)}%;background:{div_color};height:100%;border-radius:99px;"></div>
401
+ </div>
402
+ <div style="margin-top:8px;font-size:12px;color:#666;">
403
+ <span>{food_count} 种食材</span>
404
+ <div style="font-size:12px;color:#999;margin-top:2px;">{food_str}</div>
405
+ </div>
406
+ </div>
407
+ </div>
408
+
409
+ <!-- Suggestions -->
410
+ <div style="background:linear-gradient(135deg,#FFF8E1,#FFFDE7);border-radius:14px;padding:14px 18px;border:1px solid #FFE082;">
411
+ <div style="font-weight:600;font-size:14px;margin-bottom:6px;color:#E65100;">💡 {t("report_suggestions", lang)}</div>
412
+ {sug_items if sug_items else '<div style="font-size:13px;color:#888;">暂无建议</div>'}
413
+ </div>
414
+ </div>"""
415
+
416
+
417
  # ============================================================
418
  # 自定义 CSS 样式
419
  # ============================================================
420
 
421
  CUSTOM_CSS = """
422
+ /* =====================
423
+ PregoPal Modern UI v2
424
+ ===================== */
425
+
426
+ /* 全局容器 */
427
  .gradio-container {
428
  max-width: 1200px !important;
429
  margin: 0 auto !important;
430
+ padding: 4px 16px !important;
431
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, sans-serif !important;
432
+ }
433
+
434
+ /* 顶部标题禁止选中 */
435
+ h1, h2, h3 {
436
+ -webkit-user-select: none !important;
437
+ user-select: none !important;
438
  }
439
 
440
+ /* ======== Modern Tab Bar (永不折叠) ======== */
441
+ .tabs {
442
+ display: flex !important;
443
+ flex-direction: column !important;
444
+ gap: 0 !important;
445
+ }
446
+ .tabs > .tab-nav {
447
+ display: flex !important;
448
+ flex-wrap: nowrap !important;
449
+ overflow-x: auto !important;
450
+ gap: 0 !important;
451
+ background: #f8f8fb !important;
452
+ border-radius: 16px 16px 0 0 !important;
453
+ padding: 4px 4px 0 4px !important;
454
+ -webkit-overflow-scrolling: touch !important;
455
+ }
456
+ .tabs > .tab-nav > button {
457
+ flex-shrink: 0 !important;
458
+ min-width: auto !important;
459
+ white-space: nowrap !important;
460
+ font-size: 14px !important;
461
+ font-weight: 500 !important;
462
+ padding: 10px 18px !important;
463
+ border-radius: 12px 12px 0 0 !important;
464
+ background: transparent !important;
465
+ border: none !important;
466
+ color: #888 !important;
467
+ transition: all 0.2s ease !important;
468
+ margin: 0 1px !important;
469
+ cursor: pointer !important;
470
+ }
471
+ .tabs > .tab-nav > button:hover {
472
+ background: rgba(233, 30, 99, 0.04) !important;
473
+ color: #E91E63 !important;
474
+ }
475
+ .tabs > .tab-nav > button.selected {
476
+ background: #fff !important;
477
+ color: #E91E63 !important;
478
+ font-weight: 600 !important;
479
+ box-shadow: 0 -2px 8px rgba(233, 30, 99, 0.08) !important;
480
+ }
481
+
482
+ /* ======== Cards (Glassmorphism) ======== */
483
+ .glass-card {
484
+ background: rgba(255,255,255,0.75) !important;
485
+ backdrop-filter: blur(12px) !important;
486
+ -webkit-backdrop-filter: blur(12px) !important;
487
  border-radius: 16px !important;
488
+ padding: 20px 24px !important;
489
+ border: 1px solid rgba(255,255,255,0.8) !important;
490
+ box-shadow: 0 2px 16px rgba(233, 30, 99, 0.06) !important;
491
+ transition: all 0.3s ease !important;
 
492
  }
493
+ .glass-card:hover {
494
+ box-shadow: 0 4px 24px rgba(233, 30, 99, 0.10) !important;
495
+ transform: translateY(-2px);
496
+ }
497
+
498
+ /* home cards (首页卡片) */
499
+ .home-card {
500
+ border-radius: 16px !important;
501
+ padding: 16px 20px !important;
502
+ background: #fff !important;
503
+ border: 1px solid #f0f0f0 !important;
504
+ box-shadow: 0 1px 8px rgba(0,0,0,0.04) !important;
505
+ transition: all 0.2s ease !important;
506
+ }
507
+ .home-card:hover {
508
+ box-shadow: 0 4px 20px rgba(233, 30, 99, 0.08) !important;
509
+ }
510
+ .home-card h3 {
511
+ margin: 0 0 6px 0 !important;
512
+ font-size: 16px !important;
513
+ }
514
+ .home-card p {
515
+ margin: 0 !important;
516
+ font-size: 14px !important;
517
+ color: #555 !important;
518
+ }
519
+
520
+ /* AI Thinking Box */
521
+ .thinking-box textarea {
522
+ background: linear-gradient(135deg, #F3E5F5, #FFF) !important;
523
+ border: 1px solid #E1BEE7 !important;
524
+ border-radius: 12px !important;
525
+ font-size: 14px !important;
526
+ color: #6A1B9A !important;
527
+ padding: 12px 16px !important;
528
  }
529
 
530
  /* 大圆形按钮(语音启动) */
 
537
  box-shadow: 0 8px 32px rgba(255, 64, 129, 0.3) !important;
538
  cursor: pointer !important;
539
  transition: all 0.3s ease !important;
540
+ font-size: 48px !important;
541
  color: white !important;
542
+ display: inline-flex !important;
543
+ align-items: center !important;
544
+ justify-content: center !important;
545
  }
546
  .voice-main-btn:hover {
547
  transform: scale(1.05) !important;
 
551
  transform: scale(0.95) !important;
552
  }
553
 
554
+ /* 按钮统一 */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  button, .gr-button {
556
  border-radius: 12px !important;
557
  font-weight: 500 !important;
558
+ transition: all 0.2s ease !important;
559
  }
 
 
560
  input, textarea, .gr-textbox {
561
  border-radius: 12px !important;
562
  }
563
 
 
 
 
 
 
 
 
 
 
564
  /* 响应式调整 */
565
  @media (max-width: 768px) {
566
  .voice-main-btn {
567
  width: 120px !important;
568
  height: 120px !important;
569
+ font-size: 36px !important;
570
  }
571
  .gradio-container {
572
+ padding: 4px !important;
573
+ }
574
+ .tabs > .tab-nav > button {
575
+ font-size: 12px !important;
576
+ padding: 8px 12px !important;
577
  }
578
  }
579
+ """