"""
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"""
"""
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"""
"""
# 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"""
"""
# 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} 天
{bar_status}
🍽️ {t("report_meal_completion", lang)}
{meal_rows}
🥗 {t("report_nutrient_coverage", lang)}
{n_rows if n_rows else '
暂无数据
'}
🌿 {t("report_diversity", lang)}
{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"""
"""
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;
}
}
"""