fix(ai): 折叠态只显示一句话,那句话得说清是哪个指标
精简版的卡片正文就是第一条 highlight,标题不渲染。于是趋势页折叠起来是 「395 天内由 4376.0分 到 4905.0分(改善)」——什么的 4376 分?身体成分、 运动详情同样。 每个 builder 的第一条 detail 现在都自带主语,并加了不变量测试:detail 必须 包含它自己的 title(纯汇总性标题除外)。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -495,6 +495,10 @@ def rule_scope_insight(context):
|
|||||||
return {
|
return {
|
||||||
# The first highlight leads and is then dropped from the list: showing
|
# The first highlight leads and is then dropped from the list: showing
|
||||||
# it in both places printed the same sentence twice.
|
# it in both places printed the same sentence twice.
|
||||||
|
#
|
||||||
|
# This is why every builder's first `detail` has to read on its own —
|
||||||
|
# here the `title` beside it is not rendered, and "395 天内由 4376 到
|
||||||
|
# 4905(改善)" without naming 耐力分 is a sentence about nothing.
|
||||||
"headline": highlights[0]["detail"] if highlights else None,
|
"headline": highlights[0]["detail"] if highlights else None,
|
||||||
"points": [dict(h) for h in highlights[1:6]],
|
"points": [dict(h) for h in highlights[1:6]],
|
||||||
"actions": [],
|
"actions": [],
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ def build_trends(user_id, subject=None):
|
|||||||
highlights = [{
|
highlights = [{
|
||||||
"title": t["label"],
|
"title": t["label"],
|
||||||
"detail": (
|
"detail": (
|
||||||
f"{t['days']} 天内由 {t['firstMean']}{t['unit']} 到 "
|
f"{t['label']} {t['days']} 天内由 {t['firstMean']}{t['unit']} 到 "
|
||||||
f"{t['lastMean']}{t['unit']}({t['direction']}),"
|
f"{t['lastMean']}{t['unit']}({t['direction']}),"
|
||||||
f"斜率约 {t['slopePer30d']}{t['unit']}/30 天。"
|
f"斜率约 {t['slopePer30d']}{t['unit']}/30 天。"
|
||||||
),
|
),
|
||||||
@@ -332,7 +332,7 @@ def build_daily(user_id, subject=None):
|
|||||||
"detail": f"{d['label']} {d['value']}{d['unit']},偏离基线 "
|
"detail": f"{d['label']} {d['value']}{d['unit']},偏离基线 "
|
||||||
f"{abs(d['z']):.1f} 个标准差。",
|
f"{abs(d['z']):.1f} 个标准差。",
|
||||||
} for d in notable[:4]] or [
|
} for d in notable[:4]] or [
|
||||||
{"title": date, "detail": "这一天各项指标都在常态范围内。"}
|
{"title": date, "detail": f"{date} 各项指标都在常态范围内。"}
|
||||||
]
|
]
|
||||||
|
|
||||||
return date, {
|
return date, {
|
||||||
@@ -370,7 +370,8 @@ def build_body(user_id, subject=None):
|
|||||||
continue
|
continue
|
||||||
highlights.append({
|
highlights.append({
|
||||||
"title": label,
|
"title": label,
|
||||||
"detail": f"{first['date']} {a}{unit} → {last['date']} {b}{unit}"
|
"detail": f"{label}从 {first['date']} 的 {a}{unit} 到 "
|
||||||
|
f"{last['date']} 的 {b}{unit}"
|
||||||
f"({'+' if b >= a else ''}{round(b - a, 1)}{unit})。",
|
f"({'+' if b >= a else ''}{round(b - a, 1)}{unit})。",
|
||||||
})
|
})
|
||||||
elif series:
|
elif series:
|
||||||
@@ -522,9 +523,11 @@ def build_activity(user_id, subject=None):
|
|||||||
total_zone = sum(z.get("seconds") or 0 for z in zones)
|
total_zone = sum(z.get("seconds") or 0 for z in zones)
|
||||||
|
|
||||||
minutes = round((base.get("duration") or summary.get("duration") or 0) / 60)
|
minutes = round((base.get("duration") or summary.get("duration") or 0) / 60)
|
||||||
|
sport = (base.get("activity_type") or (detail or {}).get("activityType")
|
||||||
|
or "运动")
|
||||||
highlights = [{
|
highlights = [{
|
||||||
"title": base.get("activity_type") or (detail or {}).get("activityType") or "运动",
|
"title": sport,
|
||||||
"detail": f"{minutes} 分钟"
|
"detail": f"{sport} {minutes} 分钟"
|
||||||
+ (f",{round((base.get('distance') or 0) / 1000, 2)} km"
|
+ (f",{round((base.get('distance') or 0) / 1000, 2)} km"
|
||||||
if base.get("distance") else "")
|
if base.get("distance") else "")
|
||||||
+ (f",平均心率 {base.get('heart_rate_average')}"
|
+ (f",平均心率 {base.get('heart_rate_average')}"
|
||||||
|
|||||||
@@ -1053,3 +1053,23 @@ class TestSupersede:
|
|||||||
statuses = {r["subject"]: r["status"] for r in analysis_svc.query_all(
|
statuses = {r["subject"]: r["status"] for r in analysis_svc.query_all(
|
||||||
"SELECT subject, status FROM ai_jobs WHERE kind = 'trends'")}
|
"SELECT subject, status FROM ai_jobs WHERE kind = 'trends'")}
|
||||||
assert statuses.get("2026-08-01") == "running"
|
assert statuses.get("2026-08-01") == "running"
|
||||||
|
|
||||||
|
|
||||||
|
class TestHighlightsReadAlone:
|
||||||
|
"""The first highlight becomes the collapsed card's whole text, with no
|
||||||
|
title beside it — so it has to name what it is about."""
|
||||||
|
|
||||||
|
def test_every_first_highlight_names_its_subject(self, month):
|
||||||
|
for name in scopes.SCOPES:
|
||||||
|
built = scopes.build(month["id"], name)
|
||||||
|
if not built:
|
||||||
|
continue
|
||||||
|
first = built[1]["highlights"][0]
|
||||||
|
title = first["title"]
|
||||||
|
# Titles that are already summaries rather than names of a metric.
|
||||||
|
if title in ("整体", "当前预测") or title.startswith("近 "):
|
||||||
|
continue
|
||||||
|
assert title in first["detail"], (
|
||||||
|
f"{name}: 折叠态只显示 detail,但它没提到 {title!r}: "
|
||||||
|
f"{first['detail']!r}"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user