Spaces:
Runtime error
Runtime error
J.B-Lin commited on
Commit ·
3e28c81
1
Parent(s): bfe4514
chore: 回退语音按钮为纯HTML圆形按钮 + 修复思考框/嵌套边框/营养报告布局
Browse files- ui/app_builder.py +91 -33
- utils.py +48 -26
ui/app_builder.py
CHANGED
|
@@ -14,6 +14,7 @@ PregoPal - Gradio 界面构建(现代UI版)
|
|
| 14 |
|
| 15 |
import datetime
|
| 16 |
import gradio as gr
|
|
|
|
| 17 |
|
| 18 |
from modules.voiceprint import VoiceprintManager
|
| 19 |
from modules.meal_recommender import MealRecommender
|
|
@@ -39,6 +40,30 @@ diet_logger = DietLogger()
|
|
| 39 |
nutrition_analyzer = NutritionAnalyzer()
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
# ============================================================
|
| 43 |
# 工具:首页语言文本
|
| 44 |
# ============================================================
|
|
@@ -49,7 +74,7 @@ def _home_texts(lang):
|
|
| 49 |
"title": "# 🌸 欢迎来到 PregoPal",
|
| 50 |
"subtitle": "### 你的孕期AI伴侣,时刻陪伴在你身边",
|
| 51 |
"tap_speak": "🎙️ 点击说话",
|
| 52 |
-
"tap_hint": "
|
| 53 |
"trimester_label": "孕期阶段",
|
| 54 |
"nutrition_focus": "营养关注",
|
| 55 |
"today_diet": "昨日饮食",
|
|
@@ -57,6 +82,7 @@ def _home_texts(lang):
|
|
| 57 |
"weight_label": "体重管理",
|
| 58 |
"thinking_label": "🤔 AI 思考中...",
|
| 59 |
"thinking_placeholder": "等待对话中...",
|
|
|
|
| 60 |
"recent_title": "最近的饮食记录",
|
| 61 |
"meal_count_unit": "餐",
|
| 62 |
"no_data": "暂无数据",
|
|
@@ -69,7 +95,7 @@ def _home_texts(lang):
|
|
| 69 |
"title": "# 🌸 Welcome to PregoPal",
|
| 70 |
"subtitle": "### Your AI pregnancy companion, always by your side",
|
| 71 |
"tap_speak": "🎙️ Tap to Speak",
|
| 72 |
-
"tap_hint": "
|
| 73 |
"trimester_label": "Trimester",
|
| 74 |
"nutrition_focus": "Nutrition Focus",
|
| 75 |
"today_diet": "Yesterday's Diet",
|
|
@@ -77,6 +103,7 @@ def _home_texts(lang):
|
|
| 77 |
"weight_label": "Weight",
|
| 78 |
"thinking_label": "🤔 AI Thinking...",
|
| 79 |
"thinking_placeholder": "Waiting for conversation...",
|
|
|
|
| 80 |
"recent_title": "Recent Diet Records",
|
| 81 |
"meal_count_unit": " meals",
|
| 82 |
"no_data": "No data",
|
|
@@ -163,7 +190,7 @@ def _home_content(loop, lang):
|
|
| 163 |
if cards["weight_trend"]:
|
| 164 |
weight_display += f" | {cards['weight_trend']}"
|
| 165 |
|
| 166 |
-
thinking_text = cards["thinking_keywords"] if cards["thinking_keywords"] else T["
|
| 167 |
|
| 168 |
records = diet_logger.get_recent_records(days=3)
|
| 169 |
recent_html = _render_recent_records_html(records, lang)
|
|
@@ -172,28 +199,38 @@ def _home_content(loop, lang):
|
|
| 172 |
gr.Markdown(T["title"])
|
| 173 |
gr.Markdown(T["subtitle"])
|
| 174 |
|
| 175 |
-
# 语音启动按钮(
|
| 176 |
with gr.Row():
|
| 177 |
with gr.Column(scale=1):
|
| 178 |
pass
|
| 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"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
<p style="margin-top: 8px; color: #999; font-size: 14px;">{T['tap_hint']}</p>
|
| 184 |
</div>
|
| 185 |
""")
|
| 186 |
with gr.Column(scale=1):
|
| 187 |
pass
|
| 188 |
|
| 189 |
-
# AI 思考状态
|
| 190 |
-
gr.
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
)
|
| 197 |
|
| 198 |
# 卡片行 1
|
| 199 |
with gr.Row():
|
|
@@ -232,12 +269,12 @@ def _home_content(loop, lang):
|
|
| 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 |
-
|
|
|
|
| 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")
|
|
@@ -313,30 +350,47 @@ def _summary_content(lang):
|
|
| 313 |
|
| 314 |
|
| 315 |
# ============================================================
|
| 316 |
-
# Tab 4: 📈 营养报告(纯 HTML Dashboard,
|
| 317 |
# ============================================================
|
| 318 |
def _report_content(lang):
|
| 319 |
-
"""营养报告组件 — 纯 HTML 可视化"""
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
|
| 341 |
|
| 342 |
# ============================================================
|
|
@@ -354,14 +408,18 @@ def create_app(loop=None):
|
|
| 354 |
# 语言状态
|
| 355 |
lang_state = gr.State(value="zh")
|
| 356 |
|
| 357 |
-
# 顶部标题 + 语言切换
|
| 358 |
with gr.Row():
|
| 359 |
with gr.Column(scale=4):
|
| 360 |
gr.HTML("""
|
| 361 |
<div style="text-align: left; padding: 8px 0;">
|
| 362 |
-
<h1 style="margin: 0; font-size: 28px;">🌸 PregoPal</h1>
|
| 363 |
-
<p style="color: #999; margin: 4px 0 0 0; font-size: 16px;
|
|
|
|
|
|
|
|
|
|
| 364 |
</div>
|
|
|
|
| 365 |
""")
|
| 366 |
with gr.Column(scale=1, min_width=120):
|
| 367 |
lang_selector = gr.Radio(
|
|
|
|
| 14 |
|
| 15 |
import datetime
|
| 16 |
import gradio as gr
|
| 17 |
+
import time
|
| 18 |
|
| 19 |
from modules.voiceprint import VoiceprintManager
|
| 20 |
from modules.meal_recommender import MealRecommender
|
|
|
|
| 40 |
nutrition_analyzer = NutritionAnalyzer()
|
| 41 |
|
| 42 |
|
| 43 |
+
# ============================================================
|
| 44 |
+
# 语音会话入口(pass 占位,待 Llama 模型接入)
|
| 45 |
+
# ============================================================
|
| 46 |
+
async def start_voice_session(audio_data: str, lang: str = "zh") -> dict:
|
| 47 |
+
"""
|
| 48 |
+
语音会话入口(当前 pass 占位,待接入 Llama 大模型)
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
audio_data: Gradio Audio 组件返回的文件路径 (str)
|
| 52 |
+
lang: 语言标识 "zh" / "en"
|
| 53 |
+
|
| 54 |
+
Returns:
|
| 55 |
+
dict: {
|
| 56 |
+
"text": str, # ASR 转写文本
|
| 57 |
+
"response": str, # AI 回复文本
|
| 58 |
+
"speaker": str, # 识别出的说话人
|
| 59 |
+
"audio_response": bytes | None # TTS 音频(可选)
|
| 60 |
+
}
|
| 61 |
+
"""
|
| 62 |
+
# TODO: 接入 Llama 模型后替换此函数体
|
| 63 |
+
# 当前 pass 占位,返回空结果避免报错
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
|
| 67 |
# ============================================================
|
| 68 |
# 工具:首页语言文本
|
| 69 |
# ============================================================
|
|
|
|
| 74 |
"title": "# 🌸 欢迎来到 PregoPal",
|
| 75 |
"subtitle": "### 你的孕期AI伴侣,时刻陪伴在你身边",
|
| 76 |
"tap_speak": "🎙️ 点击说话",
|
| 77 |
+
"tap_hint": "🎤 点击开始聊天",
|
| 78 |
"trimester_label": "孕期阶段",
|
| 79 |
"nutrition_focus": "营养关注",
|
| 80 |
"today_diet": "昨日饮食",
|
|
|
|
| 82 |
"weight_label": "体重管理",
|
| 83 |
"thinking_label": "🤔 AI 思考中...",
|
| 84 |
"thinking_placeholder": "等待对话中...",
|
| 85 |
+
"thinking_waiting": "等待对话中...",
|
| 86 |
"recent_title": "最近的饮食记录",
|
| 87 |
"meal_count_unit": "餐",
|
| 88 |
"no_data": "暂无数据",
|
|
|
|
| 95 |
"title": "# 🌸 Welcome to PregoPal",
|
| 96 |
"subtitle": "### Your AI pregnancy companion, always by your side",
|
| 97 |
"tap_speak": "🎙️ Tap to Speak",
|
| 98 |
+
"tap_hint": "🎤 Tap to talk",
|
| 99 |
"trimester_label": "Trimester",
|
| 100 |
"nutrition_focus": "Nutrition Focus",
|
| 101 |
"today_diet": "Yesterday's Diet",
|
|
|
|
| 103 |
"weight_label": "Weight",
|
| 104 |
"thinking_label": "🤔 AI Thinking...",
|
| 105 |
"thinking_placeholder": "Waiting for conversation...",
|
| 106 |
+
"thinking_waiting": "Waiting for conversation...",
|
| 107 |
"recent_title": "Recent Diet Records",
|
| 108 |
"meal_count_unit": " meals",
|
| 109 |
"no_data": "No data",
|
|
|
|
| 190 |
if cards["weight_trend"]:
|
| 191 |
weight_display += f" | {cards['weight_trend']}"
|
| 192 |
|
| 193 |
+
thinking_text = cards["thinking_keywords"] if cards["thinking_keywords"] else T["thinking_waiting"]
|
| 194 |
|
| 195 |
records = diet_logger.get_recent_records(days=3)
|
| 196 |
recent_html = _render_recent_records_html(records, lang)
|
|
|
|
| 199 |
gr.Markdown(T["title"])
|
| 200 |
gr.Markdown(T["subtitle"])
|
| 201 |
|
| 202 |
+
# 语音启动 — 粉色渐变圆形按钮(全双工语音交互入口)
|
| 203 |
with gr.Row():
|
| 204 |
with gr.Column(scale=1):
|
| 205 |
pass
|
| 206 |
with gr.Column(scale=2):
|
| 207 |
gr.HTML(f"""
|
| 208 |
<div style="text-align: center; padding: 24px 0;">
|
| 209 |
+
<button class="voice-main-btn" style="
|
| 210 |
+
width:160px;height:160px;border-radius:50%;
|
| 211 |
+
background:linear-gradient(135deg,#FF6B8A 0%,#FF4081 100%);
|
| 212 |
+
border:4px solid #FFF;
|
| 213 |
+
box-shadow:0 8px 32px rgba(255,64,129,0.3);
|
| 214 |
+
cursor:pointer;font-size:48px;color:white;
|
| 215 |
+
transition:all 0.3s ease;"
|
| 216 |
+
onmouseover="this.style.transform='scale(1.05)';this.style.boxShadow='0 12px 40px rgba(255,64,129,0.4)'"
|
| 217 |
+
onmouseout="this.style.transform='scale(1)';this.style.boxShadow='0 8px 32px rgba(255,64,129,0.3)'"
|
| 218 |
+
>
|
| 219 |
+
🎙️
|
| 220 |
+
</button>
|
| 221 |
<p style="margin-top: 8px; color: #999; font-size: 14px;">{T['tap_hint']}</p>
|
| 222 |
</div>
|
| 223 |
""")
|
| 224 |
with gr.Column(scale=1):
|
| 225 |
pass
|
| 226 |
|
| 227 |
+
# AI 思考状态 — 纯 HTML(无 Gradio textarea 灰色外壳)
|
| 228 |
+
gr.HTML(f"""
|
| 229 |
+
<div class="thinking-box">
|
| 230 |
+
<div style="font-weight:600;color:#6A1B9A;">{T['thinking_label']}</div>
|
| 231 |
+
<div style="color:#888;font-size:14px;margin-top:4px;">{thinking_text}</div>
|
| 232 |
+
</div>
|
| 233 |
+
""")
|
|
|
|
| 234 |
|
| 235 |
# 卡片行 1
|
| 236 |
with gr.Row():
|
|
|
|
| 269 |
# ============================================================
|
| 270 |
def _family_content(lang):
|
| 271 |
"""家庭饮食习惯组件 — 纯展示模式"""
|
|
|
|
| 272 |
with gr.Column(elem_classes=["glass-card"]):
|
| 273 |
gr.Markdown(f"## {t('family_title', lang)}")
|
| 274 |
gr.Markdown(f"*{t('family_subtitle', lang)}*")
|
| 275 |
|
| 276 |
+
# 默认选中第一个 Tab(家庭菜谱)
|
| 277 |
+
with gr.Tabs(selected=0):
|
| 278 |
with gr.Tab(t("tab_recipes", lang)):
|
| 279 |
recipes_html = gr.HTML(render_family_recipes_html(lang))
|
| 280 |
refresh_recipe_btn = gr.Button(t("btn_refresh", lang), size="sm")
|
|
|
|
| 350 |
|
| 351 |
|
| 352 |
# ============================================================
|
| 353 |
+
# Tab 4: 📈 营养报告(纯 HTML Dashboard,自动生成)
|
| 354 |
# ============================================================
|
| 355 |
def _report_content(lang):
|
| 356 |
+
"""营养报告组件 — 纯 HTML 可视化,Slider 防抖自动生成"""
|
| 357 |
+
# 防抖时间戳
|
| 358 |
+
_last_change = [0.0]
|
| 359 |
+
|
| 360 |
+
with gr.Column():
|
| 361 |
+
# Slider 全宽
|
| 362 |
+
analysis_days = gr.Slider(
|
| 363 |
+
label=t("analysis_days", lang),
|
| 364 |
+
minimum=1, maximum=30, value=7, step=1,
|
| 365 |
+
interactive=True,
|
| 366 |
+
)
|
| 367 |
|
| 368 |
+
# 报告全宽
|
| 369 |
+
report_dashboard = gr.HTML(
|
| 370 |
+
f'<div style="padding:40px;text-align:center;color:#bbb;font-size:16px;">'
|
| 371 |
+
f'调整上方天数自动生成报告</div>'
|
| 372 |
+
)
|
| 373 |
|
| 374 |
def generate_report(days):
|
| 375 |
records = diet_logger.get_recent_records(days=int(days))
|
| 376 |
analysis = nutrition_analyzer.analyze_diet(records)
|
| 377 |
return render_nutrition_report_html(analysis, days=int(days), lang=lang)
|
| 378 |
|
| 379 |
+
def on_slider_change(days):
|
| 380 |
+
"""Slider 变化时防抖 0.8s 后生成报告"""
|
| 381 |
+
_last_change[0] = time.time()
|
| 382 |
+
# wait for debounce window
|
| 383 |
+
time.sleep(0.8)
|
| 384 |
+
# if another change happened during sleep, skip
|
| 385 |
+
if time.time() - _last_change[0] < 0.9:
|
| 386 |
+
return generate_report(days)
|
| 387 |
+
return gr.skip()
|
| 388 |
+
|
| 389 |
+
analysis_days.change(
|
| 390 |
+
fn=on_slider_change,
|
| 391 |
+
inputs=[analysis_days],
|
| 392 |
+
outputs=[report_dashboard],
|
| 393 |
+
)
|
| 394 |
|
| 395 |
|
| 396 |
# ============================================================
|
|
|
|
| 408 |
# 语言状态
|
| 409 |
lang_state = gr.State(value="zh")
|
| 410 |
|
| 411 |
+
# 顶部标题 + 语言切换(含英文副标题 + Google Fonts 趣味字体)
|
| 412 |
with gr.Row():
|
| 413 |
with gr.Column(scale=4):
|
| 414 |
gr.HTML("""
|
| 415 |
<div style="text-align: left; padding: 8px 0;">
|
| 416 |
+
<h1 style="margin: 0; font-size: 28px; font-family: 'Nunito', 'Quicksand', system-ui, sans-serif;">🌸 PregoPal</h1>
|
| 417 |
+
<p style="color: #999; margin: 4px 0 0 0; font-size: 16px; font-family: 'Nunito', 'Quicksand', system-ui, sans-serif;">
|
| 418 |
+
孕期陪护AI助手 — 温馨的家庭式伴侣
|
| 419 |
+
<br><span style="font-size: 14px; color: #bbb;">Pregnancy Companion AI — Your Cozy Family Partner</span>
|
| 420 |
+
</p>
|
| 421 |
</div>
|
| 422 |
+
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&family=Quicksand:wght@400;500;600;700&display=swap" rel="stylesheet">
|
| 423 |
""")
|
| 424 |
with gr.Column(scale=1, min_width=120):
|
| 425 |
lang_selector = gr.Radio(
|
utils.py
CHANGED
|
@@ -271,20 +271,35 @@ def render_family_preferences_html(lang="zh") -> str:
|
|
| 271 |
def render_family_memories_html(lang="zh") -> str:
|
| 272 |
"""渲染家庭记忆为时间线"""
|
| 273 |
from modules.family_manager import MemoryManager
|
| 274 |
-
|
| 275 |
-
|
|
|
|
| 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
|
| 280 |
-
|
| 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;">{
|
| 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}
|
|
@@ -364,7 +379,7 @@ def render_nutrition_report_html(analysis: dict, days: int, lang="zh") -> str:
|
|
| 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}
|
| 368 |
</div>
|
| 369 |
<div style="text-align:right;">
|
| 370 |
<div style="font-size:36px;font-weight:800;color:{bar_color};">{score}</div>
|
|
@@ -517,32 +532,17 @@ h1, h2, h3 {
|
|
| 517 |
color: #555 !important;
|
| 518 |
}
|
| 519 |
|
| 520 |
-
/* AI Thinking Box */
|
| 521 |
-
.thinking-box
|
| 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 |
-
/*
|
| 531 |
-
.voice-main-btn {
|
| 532 |
-
width: 160px !important;
|
| 533 |
-
height: 160px !important;
|
| 534 |
-
border-radius: 50% !important;
|
| 535 |
-
background: linear-gradient(135deg, #FF6B8A 0%, #FF4081 100%) !important;
|
| 536 |
-
border: 4px solid #FFF !important;
|
| 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;
|
| 548 |
box-shadow: 0 12px 40px rgba(255, 64, 129, 0.4) !important;
|
|
@@ -561,6 +561,28 @@ input, textarea, .gr-textbox {
|
|
| 561 |
border-radius: 12px !important;
|
| 562 |
}
|
| 563 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
/* 响应式调整 */
|
| 565 |
@media (max-width: 768px) {
|
| 566 |
.voice-main-btn {
|
|
|
|
| 271 |
def render_family_memories_html(lang="zh") -> str:
|
| 272 |
"""渲染家庭记忆为时间线"""
|
| 273 |
from modules.family_manager import MemoryManager
|
| 274 |
+
import re
|
| 275 |
+
data = MemoryManager.load_all()
|
| 276 |
+
if not any(data.values()):
|
| 277 |
no = t("no_memories", lang)
|
| 278 |
return f'<div style="padding:24px;text-align:center;color:#aaa;font-size:15px;">{no}</div>'
|
| 279 |
+
# 将 dict 展平为 (date, content) 列表
|
| 280 |
+
flat = []
|
| 281 |
+
for section_key, label in [("relationships", "关系"), ("events", "事件"), ("daily", "日常")]:
|
| 282 |
+
for line in data.get(section_key, []):
|
| 283 |
+
m = re.match(r'-\s*\*{0,2}(\d{4}-\d{2}-\d{2})\*{0,2}:?\s*(.+)', line)
|
| 284 |
+
if m:
|
| 285 |
+
flat.append((m.group(1), m.group(2)))
|
| 286 |
+
else:
|
| 287 |
+
# 无日期的行(如关系描述),用空日期
|
| 288 |
+
clean = re.sub(r'^-\s*\*{0,2}(.*?)\*{0,2}:?\s*', r'\1: ', line).strip()
|
| 289 |
+
flat.append(("", clean))
|
| 290 |
+
# 按日期排序(最新的在前),取最近10条
|
| 291 |
+
flat.sort(key=lambda x: x[0] if x[0] else "0000", reverse=True)
|
| 292 |
items = ""
|
| 293 |
+
for date_str, content in flat[:10]:
|
| 294 |
+
disp_date = date_str[-5:] if date_str else ""
|
|
|
|
| 295 |
if not content: continue
|
| 296 |
items += f"""
|
| 297 |
<div style="display:flex;gap:12px;padding:8px 0;">
|
| 298 |
+
<div style="min-width:50px;font-size:13px;color:#E91E63;font-weight:500;">{disp_date}</div>
|
| 299 |
<div style="flex:1;background:#fff;border-radius:10px;padding:10px 14px;border:1px solid #f0f0f0;font-size:14px;color:#333;">{content}</div>
|
| 300 |
</div>"""
|
| 301 |
+
if not items:
|
| 302 |
+
return f'<div style="padding:24px;text-align:center;color:#aaa;font-size:15px;">{t("no_memories", lang)}</div>'
|
| 303 |
return f"""
|
| 304 |
<div style="max-height:320px;overflow-y:auto;padding:4px 0;">
|
| 305 |
{items}
|
|
|
|
| 379 |
<div style="display:flex;justify-content:space-between;align-items:center;">
|
| 380 |
<div>
|
| 381 |
<div style="font-size:20px;font-weight:700;color:#880E4F;">📊 {t("report_overall", lang)}</div>
|
| 382 |
+
<div style="color:#888;font-size:13px;margin-top:4px;">{days_str} · 共 {total_days} 天</div>
|
| 383 |
</div>
|
| 384 |
<div style="text-align:right;">
|
| 385 |
<div style="font-size:36px;font-weight:800;color:{bar_color};">{score}</div>
|
|
|
|
| 532 |
color: #555 !important;
|
| 533 |
}
|
| 534 |
|
| 535 |
+
/* AI Thinking Box — div 版本(无 textarea 外壳) */
|
| 536 |
+
.thinking-box {
|
| 537 |
background: linear-gradient(135deg, #F3E5F5, #FFF) !important;
|
| 538 |
border: 1px solid #E1BEE7 !important;
|
| 539 |
border-radius: 12px !important;
|
| 540 |
+
padding: 12px 16px !important;
|
| 541 |
font-size: 14px !important;
|
| 542 |
color: #6A1B9A !important;
|
|
|
|
| 543 |
}
|
| 544 |
|
| 545 |
+
/* 语音按钮 — 纯 HTML 圆形按钮 hover/active 效果(主样式在 inline style 中) */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
.voice-main-btn:hover {
|
| 547 |
transform: scale(1.05) !important;
|
| 548 |
box-shadow: 0 12px 40px rgba(255, 64, 129, 0.4) !important;
|
|
|
|
| 561 |
border-radius: 12px !important;
|
| 562 |
}
|
| 563 |
|
| 564 |
+
/* 消除 Gradio Group 默认嵌套边框 */
|
| 565 |
+
.gr-group, .gr-box {
|
| 566 |
+
border: none !important;
|
| 567 |
+
box-shadow: none !important;
|
| 568 |
+
}
|
| 569 |
+
.gr-group > .gr-box,
|
| 570 |
+
.gr-box > .gr-box {
|
| 571 |
+
border: none !important;
|
| 572 |
+
box-shadow: none !important;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
/* 消除 Trimester 卡片 group 的双层边框 */
|
| 576 |
+
.card-trimester.gr-group,
|
| 577 |
+
.card-trimester .gr-box {
|
| 578 |
+
border: 1px solid #f0f0f0 !important;
|
| 579 |
+
border-radius: 16px !important;
|
| 580 |
+
box-shadow: 0 1px 8px rgba(0,0,0,0.04) !important;
|
| 581 |
+
}
|
| 582 |
+
.card-trimester.gr-group .gr-box {
|
| 583 |
+
border: none !important;
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
/* 响应式调整 */
|
| 587 |
@media (max-width: 768px) {
|
| 588 |
.voice-main-btn {
|