""" PregoPal - 工具函数 ==================== 通用工具函数,如字体设置、国际化等。 """ import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt import matplotlib.font_manager as fm from pathlib import Path # ============================================================ # 中文字体设置 # ============================================================ def setup_chinese_font(): """尝试设置中文字体,返回使用的字体名称""" font_candidates = [ 'SimHei', 'Microsoft YaHei', 'SimSun', 'KaiTi', 'FangSong', 'WenQuanYi Micro Hei', 'Noto Sans CJK SC', 'Noto Sans SC', 'Source Han Sans SC', 'PingFang SC', 'Hiragino Sans GB', 'STHeiti', ] available = [f.name for f in fm.fontManager.ttflist] for font in font_candidates: if font in available: plt.rcParams['font.sans-serif'] = [font, 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False return font system_fonts = [ Path("C:/Windows/Fonts/simhei.ttf"), Path("C:/Windows/Fonts/msyh.ttc"), Path("C:/Windows/Fonts/simsun.ttc"), ] for fp in system_fonts: if fp.exists(): try: fm.fontManager.addfont(str(fp)) font_name = fm.FontProperties(fname=str(fp)).get_name() plt.rcParams['font.sans-serif'] = [font_name, 'DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False return font_name except Exception: continue plt.rcParams['font.sans-serif'] = ['DejaVu Sans'] plt.rcParams['axes.unicode_minus'] = False return None # ============================================================ # 国际化(i18n) # ============================================================ ZH = { "app_title": "🌸 PregoPal - 孕期陪护AI助手", "app_subtitle": "一个温馨的家庭式孕期AI伴侣", "tab_home_suffix": "首页", "tab_family_suffix": "家庭饮食习惯", "tab_report_suffix": "营养报告", "chat_label": "💬 对话", "msg_placeholder": "例如:我今天中午吃了番茄牛腩", "btn_send": "发送", "btn_clear": "🗑️ 清空对话", "voice_title": "#### 🔊 声纹识别", "voice_register": "注册", "voice_identify": "识别", "member_name": "姓名", "member_relation": "身份", "relation_choices": ["孕妇", "丈夫", "婆婆", "妈妈", "爸爸", "其他家人"], "btn_register": "📝 注册", "btn_identify": "🔊 识别", "briefing_title": "#### 📋 今日简报", "tab_preferences": "🥗 饮食偏好", "tab_recipes": "🍳 家庭菜谱", "tab_memory": "📝 家庭记事", "btn_refresh": "🔄 刷新", "trimester_label": "孕期阶段", "trimester_choices": ["孕早期", "孕中期", "孕晚期"], "btn_analyze": "📊 运行分析", "analysis_result": "📋 分析结果", "menu_suggestion": "💡 菜单改进建议", "analysis_days": "分析天数", "btn_generate": "📊 生成报告", "report_text": "📋 文本报告", "lang_label": "🌐 语言", "lang_zh": "中文", "lang_en": "English", "family_title": "👨‍👩‍👧‍👦 家庭饮食档案", "family_subtitle": "AI 自动记录的家庭饮食习惯", "no_recipes": "暂无菜谱记录 — AI 将在对话中自动学习", "no_preferences": "暂无饮食偏好记录 — AI 将在对话中自动学习", "no_memories": "暂无家庭记忆 — AI 将在对话中自动学习", "report_title": "📈 营养分析报告", "report_subtitle": "基于近期饮食数据的 AI 分析", "report_overall": "综合营养评分", "report_good": "良好", "report_need_attention": "需关注", "report_meal_completion": "餐次完成率", "report_nutrient_coverage": "关键营养覆盖", "report_diversity": "饮食多样性", "report_suggestions": "饮食建议", "report_days": "近{days}天", # 三天深度分析(已合并到营养报告中) "three_day_title": "📊 三天深度营养趋势", "three_day_subtitle": "基于近三日饮食数据的持续缺失追踪", "three_day_days": "分析了 {days} 天数据", "three_day_duration": "持续不足的营养素", "three_day_suggestions": "🍽️ 食材补充建议", "three_day_summary": "📋 趋势总结", "three_day_no_data": "近三天无饮食记录,跳过深度分析", "three_day_all_good": "✅ 各营养素摄入基本达标,继续保持!", } EN = { "app_title": "🌸 PregoPal - Pregnancy Companion AI", "app_subtitle": "A cozy home-oriented AI companion for expectant mothers", "tab_home_suffix": "Home", "tab_family_suffix": "Family Diet", "tab_report_suffix": "Nutrition Report", "chat_label": "💬 Chat", "msg_placeholder": "e.g. I had tomato beef brisket for lunch today", "btn_send": "Send", "btn_clear": "🗑️ Clear", "voice_title": "#### 🔊 Voiceprint", "voice_register": "Register", "voice_identify": "Identify", "member_name": "Name", "member_relation": "Role", "relation_choices": ["Pregnant", "Husband", "Mother-in-law", "Mom", "Dad", "Other"], "btn_register": "📝 Register", "btn_identify": "🔊 Identify", "briefing_title": "#### 📋 Today's Briefing", "tab_preferences": "🥗 Preferences", "tab_recipes": "🍳 Recipes", "tab_memory": "📝 Memories", "btn_refresh": "🔄 Refresh", "trimester_label": "Trimester", "trimester_choices": ["1st Trimester", "2nd Trimester", "3rd Trimester"], "btn_analyze": "📊 Analyze", "analysis_result": "📋 Analysis Result", "menu_suggestion": "💡 Menu Suggestions", "analysis_days": "Analysis Days", "btn_generate": "📊 Generate Report", "report_text": "📋 Text Report", "lang_label": "🌐 Language", "lang_zh": "中文", "lang_en": "English", "family_title": "👨‍👩‍👧‍👦 Family Diet Profile", "family_subtitle": "AI-automated family diet memory", "no_recipes": "No recipes yet — AI will learn from conversations", "no_preferences": "No preferences yet — AI will learn from conversations", "no_memories": "No memories yet — AI will learn from conversations", "report_title": "📈 Nutrition Report", "report_subtitle": "AI analysis based on recent diet data", "report_overall": "Overall Score", "report_good": "Good", "report_need_attention": "Needs Attention", "report_meal_completion": "Meal Completion", "report_nutrient_coverage": "Key Nutrients", "report_diversity": "Diet Diversity", "report_suggestions": "Dietary Suggestions", "report_days": "Last {days} days", # 三天深度分析(合并到营养报告中) "three_day_title": "📊 3-Day Nutrient Trends", "three_day_subtitle": "Continuous deficiency tracking based on recent diet data", "three_day_days": "Analyzed {days} days of data", "three_day_duration": "Persistent Deficits", "three_day_suggestions": "🍽️ Food Suggestions", "three_day_summary": "📋 Trend Summary", "three_day_no_data": "No diet records in the last 3 days, skipping deep analysis", "three_day_all_good": "✅ All nutrients adequate, keep it up!", } def t(key: str, lang: str = "zh") -> str: """翻译函数""" d = ZH if lang == "zh" else EN return d.get(key, key) # ============================================================ # 首页卡片数据提取 # ============================================================ def get_home_cards(loop=None): """从 Loop 上下文提取首页卡片数据""" briefing = loop.get_briefing() if loop else {} trimester = briefing.get("trimester", "孕中期") focus_nutrients = briefing.get("dri_analysis", {}).get("focus_nutrients", []) recommended_foods = briefing.get("recommended_foods", []) from modules.family_manager import RecipeManager all_recipes = RecipeManager.load_all() recipe_count = len(all_recipes) recipe_names = [r.get("name", "") for r in all_recipes[:3]] yesterday = briefing.get("yesterday_diet", {}) yesterday_summary = yesterday.get("summary", "暂无记录") meal_count = yesterday.get("meal_count", 0) weight_eval = briefing.get("weight_evaluation", {}) weight_status = weight_eval.get("status", "暂无数据") weight_trend = weight_eval.get("trend", "") thinking_keywords = briefing.get("thinking_keywords", "") return { "trimester": trimester, "focus_nutrients": focus_nutrients, "recommended_foods": recommended_foods, "recipe_count": recipe_count, "recipe_names": recipe_names, "yesterday_summary": yesterday_summary, "meal_count": meal_count, "weight_status": weight_status, "weight_trend": weight_trend, "thinking_keywords": thinking_keywords, } # ============================================================ # Helper: 家庭数据 → HTML 卡片 # ============================================================ def render_family_recipes_html(lang="zh") -> str: """渲染家庭菜谱为现代卡片""" from modules.family_manager import RecipeManager recipes = RecipeManager.load_all() if not recipes: no = t("no_recipes", lang) return f'
{no}
' cards = "" for r in recipes: name = r.get("name", "") cook = r.get("cook", "") diff = r.get("difficulty", "") diff_stars = {"简单": "⭐", "中等": "⭐⭐", "困难": "⭐⭐⭐", "Easy": "⭐", "Medium": "⭐⭐", "Hard": "⭐⭐⭐"} stars = diff_stars.get(diff, "") cards += f"""
{name}
👨‍🍳 {cook} {stars}
""" return f"""
{cards}
""" def render_family_preferences_html(lang="zh") -> str: """渲染饮食偏好为现代卡片""" from modules.family_manager import PreferenceManager members = PreferenceManager.load_all() if not members: no = t("no_preferences", lang) return f'
{no}
' cards = "" for m in members: name = m.get("name", "") role = m.get("role", "") pref = m.get("preferences", "") avoid = m.get("avoid", "") allergies = m.get("allergies", "") tags = [] if pref: tags.append(f'✅ {pref}') if avoid: tags.append(f'❌ {avoid}') if allergies: tags.append(f'⚠ {allergies}') if not tags: tags.append(f'暂无记录') role_icons = {"孕妇":"🤰","丈夫":"👨","婆婆":"👩","妈妈":"👩","爸爸":"👨","其他家人":"👤", "Pregnant":"🤰","Husband":"👨","Mother-in-law":"👩","Mom":"👩","Dad":"👨","Other":"👤"} icon = role_icons.get(role, "👤") role_str = f" {role}" if role else "" cards += f"""
{icon}{name}{role_str}
{"".join(tags)}
""" return f"""
{cards}
""" def render_family_memories_html(lang="zh") -> str: """渲染家庭记忆为时间线""" from modules.family_manager import MemoryManager import re data = MemoryManager.load_all() if not any(data.values()): no = t("no_memories", lang) return f'
{no}
' # 将 dict 展平为 (date, content) 列表 flat = [] for section_key, label in [("relationships", "关系"), ("events", "事件"), ("daily", "日常")]: for line in data.get(section_key, []): m = re.match(r'-\s*\*{0,2}(\d{4}-\d{2}-\d{2})\*{0,2}:?\s*(.+)', line) if m: flat.append((m.group(1), m.group(2))) else: # 无日期的行(如关系描述),用空日期 clean = re.sub(r'^-\s*\*{0,2}(.*?)\*{0,2}:?\s*', r'\1: ', line).strip() flat.append(("", clean)) # 按日期排序(最新的在前),取最近10条 flat.sort(key=lambda x: x[0] if x[0] else "0000", reverse=True) items = "" for date_str, content in flat[:10]: disp_date = date_str[-5:] if date_str else "" if not content: continue items += f"""
{disp_date}
{content}
""" if not items: return f'
{t("no_memories", lang)}
' return f"""
{items}
""" # ============================================================ # 营养报告 HTML(含三天深度分析) # ============================================================ # 统一的设计令牌(Design Tokens) # Header: 粉色渐变 background:linear-gradient(135deg,#FCE4EC,#FFF0F2) # 卡片: background:#fff; border-radius:14px; box-shadow + border # 建议: background:linear-gradient(135deg,#FFF8E1,#FFFDE7); border:1px solid #FFE082 # 强调色: #880E4F (深粉红), #E91E63 (粉红), #E65100 (橙) # ============================================================ def render_nutrition_report_html(analysis: dict, days: int, lang="zh", deficit_data: dict = None) -> str: """渲染营养报告为现代 Dashboard HTML(含三天深度趋势分析)""" if not analysis or "error" in analysis: return f'
{analysis.get("error","暂无数据")}
' score = analysis.get("score", 0) meal_counts = analysis.get("meal_counts", {}) total_days = analysis.get("total_days", 0) nutrition_coverage = analysis.get("nutrition_coverage", {}) suggestions = analysis.get("suggestions", []) food_items = analysis.get("food_items", []) diversity_score = analysis.get("diversity_score", 0) # Score bar color bar_color = "#4CAF50" if score >= 70 else "#FF9800" if score >= 50 else "#F44336" bar_status = t("report_good", lang) if score >= 70 else t("report_need_attention", lang) # Meal completion bars meal_labels = {"早餐":"🌅", "午餐":"☀️", "晚餐":"🌙", "加餐":"🍪"} meal_rows = "" for mt, label in {"早餐":"Breakfast","午餐":"Lunch","晚餐":"Dinner","加餐":"Snack"}.items(): cnt = meal_counts.get(mt, 0) pct = min(100, round(cnt / max(total_days, 1) * 100)) icon = meal_labels.get(mt, "🍽️") mcolor = "#4CAF50" if pct >= 80 else "#FF9800" if pct >= 50 else "#F44336" meal_rows += f"""
{icon} {mt}{pct}%
""" # Nutrient coverage — 中英文双语 n_rows = "" for nut, info in nutrition_coverage.items(): pct = info.get("percentage", 0) if isinstance(info, dict) else info try: pct = min(100, int(pct)) except: pct = 0 ncolor = "#4CAF50" if pct >= 70 else "#FF9800" if pct >= 40 else "#F44336" nd = _nutrient_display(nut, lang) n_rows += f"""
{nd}{pct}%
""" # Suggestions sug_items = "" for s in suggestions[:5]: color = "#F44336" if "⚠" in s or "不足" in s or "不够" in s or "缺乏" in s else "#FF9800" sug_items += f'
• {s}
' # Diversity div_color = "#4CAF50" if diversity_score >= 60 else "#FF9800" if diversity_score >= 30 else "#F44336" food_count = len(food_items) food_str = "、".join(food_items[:8]) if len(food_items) > 8: food_str += "⋯" days_str = t("report_days", lang).format(days=days) # ============================================================ # 三天深度趋势分析(合并到营养报告内) # ============================================================ three_day_html = _render_deficit_section(deficit_data, lang) return f"""
📊 {t("report_overall", lang)}
{days_str} · 共 {total_days} 天
{score}
/ 100
{bar_status}
🍽️ {t("report_meal_completion", lang)}
{meal_rows}
🥗 {t("report_nutrient_coverage", lang)}
{n_rows if n_rows else '
暂无数据
'}
🌿 {t("report_diversity", lang)}
{diversity_score}
/ 100
{food_count} 种食材
{food_str}
💡 {t("report_suggestions", lang)}
{sug_items if sug_items else '
暂无建议
'}
{three_day_html}
""" def _render_deficit_section(deficit_data: dict, lang="zh") -> str: """ 渲染三天缺失深度分析板块,与营养报告使用同一套现代UI视觉语言。 Args: deficit_data: 来自 ThreeDaySummaryPlugin 的数据字典,包含: - "days_analyzed": int - "persistent_deficits": list[str] - "report": str (纯文本报告) lang: 语言标识 "zh" / "en" Returns: str: 纯 HTML 字符串,使用与营养报告统一的风格 """ if not deficit_data: return "" days_analyzed = deficit_data.get("days_analyzed", 0) persistent_deficits = deficit_data.get("persistent_deficits", []) report_text = deficit_data.get("report", "") # 如果没有数据则显示提示 if deficit_data.get("status") == "no_data" or days_analyzed == 0: no_data_msg = t("three_day_no_data", lang) return f"""
{no_data_msg}
""" # ——— Persistent Deficits: 紧凑标签式展示 ——— deficit_tags = "" for nutrient in persistent_deficits: nd = _nutrient_display(nutrient, lang) deficit_tags += f""" ⚠️ {nd} """ if not deficit_tags: all_good = t("three_day_all_good", lang) deficit_tags = f""" 🎉 {all_good} """ # ——— Food Suggestions ——— from modules.nutrition_standards import DRIsParser suggestion_rows = "" for nutrient in persistent_deficits[:3]: nd = _nutrient_display(nutrient, lang) foods = DRIsParser.RICH_FOODS.get(nutrient, []) if foods: food_tags = "".join( f'{f}' for f in foods[:6] ) suggestion_rows += f"""
💪 {nd}
{food_tags}
""" if not suggestion_rows: suggestion_rows = f"""
🎉 {t("three_day_all_good", lang)}
""" # ——— Summary: 结构化UI展示 ——— summary_sections_html = "" if report_text: lines = report_text.strip().split("\n") critical_items = [] warning_items = [] ok_items = [] info_items = [] for line in lines: line = line.strip() if not line: continue if line.startswith("❌"): text = line.lstrip("❌").strip() if text: critical_items.append(text) elif line.startswith("⚠️") or line.startswith("⚠"): text = line.lstrip("⚠️⚠").strip() if text: warning_items.append(text) elif line.startswith("✅"): text = line.lstrip("✅").strip() if text: ok_items.append(text) elif line.startswith("📊") or line.startswith("="): continue elif line.startswith("💡") or line.startswith("-"): continue else: info_items.append(line) # 构建结构化Summary卡片 summary_cards = "" if critical_items: for item in critical_items: summary_cards += f"""
{item}
""" if warning_items: for item in warning_items: summary_cards += f"""
⚠️ {item}
""" if ok_items: for item in ok_items: summary_cards += f"""
{item}
""" if info_items: for item in info_items: summary_cards += f"""
📌 {item}
""" if summary_cards: summary_sections_html = f"""
{summary_cards}
""" days_label = t("three_day_days", lang).format(days=days_analyzed) # ——— 拼装最终 HTML(使用与营养报告统一的设计语言) ——— return f"""
{t("three_day_title", lang)}
{t("three_day_subtitle", lang)}
{days_analyzed}
{'天' if lang == 'zh' else 'days'}
{days_label}
⚠️ {t("three_day_duration", lang)}
{deficit_tags}
{t("three_day_suggestions", lang)}
{suggestion_rows}
{f'''
📋 {t("three_day_summary", lang)}
{summary_sections_html}
''' if summary_sections_html else ''}
""" # 保留旧函数名作为代理(兼容外部调用),标记为 deprecated def render_three_day_deficit_section(deficit_data: dict, lang="zh") -> str: """已弃用:三天分析已合并到 render_nutrition_report_html 中。保留此函数保持兼容。""" return _render_deficit_section(deficit_data, lang) # ============================================================ # 营养素中英文对照表 # ============================================================ NUTRIENT_EN_NAMES = { "能量(MJ)": "Energy", "蛋白质(g)": "Protein", "钙(mg)": "Calcium", "铁(mg)": "Iron", "锌(mg)": "Zinc", "维生素A(μg RAE)": "Vitamin A", "维生素D(μg)": "Vitamin D", "维生素E(mg α-TE)": "Vitamin E", "维生素B1(mg)": "Vitamin B1", "维生素B2(mg)": "Vitamin B2", "叶酸(μg DFE)": "Folate", "碘(μg)": "Iodine", "镁(mg)": "Magnesium", "硒(μg)": "Selenium", "钾(mg)": "Potassium", "钠(mg)": "Sodium", "磷(mg)": "Phosphorus", "维生素C(mg)": "Vitamin C", "膳食纤维(g)": "Dietary Fiber", } def _nutrient_display(name_cn: str, lang: str) -> str: """返回营养素名的展示文本(中文模式显示中文+英文,英文模式显示英文)""" en_name = NUTRIENT_EN_NAMES.get(name_cn, name_cn) if lang == "zh": return f"{name_cn} / {en_name}" return en_name # ============================================================ # 自定义 CSS 样式 # ============================================================ CUSTOM_CSS = """ /* ===================== PregoPal Modern UI v2 ===================== */ /* 全局容器 */ .gradio-container { max-width: 1200px !important; margin: 0 auto !important; padding: 4px 16px !important; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, system-ui, sans-serif !important; } /* 顶部标题禁止选中 */ h1, h2, h3 { -webkit-user-select: none !important; user-select: none !important; } /* ======== Modern Tab Bar (永不折叠) ======== */ .tabs { display: flex !important; flex-direction: column !important; gap: 0 !important; } .tabs > .tab-nav { display: flex !important; flex-wrap: nowrap !important; overflow-x: auto !important; gap: 0 !important; background: #f8f8fb !important; border-radius: 16px 16px 0 0 !important; padding: 4px 4px 0 4px !important; -webkit-overflow-scrolling: touch !important; } .tabs > .tab-nav > button { flex-shrink: 0 !important; min-width: auto !important; white-space: nowrap !important; font-size: 14px !important; font-weight: 500 !important; padding: 10px 18px !important; border-radius: 12px 12px 0 0 !important; background: transparent !important; border: none !important; color: #888 !important; transition: all 0.2s ease !important; margin: 0 1px !important; cursor: pointer !important; } .tabs > .tab-nav > button:hover { background: rgba(233, 30, 99, 0.04) !important; color: #E91E63 !important; } .tabs > .tab-nav > button.selected { background: #fff !important; color: #E91E63 !important; font-weight: 600 !important; box-shadow: 0 -2px 8px rgba(233, 30, 99, 0.08) !important; } /* ======== Cards (Glassmorphism) ======== */ .glass-card { background: rgba(255,255,255,0.75) !important; backdrop-filter: blur(12px) !important; -webkit-backdrop-filter: blur(12px) !important; border-radius: 16px !important; padding: 20px 24px !important; border: 1px solid rgba(255,255,255,0.8) !important; box-shadow: 0 2px 16px rgba(233, 30, 99, 0.06) !important; transition: all 0.3s ease !important; } .glass-card:hover { box-shadow: 0 4px 24px rgba(233, 30, 99, 0.10) !important; transform: translateY(-2px); } /* home cards (首页卡片) */ .home-card { border-radius: 16px !important; padding: 16px 20px !important; background: #fff !important; border: 1px solid #f0f0f0 !important; box-shadow: 0 1px 8px rgba(0,0,0,0.04) !important; transition: all 0.2s ease !important; } .home-card:hover { box-shadow: 0 4px 20px rgba(233, 30, 99, 0.08) !important; } .home-card h3 { margin: 0 0 6px 0 !important; font-size: 16px !important; } .home-card p { margin: 0 !important; font-size: 14px !important; color: #555 !important; } /* AI Thinking Box — div 版本(无 textarea 外壳) */ .thinking-box { background: linear-gradient(135deg, #F3E5F5, #FFF) !important; border: 1px solid #E1BEE7 !important; border-radius: 12px !important; padding: 12px 16px !important; font-size: 14px !important; color: #6A1B9A !important; } /* 语音按钮 — 纯 HTML 圆形按钮 hover/active 效果(主样式在 inline style 中) */ .voice-main-btn:hover { transform: scale(1.05) !important; box-shadow: 0 12px 40px rgba(255, 64, 129, 0.4) !important; } .voice-main-btn:active { transform: scale(0.95) !important; } /* 按钮统一 */ button, .gr-button { border-radius: 12px !important; font-weight: 500 !important; transition: all 0.2s ease !important; } input, textarea, .gr-textbox { border-radius: 12px !important; } /* 消除 Gradio Group 默认嵌套边框 */ .gr-group, .gr-box { border: none !important; box-shadow: none !important; } .gr-group > .gr-box, .gr-box > .gr-box { border: none !important; box-shadow: none !important; } /* ======== 消除所有Gradio容器的灰色填充和双重边框 ======== */ /* 消除所有 Group/Box 的灰色背景 */ .gr-group, .gr-box, .gr-form, .gr-panel { background: transparent !important; } /* 消除所有文本输入区域的灰色填充 */ textarea, input[type="text"], input[type="number"], input[type="search"], input[type="email"], input[type="password"], input[type="url"], .gr-textarea, .gr-textbox, .gr-input { background: #fff !important; border-color: #e0e0e0 !important; box-shadow: none !important; } /* 消除焦点时的双重外框 */ textarea:focus, input:focus, .gr-textarea:focus, .gr-textbox:focus { outline: none !important; border-color: #E91E63 !important; box-shadow: 0 0 0 2px rgba(233, 30, 99, 0.15) !important; } /* Gradio button/input wrap 容器 — 去除额外边框 */ .gr-box > .gr-textarea, .gr-box > .gr-textbox, .gr-form > .gr-box, .gr-group > .gr-box { border: none !important; background: transparent !important; box-shadow: none !important; } /* 所有卡片容器白色背景 + 取消默认边框 */ .home-card, .home-card.gr-group, .home-card .gr-box { background: #fff !important; border: 1px solid #f0f0f0 !important; border-radius: 16px !important; box-shadow: 0 1px 8px rgba(0,0,0,0.04) !important; } /* 消除 Gr.Group 默认灰色内边 */ .gr-group { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; gap: 0 !important; } /* 消除 Trimester 卡片 group 的双层边框 */ .card-trimester.gr-group, .card-trimester .gr-box { border: 1px solid #f0f0f0 !important; border-radius: 16px !important; box-shadow: 0 1px 8px rgba(0,0,0,0.04) !important; } .card-trimester.gr-group .gr-box { border: none !important; } /* 响应式调整 */ @media (max-width: 768px) { .voice-main-btn { width: 120px !important; height: 120px !important; font-size: 36px !important; } .gradio-container { padding: 4px !important; } .tabs > .tab-nav > button { font-size: 12px !important; padding: 8px 12px !important; } } """