Harden Dream QA tip grounding
#43
by ADJCJH - opened
- dream_customs/pipeline.py +588 -37
- dream_customs/prompts.py +4 -4
- dream_customs/ui/actions.py +15 -1
- dream_customs/ui/app.py +85 -12
- dream_customs/ui/copy.py +8 -6
- dream_customs/ui/styles.py +80 -0
- tests/test_pipeline.py +293 -2
- tests/test_ui_actions.py +45 -7
dream_customs/pipeline.py
CHANGED
|
@@ -431,6 +431,7 @@ _PLACEHOLDER_ANCHORS = {
|
|
| 431 |
"the dream detail",
|
| 432 |
"dream",
|
| 433 |
"dream fragment",
|
|
|
|
| 434 |
}
|
| 435 |
|
| 436 |
|
|
@@ -524,6 +525,10 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
|
|
| 524 |
candidates.extend(["公司", "午觉", "被人泼水"])
|
| 525 |
if "elevator" in text:
|
| 526 |
candidates.append("elevator")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
if re.search(r"\bfloor\s*14\b|\b14\b", text):
|
| 528 |
candidates.append("floor 14")
|
| 529 |
if "button" in text and re.search(r"\b(melt|melted|melting|wax)\b", text):
|
|
@@ -577,6 +582,16 @@ def _english_anchor_text(text: str) -> str:
|
|
| 577 |
return clean
|
| 578 |
|
| 579 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
def _zh_anchor_text(text: str) -> str:
|
| 581 |
clean = re.sub(r"\s+", " ", (text or "").strip(" .,:;!?\"'()[]{}"))
|
| 582 |
if not clean:
|
|
@@ -609,7 +624,7 @@ def _clean_english_today_tip_language(card: TodayTipCard) -> TodayTipCard:
|
|
| 609 |
"caring_note",
|
| 610 |
"safety_note",
|
| 611 |
):
|
| 612 |
-
setattr(cleaned, field,
|
| 613 |
cleaned.dream_anchors = _dedupe_preserve_order(
|
| 614 |
[_english_anchor_text(anchor) for anchor in cleaned.dream_anchors if anchor]
|
| 615 |
)
|
|
@@ -706,6 +721,99 @@ def _story_text(intake: DreamIntake, answers: str = "") -> str:
|
|
| 706 |
).lower()
|
| 707 |
|
| 708 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
def _source_mentions_melted_detail(intake: DreamIntake, answers: str = "") -> bool:
|
| 710 |
text = _story_text(intake, answers)
|
| 711 |
return any(term in text for term in ["融化", "像蜡", "蜡", "melt", "melted", "melting", "wax", "waxy"])
|
|
@@ -839,6 +947,42 @@ def _answer_snippet(answers: str, language: str = "en") -> str:
|
|
| 839 |
return " ".join(words[:22])
|
| 840 |
|
| 841 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 842 |
def _anchor_with_article(anchor: str) -> str:
|
| 843 |
clean = (anchor or "").strip()
|
| 844 |
if clean.lower().startswith(("the ", "a ", "an ")):
|
|
@@ -850,15 +994,62 @@ def _title_anchor(text: str) -> str:
|
|
| 850 |
return " ".join(part.capitalize() for part in text.split())
|
| 851 |
|
| 852 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 853 |
def _summary_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
| 854 |
-
merged = intake.dream_text.strip() or
|
|
|
|
|
|
|
|
|
|
| 855 |
if not merged and intake.visual_clues:
|
| 856 |
merged = "、".join(intake.visual_clues[:3]) if _is_zh(language) else ", ".join(intake.visual_clues[:3])
|
| 857 |
if not merged:
|
| 858 |
return "你记录了一个还在整理中的梦。" if _is_zh(language) else "You recorded a dream that is still taking shape."
|
| 859 |
clean = re.sub(r"\s+", " ", merged).strip()
|
| 860 |
-
|
| 861 |
-
clean = clean[:69].rstrip() + "..."
|
| 862 |
if not _is_zh(language):
|
| 863 |
clean = _clean_placeholder_phrase(_english_anchor_text(clean))
|
| 864 |
if not _is_zh(language):
|
|
@@ -892,10 +1083,17 @@ def _clean_user_question(text: str, language: str = "en") -> str:
|
|
| 892 |
if not clean:
|
| 893 |
return ""
|
| 894 |
if _is_zh(language):
|
|
|
|
| 895 |
clean = re.sub(r"^(我)?(醒来后)?(最)?(想知道|想问|在想|担心|害怕)[::,,\s]*", "", clean)
|
| 896 |
clean = re.sub(r"^(只)?想知道[::,,\s]*", "", clean)
|
| 897 |
clean = clean.strip(" ::「」\"'()[]{}")
|
| 898 |
return clean if clean.endswith(("?", "?")) else f"{clean}?"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 899 |
clean = re.sub(
|
| 900 |
r"^(i\s+)?(woke\s+up\s+)?(want\s+to\s+know|wonder|am\s+wondering|need\s+to\s+know|worry|fear)\s*(if|whether|why|what|how)?\s*",
|
| 901 |
lambda match: (match.group(4) or "").strip() + " ",
|
|
@@ -1221,9 +1419,36 @@ def _fallback_interpretation(intake: DreamIntake, language: str = "en") -> str:
|
|
| 1221 |
)
|
| 1222 |
|
| 1223 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
def _numbered_suggestions(items: List[str], language: str = "en") -> str:
|
| 1225 |
-
cleaned = [item.strip() for item in items if item and item.strip()]
|
| 1226 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1227 |
|
| 1228 |
|
| 1229 |
def _seeded_option(options: List[str], intake: DreamIntake, answers: str = "") -> str:
|
|
@@ -1248,9 +1473,9 @@ def _weird_little_action(intake: DreamIntake, answers: str, anchors: List[str],
|
|
| 1248 |
f"用手指在桌面画一条看不见的海岸线,把一个小物件从「{anchor}」那侧推到自己这侧,像给醒来的身体靠岸。",
|
| 1249 |
],
|
| 1250 |
"stuck_elevator": [
|
| 1251 |
-
"找一张便利贴,画一个
|
| 1252 |
-
"在纸上画三枚电梯按钮:
|
| 1253 |
-
"把一张纸立在
|
| 1254 |
],
|
| 1255 |
"library_signal": [
|
| 1256 |
f"做一张迷你借书卡,书名写「{anchor}」,到期日写“今晚不追讨”;把它夹进一本书里,像把梦暂存进图书馆。",
|
|
@@ -1284,9 +1509,9 @@ def _weird_little_action(intake: DreamIntake, answers: str, anchors: List[str],
|
|
| 1284 |
f"Trace an invisible shoreline on the table and push one small object from the {anchor} side back toward you.",
|
| 1285 |
],
|
| 1286 |
"stuck_elevator": [
|
| 1287 |
-
"Draw
|
| 1288 |
-
"Draw three elevator buttons on paper:
|
| 1289 |
-
"Stand a piece of paper
|
| 1290 |
],
|
| 1291 |
"library_signal": [
|
| 1292 |
f"Make a tiny library card titled {anchor}, set the due date to Not tonight, and tuck it into a book.",
|
|
@@ -1354,7 +1579,7 @@ def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
|
|
| 1354 |
return _numbered_suggestions(
|
| 1355 |
[
|
| 1356 |
f"Use {anchor} to spot the real-life place where you feel stuck at the entrance.",
|
| 1357 |
-
"Choose one waking-life doorway action today:
|
| 1358 |
],
|
| 1359 |
language,
|
| 1360 |
)
|
|
@@ -1403,7 +1628,7 @@ def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
|
|
| 1403 |
return _numbered_suggestions(
|
| 1404 |
[
|
| 1405 |
f"从「{anchor}」找出现实里最像“卡在门口”的一件事。",
|
| 1406 |
-
"今天只选一个醒着能做的入口动作:
|
| 1407 |
],
|
| 1408 |
language,
|
| 1409 |
)
|
|
@@ -1422,8 +1647,16 @@ def _answer_based_tiny_action(
|
|
| 1422 |
anchors: List[str],
|
| 1423 |
language: str = "en",
|
| 1424 |
) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1425 |
lowered = (answers or "").lower()
|
| 1426 |
answer_terms = [
|
|
|
|
|
|
|
| 1427 |
"邮件",
|
| 1428 |
"email",
|
| 1429 |
"消息",
|
|
@@ -1439,31 +1672,64 @@ def _answer_based_tiny_action(
|
|
| 1439 |
"rehearse",
|
| 1440 |
"deadline",
|
| 1441 |
"application",
|
|
|
|
|
|
|
|
|
|
| 1442 |
"apolog",
|
| 1443 |
]
|
| 1444 |
-
|
| 1445 |
-
|
| 1446 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1447 |
|
| 1448 |
|
| 1449 |
def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") -> str:
|
| 1450 |
lowered = (answers or "").lower()
|
|
|
|
| 1451 |
if _is_zh(language):
|
| 1452 |
if "邮件" in lowered or "email" in lowered:
|
| 1453 |
return _numbered_suggestions(
|
| 1454 |
[
|
| 1455 |
f"把「{anchor}」翻译成一个现实沟通问题:这封邮件只需要让对方知道哪一件事。",
|
| 1456 |
-
"先只打开草稿,补上主题和第一句话,把“开始”和“发送”拆开。",
|
| 1457 |
"给自己定一个稍后回看的时间;今天可以先存草稿,不一定马上发出去。",
|
| 1458 |
],
|
| 1459 |
language,
|
| 1460 |
)
|
| 1461 |
if "消息" in lowered or "发消息" in lowered:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1462 |
return _numbered_suggestions(
|
| 1463 |
[
|
| 1464 |
f"把「{anchor}」翻译成现实里一条需要落地的沟通。",
|
| 1465 |
-
"先写一
|
| 1466 |
-
"如果还不确定,就先存草稿,等一个具体时间再决定是否发送。",
|
| 1467 |
],
|
| 1468 |
language,
|
| 1469 |
)
|
|
@@ -1476,16 +1742,49 @@ def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") ->
|
|
| 1476 |
],
|
| 1477 |
language,
|
| 1478 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1479 |
return ""
|
| 1480 |
-
if "email" in lowered
|
|
|
|
| 1481 |
return _numbered_suggestions(
|
| 1482 |
[
|
| 1483 |
-
f"Translate {anchor} into a real-world communication question: what does
|
| 1484 |
"Open the draft only long enough to add the subject and first sentence; separate starting from sending.",
|
| 1485 |
"Save it without sending, then choose one later review time.",
|
| 1486 |
],
|
| 1487 |
language,
|
| 1488 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1489 |
if "assignment" in lowered or "homework" in lowered or "draft" in lowered:
|
| 1490 |
return _numbered_suggestions(
|
| 1491 |
[
|
|
@@ -1511,6 +1810,14 @@ def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") ->
|
|
| 1511 |
],
|
| 1512 |
language,
|
| 1513 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1514 |
if "apolog" in lowered:
|
| 1515 |
return _numbered_suggestions(
|
| 1516 |
[
|
|
@@ -1519,28 +1826,49 @@ def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") ->
|
|
| 1519 |
],
|
| 1520 |
language,
|
| 1521 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1522 |
return ""
|
| 1523 |
|
| 1524 |
|
| 1525 |
def _answer_based_interpretation(answers: str, anchor: str, language: str = "en") -> str:
|
| 1526 |
lowered = (answers or "").lower()
|
|
|
|
| 1527 |
if _is_zh(language):
|
| 1528 |
if "邮件" in lowered or "email" in lowered:
|
| 1529 |
return f"也许「{anchor}」不是在催你立刻完成什么,而是在提醒你:那封邮件可以先从一句话开始。"
|
| 1530 |
if "消息" in lowered or "message" in lowered:
|
| 1531 |
-
|
|
|
|
|
|
|
| 1532 |
if "作业" in lowered or "草稿" in lowered:
|
| 1533 |
return f"也许「{anchor}」不是在说你已经来不及,而是在提醒你:草稿可以先从下一小段开始。"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1534 |
return ""
|
| 1535 |
if "email" in lowered:
|
|
|
|
| 1536 |
return (
|
| 1537 |
-
f"Maybe the {anchor} is not asking you to finish
|
| 1538 |
"It is pointing to the gentler threshold: opening it and writing one first sentence."
|
| 1539 |
)
|
| 1540 |
if "message" in lowered:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1541 |
return (
|
| 1542 |
-
f"Maybe the {anchor} is not asking you to
|
| 1543 |
-
"It is pointing to the gentler threshold:
|
| 1544 |
)
|
| 1545 |
if "assignment" in lowered or "homework" in lowered or "draft" in lowered:
|
| 1546 |
return (
|
|
@@ -1557,11 +1885,21 @@ def _answer_based_interpretation(answers: str, anchor: str, language: str = "en"
|
|
| 1557 |
f"Maybe the {anchor} is not asking you to finish the whole application in one push. "
|
| 1558 |
"It is pointing to the gentler threshold: finding the next missing item."
|
| 1559 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1560 |
if "apolog" in lowered:
|
| 1561 |
return (
|
| 1562 |
f"Maybe the {anchor} is not asking you to repair everything at once. "
|
| 1563 |
"It is pointing to the gentler threshold: drafting one honest sentence."
|
| 1564 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1565 |
return ""
|
| 1566 |
|
| 1567 |
|
|
@@ -2135,6 +2473,13 @@ def _has_unsupported_emotion_or_generic_wellness(text: str, intake: DreamIntake,
|
|
| 2135 |
"保持积极",
|
| 2136 |
"积极心态",
|
| 2137 |
"明天会更好",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2138 |
"喝一杯温水",
|
| 2139 |
"一杯温水",
|
| 2140 |
"温水",
|
|
@@ -2143,11 +2488,150 @@ def _has_unsupported_emotion_or_generic_wellness(text: str, intake: DreamIntake,
|
|
| 2143 |
"stay positive",
|
| 2144 |
"positive mindset",
|
| 2145 |
"tomorrow will be better",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2146 |
"warm water",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2147 |
]
|
| 2148 |
return any(marker in clean for marker in generic_markers)
|
| 2149 |
|
| 2150 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2151 |
def _nonclinical_caring_note(anchors: List[str], language: str = "en") -> str:
|
| 2152 |
anchor = anchors[0] if anchors else ("梦里的这个细节" if _is_zh(language) else "this dream detail")
|
| 2153 |
if _is_zh(language):
|
|
@@ -2155,6 +2639,35 @@ def _nonclinical_caring_note(anchors: List[str], language: str = "en") -> str:
|
|
| 2155 |
return f"Writing this dream down is already enough; today, let {anchor} be one detail you meet gently."
|
| 2156 |
|
| 2157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2158 |
def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = "", language: str = "en") -> TodayTipCard:
|
| 2159 |
polished = card.model_copy(deep=True)
|
| 2160 |
answer_lines = [line.strip() for line in (answers or "").splitlines() if line.strip()]
|
|
@@ -2165,21 +2678,20 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2165 |
intake,
|
| 2166 |
answers,
|
| 2167 |
)
|
|
|
|
| 2168 |
card_anchors = _remove_placeholder_anchors(
|
| 2169 |
polished.dream_anchors
|
| 2170 |
if _is_zh(language)
|
| 2171 |
else _dedupe_preserve_order([_english_anchor_text(anchor) for anchor in polished.dream_anchors])
|
| 2172 |
)
|
| 2173 |
card_anchors = _without_unsupported_melted_anchors(card_anchors, intake, answers)
|
| 2174 |
-
|
| 2175 |
-
|
| 2176 |
-
else:
|
| 2177 |
-
anchors = card_anchors or intake_anchors
|
| 2178 |
anchors = _without_unsupported_melted_anchors(anchors, intake, answers)
|
| 2179 |
if not anchors:
|
| 2180 |
anchors = _remove_placeholder_anchors([_primary_anchor(intake, language)])
|
| 2181 |
if not anchors:
|
| 2182 |
-
anchors = ["梦境
|
| 2183 |
polished.dream_anchors = anchors
|
| 2184 |
for field in (
|
| 2185 |
"dream_summary",
|
|
@@ -2197,7 +2709,12 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2197 |
_clean_unsupported_melted_detail(_clean_placeholder_phrase(question), intake, anchors, language, answers)
|
| 2198 |
for question in polished.followup_questions
|
| 2199 |
]
|
| 2200 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2201 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 2202 |
explicit_question = _extract_explicit_user_question(intake, answers, language)
|
| 2203 |
if explicit_question:
|
|
@@ -2212,14 +2729,22 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2212 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 2213 |
answer_tip = _answer_based_today_tip(answers, anchors[0], language)
|
| 2214 |
answer_action = _answer_based_tiny_action(answers, intake, anchors, language)
|
| 2215 |
-
answer_should_shape_visible_tip = bool(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2216 |
if answer_interpretation and answer_should_shape_visible_tip:
|
| 2217 |
polished.interpretation = answer_interpretation
|
| 2218 |
elif emotion_interpretation:
|
| 2219 |
polished.interpretation = emotion_interpretation
|
| 2220 |
elif answer_interpretation:
|
| 2221 |
polished.interpretation = answer_interpretation
|
| 2222 |
-
elif
|
| 2223 |
anchor = _answer_bridge_anchor(anchors)
|
| 2224 |
polished.interpretation = (
|
| 2225 |
f"Maybe the {anchor} is best treated as a fear-shaped image, not as evidence that something bad will happen."
|
|
@@ -2233,7 +2758,11 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2233 |
if not _is_zh(language)
|
| 2234 |
else f"目前线索很少,先把「{anchor}」当作一个可以继续补充的线索,而不是确定解读。"
|
| 2235 |
)
|
| 2236 |
-
elif
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2237 |
polished.interpretation = _fallback_interpretation(intake, language)
|
| 2238 |
generic_tip_markers = [
|
| 2239 |
"drink water",
|
|
@@ -2246,15 +2775,24 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2246 |
"borrow one action",
|
| 2247 |
"借一个动作",
|
| 2248 |
"open the task",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2249 |
]
|
| 2250 |
emotion_tip = _emotion_led_today_tip(intake, answers, anchors, language)
|
| 2251 |
if answer_tip and answer_should_shape_visible_tip:
|
| 2252 |
polished.today_tip = answer_tip
|
| 2253 |
elif emotion_tip:
|
| 2254 |
polished.today_tip = emotion_tip
|
| 2255 |
-
elif answer_tip:
|
| 2256 |
polished.today_tip = answer_tip
|
| 2257 |
-
elif
|
| 2258 |
anchor = anchors[0]
|
| 2259 |
polished.today_tip = (
|
| 2260 |
f"For today, do not test whether the {anchor} is a sign. Name one ordinary worry it resembles, then choose one small calming step."
|
|
@@ -2273,8 +2811,10 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2273 |
or any(marker in polished.today_tip.lower() for marker in generic_tip_markers)
|
| 2274 |
or _is_placeholder_anchor(polished.today_tip)
|
| 2275 |
or not _anchor_in_text(polished.today_tip, anchors)
|
|
|
|
| 2276 |
):
|
| 2277 |
polished.today_tip = _grounded_today_tip(intake, language)
|
|
|
|
| 2278 |
hard_action_markers = [
|
| 2279 |
"address it immediately",
|
| 2280 |
"fix it immediately",
|
|
@@ -2301,6 +2841,7 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2301 |
or _is_placeholder_anchor(polished.tiny_action)
|
| 2302 |
or not _anchor_in_text(polished.tiny_action, anchors)
|
| 2303 |
or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
|
|
|
|
| 2304 |
):
|
| 2305 |
polished.tiny_action = _weird_little_action(intake, answers, anchors, language)
|
| 2306 |
emotion_caring_note = _emotion_led_caring_note(intake, answers, language)
|
|
@@ -2325,8 +2866,17 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2325 |
polished.caring_note = "You do not have to solve the whole dream this morning; start by noticing one detail and one small next step."
|
| 2326 |
elif _is_zh(language) and "所有楼层" in polished.caring_note:
|
| 2327 |
polished.caring_note = "你不需要一醒来就解释完整个梦;先照顾一个细节和一个很小的下一步就好。"
|
|
|
|
|
|
|
| 2328 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 2329 |
has_escalation = needs_escalation(merged)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2330 |
if not has_escalation:
|
| 2331 |
if _has_unsupported_clinical_frame(polished.interpretation) or _has_unsupported_emotion_or_generic_wellness(
|
| 2332 |
polished.interpretation, intake, answers
|
|
@@ -2345,6 +2895,7 @@ def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = ""
|
|
| 2345 |
):
|
| 2346 |
polished.caring_note = _nonclinical_caring_note(anchors, language)
|
| 2347 |
polished.safety_note = safety_note(language) if has_escalation else ""
|
|
|
|
| 2348 |
if not _is_zh(language):
|
| 2349 |
polished = _clean_english_today_tip_language(polished)
|
| 2350 |
return polished
|
|
|
|
| 431 |
"the dream detail",
|
| 432 |
"dream",
|
| 433 |
"dream fragment",
|
| 434 |
+
"dream fragments",
|
| 435 |
}
|
| 436 |
|
| 437 |
|
|
|
|
| 525 |
candidates.extend(["公司", "午觉", "被人泼水"])
|
| 526 |
if "elevator" in text:
|
| 527 |
candidates.append("elevator")
|
| 528 |
+
if re.search(r"frightening dream|scary dream|recurring dream|same frightening dream", text):
|
| 529 |
+
candidates.append("frightening dream")
|
| 530 |
+
if re.search(r"barely slept|many nights|can't sleep|cannot sleep|could not sleep|insomnia|slept", text):
|
| 531 |
+
candidates.append("lost sleep")
|
| 532 |
if re.search(r"\bfloor\s*14\b|\b14\b", text):
|
| 533 |
candidates.append("floor 14")
|
| 534 |
if "button" in text and re.search(r"\b(melt|melted|melting|wax)\b", text):
|
|
|
|
| 582 |
return clean
|
| 583 |
|
| 584 |
|
| 585 |
+
def _english_public_text(text: str) -> str:
|
| 586 |
+
clean = text or ""
|
| 587 |
+
for source, target in sorted(_ZH_TO_EN_PHRASES.items(), key=lambda item: len(item[0]), reverse=True):
|
| 588 |
+
clean = clean.replace(source, target)
|
| 589 |
+
clean = re.sub(r"[\u4e00-\u9fff]+", " dream fragment ", clean)
|
| 590 |
+
clean = re.sub(r"\s+([,.!?;:])", r"\1", clean)
|
| 591 |
+
clean = re.sub(r"\s+", " ", clean).strip()
|
| 592 |
+
return clean
|
| 593 |
+
|
| 594 |
+
|
| 595 |
def _zh_anchor_text(text: str) -> str:
|
| 596 |
clean = re.sub(r"\s+", " ", (text or "").strip(" .,:;!?\"'()[]{}"))
|
| 597 |
if not clean:
|
|
|
|
| 624 |
"caring_note",
|
| 625 |
"safety_note",
|
| 626 |
):
|
| 627 |
+
setattr(cleaned, field, _english_public_text(getattr(cleaned, field)))
|
| 628 |
cleaned.dream_anchors = _dedupe_preserve_order(
|
| 629 |
[_english_anchor_text(anchor) for anchor in cleaned.dream_anchors if anchor]
|
| 630 |
)
|
|
|
|
| 721 |
).lower()
|
| 722 |
|
| 723 |
|
| 724 |
+
def _provenance_text(intake: DreamIntake, answers: str = "") -> str:
|
| 725 |
+
parts = [
|
| 726 |
+
intake.dream_text,
|
| 727 |
+
intake.voice_transcript,
|
| 728 |
+
" ".join(intake.visual_clues),
|
| 729 |
+
" ".join(intake.recurring_symbols),
|
| 730 |
+
intake.mood,
|
| 731 |
+
intake.main_question,
|
| 732 |
+
intake.uncertainty,
|
| 733 |
+
intake.user_context,
|
| 734 |
+
answers or "",
|
| 735 |
+
]
|
| 736 |
+
return re.sub(r"\s+", " ", " ".join(part.strip() for part in parts if part and part.strip())).lower()
|
| 737 |
+
|
| 738 |
+
|
| 739 |
+
def _source_has_any(source: str, terms: List[str]) -> bool:
|
| 740 |
+
return any(term in source for term in terms)
|
| 741 |
+
|
| 742 |
+
|
| 743 |
+
def _anchor_has_source_provenance(anchor: str, intake: DreamIntake, answers: str = "", language: str = "en") -> bool:
|
| 744 |
+
if _is_placeholder_anchor(anchor):
|
| 745 |
+
return False
|
| 746 |
+
source = _provenance_text(intake, answers)
|
| 747 |
+
clean = _english_anchor_text(anchor).lower() if not _is_zh(language) else anchor.lower()
|
| 748 |
+
clean = re.sub(r"\s+", " ", clean).strip(" .,:;!?\"'()[]{}")
|
| 749 |
+
if not clean or _is_placeholder_anchor(clean):
|
| 750 |
+
return False
|
| 751 |
+
|
| 752 |
+
high_risk_requirements = [
|
| 753 |
+
(["office building", "办公楼"], ["office building", "office tower", "办公楼"]),
|
| 754 |
+
(["overdue email", "email", "e-mail", "邮件"], ["email", "e-mail", "邮件"]),
|
| 755 |
+
(["email draft", "邮件草稿"], ["email", "e-mail", "邮件"]),
|
| 756 |
+
(["first sentence of the email", "email first sentence"], ["email", "e-mail", "邮件"]),
|
| 757 |
+
]
|
| 758 |
+
for markers, required_terms in high_risk_requirements:
|
| 759 |
+
if any(marker in clean for marker in markers):
|
| 760 |
+
return _source_has_any(source, required_terms)
|
| 761 |
+
if "draft" in clean or "草稿" in clean:
|
| 762 |
+
return _source_has_any(
|
| 763 |
+
source,
|
| 764 |
+
[
|
| 765 |
+
"draft",
|
| 766 |
+
"草稿",
|
| 767 |
+
"email",
|
| 768 |
+
"e-mail",
|
| 769 |
+
"邮件",
|
| 770 |
+
"leave request",
|
| 771 |
+
"time off",
|
| 772 |
+
"sick leave",
|
| 773 |
+
"application",
|
| 774 |
+
"assignment",
|
| 775 |
+
"homework",
|
| 776 |
+
"presentation",
|
| 777 |
+
"speech",
|
| 778 |
+
"apolog",
|
| 779 |
+
"请假",
|
| 780 |
+
"申请",
|
| 781 |
+
"作业",
|
| 782 |
+
"演讲",
|
| 783 |
+
"汇报",
|
| 784 |
+
"道歉",
|
| 785 |
+
],
|
| 786 |
+
)
|
| 787 |
+
|
| 788 |
+
if clean in source:
|
| 789 |
+
return True
|
| 790 |
+
if _is_zh(language):
|
| 791 |
+
for source_phrase, target in _EN_TO_ZH_PHRASES.items():
|
| 792 |
+
if target == clean and source_phrase in source:
|
| 793 |
+
return True
|
| 794 |
+
if "elevator" in clean or "电梯" in clean:
|
| 795 |
+
return "elevator" in source or "电梯" in source
|
| 796 |
+
if "phone" in clean or "手机" in clean:
|
| 797 |
+
return _source_has_any(source, ["phone", "手机", "battery", "电量"])
|
| 798 |
+
if "sleep" in clean or "slept" in clean or "失眠" in clean:
|
| 799 |
+
return _source_has_any(source, ["sleep", "slept", "insomnia", "barely slept", "睡", "失眠"])
|
| 800 |
+
if "message" in clean or "消息" in clean:
|
| 801 |
+
return _source_has_any(source, ["message", "messages", "消息", "发消息"])
|
| 802 |
+
tokens = [token for token in re.split(r"[\s,,。::;;、]+", clean) if len(token) >= 3]
|
| 803 |
+
return bool(tokens) and any(token in source for token in tokens)
|
| 804 |
+
|
| 805 |
+
|
| 806 |
+
def _filter_anchors_by_source(
|
| 807 |
+
anchors: List[str],
|
| 808 |
+
intake: DreamIntake,
|
| 809 |
+
answers: str = "",
|
| 810 |
+
language: str = "en",
|
| 811 |
+
) -> List[str]:
|
| 812 |
+
return _dedupe_preserve_order(
|
| 813 |
+
[anchor for anchor in anchors if _anchor_has_source_provenance(anchor, intake, answers, language)]
|
| 814 |
+
)
|
| 815 |
+
|
| 816 |
+
|
| 817 |
def _source_mentions_melted_detail(intake: DreamIntake, answers: str = "") -> bool:
|
| 818 |
text = _story_text(intake, answers)
|
| 819 |
return any(term in text for term in ["融化", "像蜡", "蜡", "melt", "melted", "melting", "wax", "waxy"])
|
|
|
|
| 947 |
return " ".join(words[:22])
|
| 948 |
|
| 949 |
|
| 950 |
+
def _answer_reality_cue(answers: str, language: str = "en") -> str:
|
| 951 |
+
snippet = _answer_snippet(answers, language)
|
| 952 |
+
if not snippet:
|
| 953 |
+
return ""
|
| 954 |
+
lowered = snippet.lower()
|
| 955 |
+
if _is_zh(language):
|
| 956 |
+
lead_ins = [
|
| 957 |
+
"现实里",
|
| 958 |
+
"现实中",
|
| 959 |
+
"可能是",
|
| 960 |
+
"大概是",
|
| 961 |
+
"像是",
|
| 962 |
+
"让我想到",
|
| 963 |
+
"提醒我",
|
| 964 |
+
"其实是",
|
| 965 |
+
]
|
| 966 |
+
for lead in lead_ins:
|
| 967 |
+
if lead in snippet:
|
| 968 |
+
return snippet[snippet.find(lead) :].strip(" ,。;、")
|
| 969 |
+
return snippet
|
| 970 |
+
lead_ins = [
|
| 971 |
+
"in real life",
|
| 972 |
+
"it is probably",
|
| 973 |
+
"it's probably",
|
| 974 |
+
"probably",
|
| 975 |
+
"it reminded me of",
|
| 976 |
+
"it reminds me of",
|
| 977 |
+
"it felt like",
|
| 978 |
+
"it feels like",
|
| 979 |
+
]
|
| 980 |
+
for lead in lead_ins:
|
| 981 |
+
if lead in lowered:
|
| 982 |
+
return snippet[lowered.find(lead) :].strip(" ,.;:")
|
| 983 |
+
return snippet
|
| 984 |
+
|
| 985 |
+
|
| 986 |
def _anchor_with_article(anchor: str) -> str:
|
| 987 |
clean = (anchor or "").strip()
|
| 988 |
if clean.lower().startswith(("the ", "a ", "an ")):
|
|
|
|
| 994 |
return " ".join(part.capitalize() for part in text.split())
|
| 995 |
|
| 996 |
|
| 997 |
+
def _story_portion_for_summary(text: str, language: str = "en") -> str:
|
| 998 |
+
clean = re.sub(r"\s+", " ", (text or "").strip())
|
| 999 |
+
if not clean:
|
| 1000 |
+
return ""
|
| 1001 |
+
if _is_zh(language):
|
| 1002 |
+
parts = re.split(r"(?:我)?(?:最)?想知道|(?:我)?想问|醒来(?:后)?最想知道", clean, maxsplit=1)
|
| 1003 |
+
return parts[0].strip(" ,。;、") or clean
|
| 1004 |
+
parts = re.split(r"\bmy\s+question\s*:|\bi\s+want\s+to\s+know\s*:?", clean, maxsplit=1, flags=re.IGNORECASE)
|
| 1005 |
+
return parts[0].strip(" ,.;:") or clean
|
| 1006 |
+
|
| 1007 |
+
|
| 1008 |
+
def _truncate_summary_text(text: str, language: str = "en", limit: int = 96) -> str:
|
| 1009 |
+
clean = re.sub(r"\s+", " ", (text or "").strip())
|
| 1010 |
+
if len(clean) <= limit:
|
| 1011 |
+
return clean
|
| 1012 |
+
prefix = clean[:limit].rstrip()
|
| 1013 |
+
if _is_zh(language):
|
| 1014 |
+
break_at = max(prefix.rfind(mark) for mark in ("。", "!", "?", ";"))
|
| 1015 |
+
if break_at >= 28:
|
| 1016 |
+
return prefix[: break_at + 1]
|
| 1017 |
+
return prefix.rstrip(",;、 ") + "..."
|
| 1018 |
+
break_at = max(prefix.rfind(mark) for mark in (".", "!", "?", ";"))
|
| 1019 |
+
if break_at >= 42:
|
| 1020 |
+
return prefix[: break_at + 1]
|
| 1021 |
+
word_break = prefix.rfind(" ")
|
| 1022 |
+
if word_break >= 48:
|
| 1023 |
+
prefix = prefix[:word_break]
|
| 1024 |
+
prefix = re.sub(r"\b(?:a|an|the|and|or|but|with|for|to|of|in|at|was|were)\s*$", "", prefix, flags=re.IGNORECASE)
|
| 1025 |
+
return prefix.rstrip(" ,;:") + "..."
|
| 1026 |
+
|
| 1027 |
+
|
| 1028 |
+
def _looks_like_bad_summary(text: str, intake: DreamIntake, answers: str = "", language: str = "en") -> bool:
|
| 1029 |
+
clean = re.sub(r"\s+", " ", (text or "").strip())
|
| 1030 |
+
if not clean:
|
| 1031 |
+
return True
|
| 1032 |
+
if _contains_unprovenanced_detail(clean, intake, answers, language):
|
| 1033 |
+
return True
|
| 1034 |
+
if not _is_zh(language):
|
| 1035 |
+
if re.search(r"\b(?:a|an|the|and|or|but|with|for|to|of|in|at|was|were)\s*$", clean, re.IGNORECASE):
|
| 1036 |
+
return True
|
| 1037 |
+
if len(clean) < 80 and not clean.endswith((".", "!", "?", "...")) and re.search(r"\b\w{1,3}$", clean):
|
| 1038 |
+
return True
|
| 1039 |
+
return False
|
| 1040 |
+
|
| 1041 |
+
|
| 1042 |
def _summary_from_intake(intake: DreamIntake, language: str = "en") -> str:
|
| 1043 |
+
merged = _story_portion_for_summary(intake.dream_text.strip(), language) or _story_portion_for_summary(
|
| 1044 |
+
intake.voice_transcript.strip(),
|
| 1045 |
+
language,
|
| 1046 |
+
)
|
| 1047 |
if not merged and intake.visual_clues:
|
| 1048 |
merged = "、".join(intake.visual_clues[:3]) if _is_zh(language) else ", ".join(intake.visual_clues[:3])
|
| 1049 |
if not merged:
|
| 1050 |
return "你记录了一个还在整理中的梦。" if _is_zh(language) else "You recorded a dream that is still taking shape."
|
| 1051 |
clean = re.sub(r"\s+", " ", merged).strip()
|
| 1052 |
+
clean = _truncate_summary_text(clean, language)
|
|
|
|
| 1053 |
if not _is_zh(language):
|
| 1054 |
clean = _clean_placeholder_phrase(_english_anchor_text(clean))
|
| 1055 |
if not _is_zh(language):
|
|
|
|
| 1083 |
if not clean:
|
| 1084 |
return ""
|
| 1085 |
if _is_zh(language):
|
| 1086 |
+
clean = re.sub(r"^(我)?(的)?(主要)?问题是[::,,\s]*", "", clean)
|
| 1087 |
clean = re.sub(r"^(我)?(醒来后)?(最)?(想知道|想问|在想|担心|害怕)[::,,\s]*", "", clean)
|
| 1088 |
clean = re.sub(r"^(只)?想知道[::,,\s]*", "", clean)
|
| 1089 |
clean = clean.strip(" ::「」\"'()[]{}")
|
| 1090 |
return clean if clean.endswith(("?", "?")) else f"{clean}?"
|
| 1091 |
+
clean = re.sub(
|
| 1092 |
+
r"^(?:my\s+)?(?:main\s+)?question\s*(?:is|was)?\s*[::,\-]?\s*",
|
| 1093 |
+
"",
|
| 1094 |
+
clean,
|
| 1095 |
+
flags=re.IGNORECASE,
|
| 1096 |
+
).strip()
|
| 1097 |
clean = re.sub(
|
| 1098 |
r"^(i\s+)?(woke\s+up\s+)?(want\s+to\s+know|wonder|am\s+wondering|need\s+to\s+know|worry|fear)\s*(if|whether|why|what|how)?\s*",
|
| 1099 |
lambda match: (match.group(4) or "").strip() + " ",
|
|
|
|
| 1419 |
)
|
| 1420 |
|
| 1421 |
|
| 1422 |
+
def _strip_list_marker(text: str) -> str:
|
| 1423 |
+
return re.sub(r"^\s*(?:\d+[\.)]|[-*•])\s*", "", text or "").strip()
|
| 1424 |
+
|
| 1425 |
+
|
| 1426 |
def _numbered_suggestions(items: List[str], language: str = "en") -> str:
|
| 1427 |
+
cleaned = [_strip_list_marker(item.strip()) for item in items if item and item.strip()]
|
| 1428 |
+
cleaned = [item for item in cleaned if item]
|
| 1429 |
+
if not cleaned:
|
| 1430 |
+
return ""
|
| 1431 |
+
first = cleaned[0].rstrip("。.;; ")
|
| 1432 |
+
if len(cleaned) == 1:
|
| 1433 |
+
return first
|
| 1434 |
+
second = cleaned[1].strip()
|
| 1435 |
+
if _is_zh(language):
|
| 1436 |
+
return f"{first};{second}"
|
| 1437 |
+
return f"{first}. {second}"
|
| 1438 |
+
|
| 1439 |
+
|
| 1440 |
+
def _one_tip_text(text: str, language: str = "en") -> str:
|
| 1441 |
+
clean = re.sub(r"\s+", " ", (text or "").strip())
|
| 1442 |
+
if not clean:
|
| 1443 |
+
return ""
|
| 1444 |
+
if not re.search(r"(?:^|\s)\d+[\.)]\s+", clean):
|
| 1445 |
+
return _strip_list_marker(clean)
|
| 1446 |
+
parts = [
|
| 1447 |
+
_strip_list_marker(part)
|
| 1448 |
+
for part in re.split(r"(?:^|\s)\d+[\.)]\s+", clean)
|
| 1449 |
+
if _strip_list_marker(part)
|
| 1450 |
+
]
|
| 1451 |
+
return _numbered_suggestions(parts[:2], language) if parts else _strip_list_marker(clean)
|
| 1452 |
|
| 1453 |
|
| 1454 |
def _seeded_option(options: List[str], intake: DreamIntake, answers: str = "") -> str:
|
|
|
|
| 1473 |
f"用手指在桌面画一条看不见的海岸线,把一个小物件从「{anchor}」那侧推到自己这侧,像给醒来的身体靠岸。",
|
| 1474 |
],
|
| 1475 |
"stuck_elevator": [
|
| 1476 |
+
f"找一张便利贴,画一个写着「{anchor}」的小电梯按钮,贴在桌边;用手指按一下,再写下今天最小的入口动作。",
|
| 1477 |
+
f"在纸上画三枚电梯按钮:等待、开始、暂停;闭眼按一下最像「{anchor}」的那一枚,然后只做一个一分钟动作。",
|
| 1478 |
+
f"把一张纸立在桌边当电梯门,门上写「{anchor}」;推开一厘米,再说出一个今天能先确认的小事实。",
|
| 1479 |
],
|
| 1480 |
"library_signal": [
|
| 1481 |
f"做一张迷你借书卡,书名写「{anchor}」,到期日写“今晚不追讨”;把它夹进一本书里,像把梦暂存进图书馆。",
|
|
|
|
| 1509 |
f"Trace an invisible shoreline on the table and push one small object from the {anchor} side back toward you.",
|
| 1510 |
],
|
| 1511 |
"stuck_elevator": [
|
| 1512 |
+
f"Draw a tiny elevator button labeled {anchor}, press it once with your finger, then name the smallest doorway action you can try today.",
|
| 1513 |
+
f"Draw three elevator buttons on paper: Wait, Start, Pause. Press the one that feels closest to {anchor}, then do one action under a minute.",
|
| 1514 |
+
f"Stand a piece of paper on the desk like elevator doors, write {anchor} on it, open the paper one centimeter, and name one fact you can check today.",
|
| 1515 |
],
|
| 1516 |
"library_signal": [
|
| 1517 |
f"Make a tiny library card titled {anchor}, set the due date to Not tonight, and tuck it into a book.",
|
|
|
|
| 1579 |
return _numbered_suggestions(
|
| 1580 |
[
|
| 1581 |
f"Use {anchor} to spot the real-life place where you feel stuck at the entrance.",
|
| 1582 |
+
"Choose one waking-life doorway action today: check one fact, ask one person, or name what you are waiting for.",
|
| 1583 |
],
|
| 1584 |
language,
|
| 1585 |
)
|
|
|
|
| 1628 |
return _numbered_suggestions(
|
| 1629 |
[
|
| 1630 |
f"从「{anchor}」找出现实里最像“卡在门口”的一件事。",
|
| 1631 |
+
"今天只选一个醒着能做的入口动作:查一个信息、问一个人,或写下自己在等什么。",
|
| 1632 |
],
|
| 1633 |
language,
|
| 1634 |
)
|
|
|
|
| 1647 |
anchors: List[str],
|
| 1648 |
language: str = "en",
|
| 1649 |
) -> str:
|
| 1650 |
+
if _answer_has_concrete_task_keyword(answers):
|
| 1651 |
+
return _weird_little_action(intake, answers, anchors, language)
|
| 1652 |
+
return ""
|
| 1653 |
+
|
| 1654 |
+
|
| 1655 |
+
def _answer_has_concrete_task_keyword(answers: str) -> bool:
|
| 1656 |
lowered = (answers or "").lower()
|
| 1657 |
answer_terms = [
|
| 1658 |
+
"请假",
|
| 1659 |
+
"申请",
|
| 1660 |
"邮件",
|
| 1661 |
"email",
|
| 1662 |
"消息",
|
|
|
|
| 1672 |
"rehearse",
|
| 1673 |
"deadline",
|
| 1674 |
"application",
|
| 1675 |
+
"leave request",
|
| 1676 |
+
"time off",
|
| 1677 |
+
"sick leave",
|
| 1678 |
"apolog",
|
| 1679 |
]
|
| 1680 |
+
return any(term in lowered for term in answer_terms)
|
| 1681 |
+
|
| 1682 |
+
|
| 1683 |
+
def _is_relationship_message_context(text: str) -> bool:
|
| 1684 |
+
lowered = (text or "").lower()
|
| 1685 |
+
relationship_terms = [
|
| 1686 |
+
"friend",
|
| 1687 |
+
"close friend",
|
| 1688 |
+
"misunderstood",
|
| 1689 |
+
"wronged",
|
| 1690 |
+
"hurt",
|
| 1691 |
+
"invisible",
|
| 1692 |
+
"nobody listened",
|
| 1693 |
+
"not angry",
|
| 1694 |
+
"worst possible way",
|
| 1695 |
+
"前任",
|
| 1696 |
+
"朋友",
|
| 1697 |
+
"误解",
|
| 1698 |
+
"委屈",
|
| 1699 |
+
"受伤",
|
| 1700 |
+
"没人听",
|
| 1701 |
+
"看不见",
|
| 1702 |
+
]
|
| 1703 |
+
task_terms = ["work message", "client", "deadline", "email", "overdue", "工作", "客户", "邮件", "截止"]
|
| 1704 |
+
return any(term in lowered for term in relationship_terms) and not any(term in lowered for term in task_terms)
|
| 1705 |
|
| 1706 |
|
| 1707 |
def _answer_based_today_tip(answers: str, anchor: str, language: str = "en") -> str:
|
| 1708 |
lowered = (answers or "").lower()
|
| 1709 |
+
reality_cue = _answer_reality_cue(answers, language)
|
| 1710 |
if _is_zh(language):
|
| 1711 |
if "邮件" in lowered or "email" in lowered:
|
| 1712 |
return _numbered_suggestions(
|
| 1713 |
[
|
| 1714 |
f"把「{anchor}」翻译成一个现实沟通问题:这封邮件只需要让对方知道哪一件事。",
|
| 1715 |
+
"先只打开草稿,补上主题和第一句话,把“开始”和“发送”拆开;今天可以先存草稿,不一定马上发出去。",
|
| 1716 |
"给自己定一个稍后回看的时间;今天可以先存草稿,不一定马上发出去。",
|
| 1717 |
],
|
| 1718 |
language,
|
| 1719 |
)
|
| 1720 |
if "消息" in lowered or "发消息" in lowered:
|
| 1721 |
+
if _is_relationship_message_context(answers):
|
| 1722 |
+
return _numbered_suggestions(
|
| 1723 |
+
[
|
| 1724 |
+
f"把「{anchor}」和那种被误解、没被听见的感觉接起来。",
|
| 1725 |
+
"今天先写一句私密的澄清:我真正希望朋友听懂的是哪一点;不急着立刻发出。",
|
| 1726 |
+
],
|
| 1727 |
+
language,
|
| 1728 |
+
)
|
| 1729 |
return _numbered_suggestions(
|
| 1730 |
[
|
| 1731 |
f"把「{anchor}」翻译成现实里一条需要落地的沟通。",
|
| 1732 |
+
"先写一句它需要传达的核心意思,不要求把整件事立刻完成或发出。",
|
|
|
|
| 1733 |
],
|
| 1734 |
language,
|
| 1735 |
)
|
|
|
|
| 1742 |
],
|
| 1743 |
language,
|
| 1744 |
)
|
| 1745 |
+
if "请假" in lowered or "申请" in lowered:
|
| 1746 |
+
return _numbered_suggestions(
|
| 1747 |
+
[
|
| 1748 |
+
f"把「{anchor}」翻译成现实里那件需要开口的申请,而不是要求自己一次解释清楚所有内疚。",
|
| 1749 |
+
"今天先写一句最普通的请求,说明你需要什么;先存草稿,不急着立刻发送。",
|
| 1750 |
+
],
|
| 1751 |
+
language,
|
| 1752 |
+
)
|
| 1753 |
+
if reality_cue:
|
| 1754 |
+
return _numbered_suggestions(
|
| 1755 |
+
[
|
| 1756 |
+
f"把「{anchor}」和你刚才说的「{reality_cue}」接起来,而不是只解释梦的象征。",
|
| 1757 |
+
"今天只选一个可回头的小动作:写一句说明、问一个入口,或把下一步先放到草稿里。",
|
| 1758 |
+
],
|
| 1759 |
+
language,
|
| 1760 |
+
)
|
| 1761 |
return ""
|
| 1762 |
+
if "email" in lowered:
|
| 1763 |
+
email_label = "the overdue email" if "overdue email" in lowered else "that email"
|
| 1764 |
return _numbered_suggestions(
|
| 1765 |
[
|
| 1766 |
+
f"Translate {anchor} into a real-world communication question: what does {email_label} need the other person to know?",
|
| 1767 |
"Open the draft only long enough to add the subject and first sentence; separate starting from sending.",
|
| 1768 |
"Save it without sending, then choose one later review time.",
|
| 1769 |
],
|
| 1770 |
language,
|
| 1771 |
)
|
| 1772 |
+
if "message" in lowered:
|
| 1773 |
+
if _is_relationship_message_context(answers):
|
| 1774 |
+
return _numbered_suggestions(
|
| 1775 |
+
[
|
| 1776 |
+
f"Connect {anchor} to the friend-message worry you named, especially the feeling of not being heard.",
|
| 1777 |
+
"Write one private clarifying sentence about what you hoped they understood, then decide later whether anything needs to be sent.",
|
| 1778 |
+
],
|
| 1779 |
+
language,
|
| 1780 |
+
)
|
| 1781 |
+
return _numbered_suggestions(
|
| 1782 |
+
[
|
| 1783 |
+
f"Connect {anchor} to the work message pressure you named, without changing it into a different task.",
|
| 1784 |
+
"Write down the one thing the message needs to convey before deciding when to open or send it.",
|
| 1785 |
+
],
|
| 1786 |
+
language,
|
| 1787 |
+
)
|
| 1788 |
if "assignment" in lowered or "homework" in lowered or "draft" in lowered:
|
| 1789 |
return _numbered_suggestions(
|
| 1790 |
[
|
|
|
|
| 1810 |
],
|
| 1811 |
language,
|
| 1812 |
)
|
| 1813 |
+
if "leave request" in lowered or "time off" in lowered or "sick leave" in lowered:
|
| 1814 |
+
return _numbered_suggestions(
|
| 1815 |
+
[
|
| 1816 |
+
f"Translate {anchor} into the leave request you named, not into a verdict about your guilt.",
|
| 1817 |
+
"Draft one plain sentence that states what you need; save it before deciding when to send.",
|
| 1818 |
+
],
|
| 1819 |
+
language,
|
| 1820 |
+
)
|
| 1821 |
if "apolog" in lowered:
|
| 1822 |
return _numbered_suggestions(
|
| 1823 |
[
|
|
|
|
| 1826 |
],
|
| 1827 |
language,
|
| 1828 |
)
|
| 1829 |
+
if reality_cue:
|
| 1830 |
+
return _numbered_suggestions(
|
| 1831 |
+
[
|
| 1832 |
+
f"Connect {anchor} to the waking-life clue you named: \"{reality_cue}\".",
|
| 1833 |
+
"Choose one reversible first step today: draft one sentence, ask for one doorway, or park the next step where you can return to it.",
|
| 1834 |
+
],
|
| 1835 |
+
language,
|
| 1836 |
+
)
|
| 1837 |
return ""
|
| 1838 |
|
| 1839 |
|
| 1840 |
def _answer_based_interpretation(answers: str, anchor: str, language: str = "en") -> str:
|
| 1841 |
lowered = (answers or "").lower()
|
| 1842 |
+
reality_cue = _answer_reality_cue(answers, language)
|
| 1843 |
if _is_zh(language):
|
| 1844 |
if "邮件" in lowered or "email" in lowered:
|
| 1845 |
return f"也许「{anchor}」不是在催你立刻完成什么,而是在提醒你:那封邮件可以先从一句话开始。"
|
| 1846 |
if "消息" in lowered or "message" in lowered:
|
| 1847 |
+
if _is_relationship_message_context(answers):
|
| 1848 |
+
return f"也许「{anchor}」更像是在放大被误解、没被听见的委屈,而不是催你立刻回消息。"
|
| 1849 |
+
return f"也许「{anchor}」不是在催你立刻回应什么,而是在提醒你:那条消息可以先只看清要传达的一点。"
|
| 1850 |
if "作业" in lowered or "草稿" in lowered:
|
| 1851 |
return f"也许「{anchor}」不是在说你已经来不及,而是在提醒你:草稿可以先从下一小段开始。"
|
| 1852 |
+
if "请假" in lowered or "申请" in lowered:
|
| 1853 |
+
return f"也许「{anchor}」不是在责怪你有需求,而是在提醒你:那件申请可以先从一句普通请求开始。"
|
| 1854 |
+
if reality_cue:
|
| 1855 |
+
return f"也许「{anchor}」不是要给梦一个固定解释,而是在把你刚才说的「{reality_cue}」推到更温和的入口。"
|
| 1856 |
return ""
|
| 1857 |
if "email" in lowered:
|
| 1858 |
+
email_label = "the overdue email" if "overdue email" in lowered else "that email"
|
| 1859 |
return (
|
| 1860 |
+
f"Maybe the {anchor} is not asking you to finish {email_label} at once. "
|
| 1861 |
"It is pointing to the gentler threshold: opening it and writing one first sentence."
|
| 1862 |
)
|
| 1863 |
if "message" in lowered:
|
| 1864 |
+
if _is_relationship_message_context(answers):
|
| 1865 |
+
return (
|
| 1866 |
+
f"Maybe the {anchor} is less about answering a thread and more about the hurt of feeling unseen. "
|
| 1867 |
+
"The friend message can wait until you know what felt unheard."
|
| 1868 |
+
)
|
| 1869 |
return (
|
| 1870 |
+
f"Maybe the {anchor} is not asking you to finish every message at once. "
|
| 1871 |
+
"It is pointing to the gentler threshold: naming what the message needs to carry."
|
| 1872 |
)
|
| 1873 |
if "assignment" in lowered or "homework" in lowered or "draft" in lowered:
|
| 1874 |
return (
|
|
|
|
| 1885 |
f"Maybe the {anchor} is not asking you to finish the whole application in one push. "
|
| 1886 |
"It is pointing to the gentler threshold: finding the next missing item."
|
| 1887 |
)
|
| 1888 |
+
if "leave request" in lowered or "time off" in lowered or "sick leave" in lowered:
|
| 1889 |
+
return (
|
| 1890 |
+
f"Maybe the {anchor} is not blaming you for needing care. "
|
| 1891 |
+
"It is pointing to the gentler threshold: drafting one plain leave request sentence."
|
| 1892 |
+
)
|
| 1893 |
if "apolog" in lowered:
|
| 1894 |
return (
|
| 1895 |
f"Maybe the {anchor} is not asking you to repair everything at once. "
|
| 1896 |
"It is pointing to the gentler threshold: drafting one honest sentence."
|
| 1897 |
)
|
| 1898 |
+
if reality_cue:
|
| 1899 |
+
return (
|
| 1900 |
+
f"Maybe the {anchor} is not asking for a fixed dream meaning. "
|
| 1901 |
+
f"It is pointing back to the waking-life clue you named: \"{reality_cue}\"."
|
| 1902 |
+
)
|
| 1903 |
return ""
|
| 1904 |
|
| 1905 |
|
|
|
|
| 2473 |
"保持积极",
|
| 2474 |
"积极心态",
|
| 2475 |
"明天会更好",
|
| 2476 |
+
"优化效率",
|
| 2477 |
+
"提高效率",
|
| 2478 |
+
"待办清单",
|
| 2479 |
+
"待办事项",
|
| 2480 |
+
"保持专注",
|
| 2481 |
+
"按优先级",
|
| 2482 |
+
"优先级排序",
|
| 2483 |
"喝一杯温水",
|
| 2484 |
"一杯温水",
|
| 2485 |
"温水",
|
|
|
|
| 2488 |
"stay positive",
|
| 2489 |
"positive mindset",
|
| 2490 |
"tomorrow will be better",
|
| 2491 |
+
"productivity advice",
|
| 2492 |
+
"productivity hack",
|
| 2493 |
+
"be more productive",
|
| 2494 |
+
"to-do list",
|
| 2495 |
+
"todo list",
|
| 2496 |
+
"sort your tasks",
|
| 2497 |
+
"prioritize your tasks",
|
| 2498 |
"warm water",
|
| 2499 |
+
"cold glass of water",
|
| 2500 |
+
"glass of water",
|
| 2501 |
+
"clear your mind",
|
| 2502 |
+
"feel more grounded",
|
| 2503 |
+
"make you feel more grounded",
|
| 2504 |
+
"calming song",
|
| 2505 |
+
"calming music",
|
| 2506 |
]
|
| 2507 |
return any(marker in clean for marker in generic_markers)
|
| 2508 |
|
| 2509 |
|
| 2510 |
+
def _source_allows_draft_language(source: str) -> bool:
|
| 2511 |
+
return _source_has_any(
|
| 2512 |
+
source,
|
| 2513 |
+
[
|
| 2514 |
+
"draft",
|
| 2515 |
+
"草稿",
|
| 2516 |
+
"email",
|
| 2517 |
+
"e-mail",
|
| 2518 |
+
"邮件",
|
| 2519 |
+
"leave request",
|
| 2520 |
+
"time off",
|
| 2521 |
+
"sick leave",
|
| 2522 |
+
"application",
|
| 2523 |
+
"assignment",
|
| 2524 |
+
"homework",
|
| 2525 |
+
"presentation",
|
| 2526 |
+
"speech",
|
| 2527 |
+
"apolog",
|
| 2528 |
+
"请假",
|
| 2529 |
+
"申请",
|
| 2530 |
+
"作业",
|
| 2531 |
+
"演讲",
|
| 2532 |
+
"汇报",
|
| 2533 |
+
"道歉",
|
| 2534 |
+
],
|
| 2535 |
+
)
|
| 2536 |
+
|
| 2537 |
+
|
| 2538 |
+
def _contains_unprovenanced_detail(text: str, intake: DreamIntake, answers: str = "", language: str = "en") -> bool:
|
| 2539 |
+
clean = re.sub(r"\s+", " ", (text or "").lower())
|
| 2540 |
+
if not clean:
|
| 2541 |
+
return False
|
| 2542 |
+
source = _provenance_text(intake, answers)
|
| 2543 |
+
if "dream fragment" in clean and "dream fragment" not in source:
|
| 2544 |
+
return True
|
| 2545 |
+
if "office building" in clean and not _source_has_any(source, ["office building", "office tower", "办公楼"]):
|
| 2546 |
+
return True
|
| 2547 |
+
if ("elevator" in clean or "电梯" in clean) and not _source_has_any(source, ["elevator", "电梯"]):
|
| 2548 |
+
return True
|
| 2549 |
+
if re.search(r"\bfloor\s*14\b|\b14\b|数字\s*14|14\s*层|14层", clean) and not _source_has_any(
|
| 2550 |
+
source,
|
| 2551 |
+
["floor 14", "number 14", "14", "数字 14", "14 层", "14层"],
|
| 2552 |
+
):
|
| 2553 |
+
return True
|
| 2554 |
+
email_is_allowed = _source_has_any(source, ["email", "e-mail", "邮件"])
|
| 2555 |
+
if re.search(r"\boverdue email\b", clean) and not _source_has_any(
|
| 2556 |
+
source,
|
| 2557 |
+
["overdue email", "unanswered email", "late email", "迟迟没发出去的邮件"],
|
| 2558 |
+
):
|
| 2559 |
+
return True
|
| 2560 |
+
email_patterns = [
|
| 2561 |
+
r"\bemail\b",
|
| 2562 |
+
r"\be-mail\b",
|
| 2563 |
+
r"\bemail draft\b",
|
| 2564 |
+
r"\bfirst sentence of the email\b",
|
| 2565 |
+
"邮件",
|
| 2566 |
+
]
|
| 2567 |
+
if any(re.search(pattern, clean) if pattern.startswith("\\") else pattern in clean for pattern in email_patterns):
|
| 2568 |
+
if not email_is_allowed:
|
| 2569 |
+
return True
|
| 2570 |
+
if re.search(r"\bfirst sentence\b", clean) and not _source_has_any(
|
| 2571 |
+
source,
|
| 2572 |
+
["first sentence", "first line", "第一句", "第一句话", "email", "e-mail", "邮件"],
|
| 2573 |
+
):
|
| 2574 |
+
return True
|
| 2575 |
+
draft_patterns = [
|
| 2576 |
+
r"\bopen the draft\b",
|
| 2577 |
+
r"\bthe draft\b",
|
| 2578 |
+
r"\bdraft\b",
|
| 2579 |
+
r"\bdrafting\b",
|
| 2580 |
+
r"\bdrafted\b",
|
| 2581 |
+
"草稿",
|
| 2582 |
+
]
|
| 2583 |
+
if any(re.search(pattern, clean) if pattern.startswith("\\") else pattern in clean for pattern in draft_patterns):
|
| 2584 |
+
if not _source_allows_draft_language(source):
|
| 2585 |
+
return True
|
| 2586 |
+
if "unsent reply" in clean and not _source_has_any(source, ["message", "contact", "friend", "前任", "消息", "朋友", "联系"]):
|
| 2587 |
+
return True
|
| 2588 |
+
return False
|
| 2589 |
+
|
| 2590 |
+
|
| 2591 |
+
def _requires_distress_surrounding_override(text: str) -> bool:
|
| 2592 |
+
lowered = (text or "").lower()
|
| 2593 |
+
return any(
|
| 2594 |
+
term in lowered
|
| 2595 |
+
for term in [
|
| 2596 |
+
"hurt myself",
|
| 2597 |
+
"kill myself",
|
| 2598 |
+
"do not want to wake up",
|
| 2599 |
+
"don't want to wake up",
|
| 2600 |
+
"hopeless",
|
| 2601 |
+
"can't go on",
|
| 2602 |
+
"cannot go on",
|
| 2603 |
+
"self-harm",
|
| 2604 |
+
"hurt someone",
|
| 2605 |
+
"many nights",
|
| 2606 |
+
"barely slept",
|
| 2607 |
+
"can't sleep",
|
| 2608 |
+
"cannot sleep",
|
| 2609 |
+
"cannot function",
|
| 2610 |
+
"panic attack",
|
| 2611 |
+
"想伤害自己",
|
| 2612 |
+
"自杀",
|
| 2613 |
+
"自残",
|
| 2614 |
+
"不想醒来",
|
| 2615 |
+
"不想活",
|
| 2616 |
+
"活不下去",
|
| 2617 |
+
"轻生",
|
| 2618 |
+
"绝望",
|
| 2619 |
+
"伤害别人",
|
| 2620 |
+
"很多天睡不着",
|
| 2621 |
+
"连续3晚",
|
| 2622 |
+
"连续 3 晚",
|
| 2623 |
+
"连续三晚",
|
| 2624 |
+
"连续很多天",
|
| 2625 |
+
"无法正常生活",
|
| 2626 |
+
"无法生活",
|
| 2627 |
+
]
|
| 2628 |
+
)
|
| 2629 |
+
|
| 2630 |
+
|
| 2631 |
+
def _has_numbered_tip(text: str) -> bool:
|
| 2632 |
+
return bool(re.search(r"(?:^|\s)\d+[\.)]\s+", text or ""))
|
| 2633 |
+
|
| 2634 |
+
|
| 2635 |
def _nonclinical_caring_note(anchors: List[str], language: str = "en") -> str:
|
| 2636 |
anchor = anchors[0] if anchors else ("梦里的这个细节" if _is_zh(language) else "this dream detail")
|
| 2637 |
if _is_zh(language):
|
|
|
|
| 2639 |
return f"Writing this dream down is already enough; today, let {anchor} be one detail you meet gently."
|
| 2640 |
|
| 2641 |
|
| 2642 |
+
def _distress_interpretation(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 2643 |
+
anchor = _story_anchor_phrase(intake, anchors, language, answers)
|
| 2644 |
+
if _is_zh(language):
|
| 2645 |
+
return (
|
| 2646 |
+
f"也许「{anchor}」不是在说明你出了什么问题,而是在提醒你:反复惊醒、很久睡不好、白天难以运转,"
|
| 2647 |
+
"已经值得被认真接住。先把害怕和疲惫放在第一位,不急着给梦下结论。"
|
| 2648 |
+
)
|
| 2649 |
+
return (
|
| 2650 |
+
f"Maybe {anchor} is not proof that something is wrong with you. "
|
| 2651 |
+
"The repeated frightening dream, lost sleep, and trouble functioning are signals that the fear and exhaustion deserve support before interpretation."
|
| 2652 |
+
)
|
| 2653 |
+
|
| 2654 |
+
|
| 2655 |
+
def _distress_today_tip(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 2656 |
+
anchor = _story_anchor_phrase(intake, anchors, language, answers)
|
| 2657 |
+
if _is_zh(language):
|
| 2658 |
+
return f"今天先把「{anchor}」当成需要支持的信号:选一个可信任的人或专业支持入口,说清楚你已经连续睡不好、白天难以运转。"
|
| 2659 |
+
return (
|
| 2660 |
+
f"For today, treat {anchor} as a reason to get support, not as a diagnosis: choose one trusted person or professional support option and tell them the frightening dream and lost sleep are affecting your day."
|
| 2661 |
+
)
|
| 2662 |
+
|
| 2663 |
+
|
| 2664 |
+
def _distress_tiny_action(intake: DreamIntake, answers: str, anchors: List[str], language: str = "en") -> str:
|
| 2665 |
+
anchor = _story_anchor_phrase(intake, anchors, language, answers)
|
| 2666 |
+
if _is_zh(language):
|
| 2667 |
+
return f"在纸上写下「{anchor}」旁边的三个事实:睡了多久、白天哪里最难、可以联系谁;只写事实,不评判自己。"
|
| 2668 |
+
return f"Write three plain facts beside {anchor}: how long this has affected sleep, what is hardest during the day, and who you can contact; keep it factual, not self-blaming."
|
| 2669 |
+
|
| 2670 |
+
|
| 2671 |
def _polish_today_tip(card: TodayTipCard, intake: DreamIntake, answers: str = "", language: str = "en") -> TodayTipCard:
|
| 2672 |
polished = card.model_copy(deep=True)
|
| 2673 |
answer_lines = [line.strip() for line in (answers or "").splitlines() if line.strip()]
|
|
|
|
| 2678 |
intake,
|
| 2679 |
answers,
|
| 2680 |
)
|
| 2681 |
+
intake_anchors = _filter_anchors_by_source(intake_anchors, intake, answers, language)
|
| 2682 |
card_anchors = _remove_placeholder_anchors(
|
| 2683 |
polished.dream_anchors
|
| 2684 |
if _is_zh(language)
|
| 2685 |
else _dedupe_preserve_order([_english_anchor_text(anchor) for anchor in polished.dream_anchors])
|
| 2686 |
)
|
| 2687 |
card_anchors = _without_unsupported_melted_anchors(card_anchors, intake, answers)
|
| 2688 |
+
card_anchors = _filter_anchors_by_source(card_anchors, intake, answers, language)
|
| 2689 |
+
anchors = _dedupe_preserve_order(card_anchors + [anchor for anchor in intake_anchors if anchor not in card_anchors])
|
|
|
|
|
|
|
| 2690 |
anchors = _without_unsupported_melted_anchors(anchors, intake, answers)
|
| 2691 |
if not anchors:
|
| 2692 |
anchors = _remove_placeholder_anchors([_primary_anchor(intake, language)])
|
| 2693 |
if not anchors:
|
| 2694 |
+
anchors = ["梦境线索" if _is_zh(language) else "dream clues"]
|
| 2695 |
polished.dream_anchors = anchors
|
| 2696 |
for field in (
|
| 2697 |
"dream_summary",
|
|
|
|
| 2709 |
_clean_unsupported_melted_detail(_clean_placeholder_phrase(question), intake, anchors, language, answers)
|
| 2710 |
for question in polished.followup_questions
|
| 2711 |
]
|
| 2712 |
+
if (
|
| 2713 |
+
not polished.dream_summary.strip()
|
| 2714 |
+
or _is_placeholder_anchor(polished.dream_summary)
|
| 2715 |
+
or not _text_uses_anchor(polished.dream_summary, anchors)
|
| 2716 |
+
or _looks_like_bad_summary(polished.dream_summary, intake, answers, language)
|
| 2717 |
+
):
|
| 2718 |
polished.dream_summary = _summary_from_intake(intake, language)
|
| 2719 |
explicit_question = _extract_explicit_user_question(intake, answers, language)
|
| 2720 |
if explicit_question:
|
|
|
|
| 2729 |
answer_interpretation = _answer_based_interpretation(answers, _answer_bridge_anchor(anchors), language)
|
| 2730 |
answer_tip = _answer_based_today_tip(answers, anchors[0], language)
|
| 2731 |
answer_action = _answer_based_tiny_action(answers, intake, anchors, language)
|
| 2732 |
+
answer_should_shape_visible_tip = bool(
|
| 2733 |
+
(answer_tip or answer_action)
|
| 2734 |
+
and not _needs_comfort(answers, language)
|
| 2735 |
+
and (
|
| 2736 |
+
_answer_has_concrete_task_keyword(answers)
|
| 2737 |
+
or not _should_use_emotion_led_response(intake, answers, language)
|
| 2738 |
+
)
|
| 2739 |
+
)
|
| 2740 |
+
has_prophecy_frame = _has_prophecy_frame(intake.merged_text())
|
| 2741 |
if answer_interpretation and answer_should_shape_visible_tip:
|
| 2742 |
polished.interpretation = answer_interpretation
|
| 2743 |
elif emotion_interpretation:
|
| 2744 |
polished.interpretation = emotion_interpretation
|
| 2745 |
elif answer_interpretation:
|
| 2746 |
polished.interpretation = answer_interpretation
|
| 2747 |
+
elif has_prophecy_frame:
|
| 2748 |
anchor = _answer_bridge_anchor(anchors)
|
| 2749 |
polished.interpretation = (
|
| 2750 |
f"Maybe the {anchor} is best treated as a fear-shaped image, not as evidence that something bad will happen."
|
|
|
|
| 2758 |
if not _is_zh(language)
|
| 2759 |
else f"目前线索很少,先把「{anchor}」当作一个可以继续补充的线索,而不是确定解读。"
|
| 2760 |
)
|
| 2761 |
+
elif (
|
| 2762 |
+
not polished.interpretation.strip()
|
| 2763 |
+
or not _anchor_in_text(polished.interpretation, anchors)
|
| 2764 |
+
or _contains_unprovenanced_detail(polished.interpretation, intake, answers, language)
|
| 2765 |
+
):
|
| 2766 |
polished.interpretation = _fallback_interpretation(intake, language)
|
| 2767 |
generic_tip_markers = [
|
| 2768 |
"drink water",
|
|
|
|
| 2775 |
"borrow one action",
|
| 2776 |
"借一个动作",
|
| 2777 |
"open the task",
|
| 2778 |
+
"for the tiny action",
|
| 2779 |
+
"cold glass of water",
|
| 2780 |
+
"glass of water",
|
| 2781 |
+
"clear your mind",
|
| 2782 |
+
"feel more grounded",
|
| 2783 |
+
"make you feel more grounded",
|
| 2784 |
+
"calming song",
|
| 2785 |
+
"calming music",
|
| 2786 |
+
"listen to a short",
|
| 2787 |
]
|
| 2788 |
emotion_tip = _emotion_led_today_tip(intake, answers, anchors, language)
|
| 2789 |
if answer_tip and answer_should_shape_visible_tip:
|
| 2790 |
polished.today_tip = answer_tip
|
| 2791 |
elif emotion_tip:
|
| 2792 |
polished.today_tip = emotion_tip
|
| 2793 |
+
elif answer_tip and not has_prophecy_frame and not _should_use_emotion_led_response(intake, answers, language):
|
| 2794 |
polished.today_tip = answer_tip
|
| 2795 |
+
elif has_prophecy_frame:
|
| 2796 |
anchor = anchors[0]
|
| 2797 |
polished.today_tip = (
|
| 2798 |
f"For today, do not test whether the {anchor} is a sign. Name one ordinary worry it resembles, then choose one small calming step."
|
|
|
|
| 2811 |
or any(marker in polished.today_tip.lower() for marker in generic_tip_markers)
|
| 2812 |
or _is_placeholder_anchor(polished.today_tip)
|
| 2813 |
or not _anchor_in_text(polished.today_tip, anchors)
|
| 2814 |
+
or _contains_unprovenanced_detail(polished.today_tip, intake, answers, language)
|
| 2815 |
):
|
| 2816 |
polished.today_tip = _grounded_today_tip(intake, language)
|
| 2817 |
+
polished.today_tip = _one_tip_text(polished.today_tip, language)
|
| 2818 |
hard_action_markers = [
|
| 2819 |
"address it immediately",
|
| 2820 |
"fix it immediately",
|
|
|
|
| 2841 |
or _is_placeholder_anchor(polished.tiny_action)
|
| 2842 |
or not _anchor_in_text(polished.tiny_action, anchors)
|
| 2843 |
or any(marker in polished.tiny_action.lower() for marker in hard_action_markers)
|
| 2844 |
+
or _contains_unprovenanced_detail(polished.tiny_action, intake, answers, language)
|
| 2845 |
):
|
| 2846 |
polished.tiny_action = _weird_little_action(intake, answers, anchors, language)
|
| 2847 |
emotion_caring_note = _emotion_led_caring_note(intake, answers, language)
|
|
|
|
| 2866 |
polished.caring_note = "You do not have to solve the whole dream this morning; start by noticing one detail and one small next step."
|
| 2867 |
elif _is_zh(language) and "所有楼层" in polished.caring_note:
|
| 2868 |
polished.caring_note = "你不需要一醒来就解释完整个梦;先照顾一个细节和一个很小的下一步就好。"
|
| 2869 |
+
elif _contains_unprovenanced_detail(polished.caring_note, intake, answers, language):
|
| 2870 |
+
polished.caring_note = _nonclinical_caring_note(anchors, language)
|
| 2871 |
merged = "\n".join([intake.merged_text(), answers or ""])
|
| 2872 |
has_escalation = needs_escalation(merged)
|
| 2873 |
+
if has_escalation and _requires_distress_surrounding_override(merged):
|
| 2874 |
+
polished.interpretation = _distress_interpretation(intake, answers, anchors, language)
|
| 2875 |
+
polished.today_tip = _distress_today_tip(intake, answers, anchors, language)
|
| 2876 |
+
if _contains_unprovenanced_detail(polished.tiny_action, intake, answers, language):
|
| 2877 |
+
polished.tiny_action = _distress_tiny_action(intake, answers, anchors, language)
|
| 2878 |
+
if _contains_unprovenanced_detail(polished.caring_note, intake, answers, language):
|
| 2879 |
+
polished.caring_note = _nonclinical_caring_note(anchors, language)
|
| 2880 |
if not has_escalation:
|
| 2881 |
if _has_unsupported_clinical_frame(polished.interpretation) or _has_unsupported_emotion_or_generic_wellness(
|
| 2882 |
polished.interpretation, intake, answers
|
|
|
|
| 2895 |
):
|
| 2896 |
polished.caring_note = _nonclinical_caring_note(anchors, language)
|
| 2897 |
polished.safety_note = safety_note(language) if has_escalation else ""
|
| 2898 |
+
polished.today_tip = _one_tip_text(polished.today_tip, language)
|
| 2899 |
if not _is_zh(language):
|
| 2900 |
polished = _clean_english_today_tip_language(polished)
|
| 2901 |
return polished
|
dream_customs/prompts.py
CHANGED
|
@@ -54,18 +54,18 @@ Return strict JSON with:
|
|
| 54 |
def today_tip_prompt(state: DreamQAState, language: str = "en") -> str:
|
| 55 |
return f"""
|
| 56 |
You are MiniCPM5-1B writing the final Dream QA result.
|
| 57 |
-
Write a non-diagnostic interpretation draft,
|
| 58 |
and one weird little thing / 古怪的小事.
|
| 59 |
First answer the user's stated question directly. If the user sounds scared, sad,
|
| 60 |
overwhelmed, guilty, lonely, or asks for comfort, follow that emotion before giving any action.
|
| 61 |
The interpretation must be step-by-step: use 2 to 4 short layers that move from
|
| 62 |
-
the user's feeling, to concrete dream anchors, to the
|
| 63 |
Do not collapse every dream into productivity advice such as opening a task,
|
| 64 |
writing a first line, or making the first step smaller.
|
| 65 |
Use non-certain language such as "也许", "可以把它当作", "maybe", or "for today, try".
|
| 66 |
The today_tip must be about the user's awake, real-world life, not about acting inside the dream scene.
|
| 67 |
-
It
|
| 68 |
-
ordinary
|
| 69 |
The tiny_action field is the weird little thing / 古怪的小事. It must cite at least one concrete dream anchor,
|
| 70 |
use real-world physics or an ordinary physical object, and create one strange, playful, eye-opening action
|
| 71 |
the user can actually do in 1 to 5 minutes while awake. It should feel random and fresh, not like a stock
|
|
|
|
| 54 |
def today_tip_prompt(state: DreamQAState, language: str = "en") -> str:
|
| 55 |
return f"""
|
| 56 |
You are MiniCPM5-1B writing the final Dream QA result.
|
| 57 |
+
Write a non-diagnostic interpretation draft, one waking-life Today Tip / 今日小 Tips,
|
| 58 |
and one weird little thing / 古怪的小事.
|
| 59 |
First answer the user's stated question directly. If the user sounds scared, sad,
|
| 60 |
overwhelmed, guilty, lonely, or asks for comfort, follow that emotion before giving any action.
|
| 61 |
The interpretation must be step-by-step: use 2 to 4 short layers that move from
|
| 62 |
+
the user's feeling, to concrete dream anchors, to the follow-up answers, to one gentle way to care for today.
|
| 63 |
Do not collapse every dream into productivity advice such as opening a task,
|
| 64 |
writing a first line, or making the first step smaller.
|
| 65 |
Use non-certain language such as "也许", "可以把它当作", "maybe", or "for today, try".
|
| 66 |
The today_tip must be about the user's awake, real-world life, not about acting inside the dream scene.
|
| 67 |
+
It must refer to at least one concrete dream anchor and translate it into one practical waking-life choice,
|
| 68 |
+
ordinary constraint, or real-world consequence. Return a single grounded tip, not a numbered list or multiple instructions.
|
| 69 |
The tiny_action field is the weird little thing / 古怪的小事. It must cite at least one concrete dream anchor,
|
| 70 |
use real-world physics or an ordinary physical object, and create one strange, playful, eye-opening action
|
| 71 |
the user can actually do in 1 to 5 minutes while awake. It should feel random and fresh, not like a stock
|
dream_customs/ui/actions.py
CHANGED
|
@@ -108,6 +108,19 @@ def _card_plain_text(card: TodayTipCard, language: str) -> str:
|
|
| 108 |
return "\n".join(lines)
|
| 109 |
|
| 110 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
def _render_today_pass(card: TodayTipCard, language: str) -> str:
|
| 112 |
return render_today_tip_card(card, language=language)
|
| 113 |
|
|
@@ -139,7 +152,8 @@ def _view_payload(
|
|
| 139 |
else:
|
| 140 |
status = "record"
|
| 141 |
copy = copy_for(language)
|
| 142 |
-
|
|
|
|
| 143 |
payload = {
|
| 144 |
"status": status,
|
| 145 |
"phase": session.phase,
|
|
|
|
| 108 |
return "\n".join(lines)
|
| 109 |
|
| 110 |
|
| 111 |
+
def _sanitize_visible_card(card: TodayTipCard, language: str) -> TodayTipCard:
|
| 112 |
+
visible = card.model_copy(deep=True)
|
| 113 |
+
caring_note = (visible.caring_note or "").lower()
|
| 114 |
+
if "dream fragment" in caring_note or "梦境片段" in caring_note:
|
| 115 |
+
anchor = visible.dream_anchors[0] if visible.dream_anchors else ("梦里的这个细节" if language == "zh" else "this dream detail")
|
| 116 |
+
visible.caring_note = (
|
| 117 |
+
f"今天先把「{anchor}」当作一个被看见的线索,不急着把整个梦解释完。"
|
| 118 |
+
if language == "zh"
|
| 119 |
+
else f"You do not have to solve the whole dream today; let {anchor} be one detail you meet gently."
|
| 120 |
+
)
|
| 121 |
+
return visible
|
| 122 |
+
|
| 123 |
+
|
| 124 |
def _render_today_pass(card: TodayTipCard, language: str) -> str:
|
| 125 |
return render_today_tip_card(card, language=language)
|
| 126 |
|
|
|
|
| 152 |
else:
|
| 153 |
status = "record"
|
| 154 |
copy = copy_for(language)
|
| 155 |
+
raw_card = None if status == "ask" else session.sealed_tip or session.draft_tip
|
| 156 |
+
card = _sanitize_visible_card(raw_card, language) if raw_card else None
|
| 157 |
payload = {
|
| 158 |
"status": status,
|
| 159 |
"phase": session.phase,
|
dream_customs/ui/app.py
CHANGED
|
@@ -455,14 +455,52 @@ VOICE_JS = r"""
|
|
| 455 |
};
|
| 456 |
|
| 457 |
const bindProcessingButtons = () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
const bindButton = (selector, mode) => {
|
| 459 |
document.querySelectorAll(selector).forEach((root) => {
|
| 460 |
const button = root.matches("button") ? root : root.querySelector("button");
|
| 461 |
-
if (!button
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
return;
|
| 463 |
}
|
| 464 |
button.dataset.processingBound = "true";
|
| 465 |
-
button.addEventListener("click", () =>
|
|
|
|
|
|
|
|
|
|
| 466 |
});
|
| 467 |
};
|
| 468 |
|
|
@@ -518,9 +556,43 @@ def _notice_html(view: dict) -> str:
|
|
| 518 |
return f"<div class='{css}'>{message}</div>" if message else ""
|
| 519 |
|
| 520 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
def _question_markdown(view: dict, language: str = DEFAULT_LANGUAGE) -> str:
|
| 522 |
copy = copy_for(language)
|
| 523 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
anchor_items = [str(anchor).strip() for anchor in view.get("dream_anchors", [])[:3] if str(anchor).strip()]
|
| 525 |
anchor_label = "梦境锚点" if language == "zh" else "Dream anchors"
|
| 526 |
anchor_chips = "".join(f"<span>{escape(anchor)}</span>" for anchor in anchor_items)
|
|
@@ -533,7 +605,7 @@ def _question_markdown(view: dict, language: str = DEFAULT_LANGUAGE) -> str:
|
|
| 533 |
else ""
|
| 534 |
)
|
| 535 |
optional_question = (
|
| 536 |
-
f"<
|
| 537 |
if question
|
| 538 |
else ""
|
| 539 |
)
|
|
@@ -544,6 +616,7 @@ def _question_markdown(view: dict, language: str = DEFAULT_LANGUAGE) -> str:
|
|
| 544 |
<p>{copy['question_body']}</p>
|
| 545 |
{anchor_strip}
|
| 546 |
{optional_question}
|
|
|
|
| 547 |
<p class="dc-question-note">{copy['question_note']}</p>
|
| 548 |
</div>
|
| 549 |
""".strip()
|
|
@@ -756,7 +829,7 @@ def _agent_dream_qa(dream_text: str, mood: str = "", answer: str = "", language:
|
|
| 756 |
|
| 757 |
|
| 758 |
def _active_step_for_status(status: str) -> int:
|
| 759 |
-
return {"record": 1, "error": 1, "ask":
|
| 760 |
|
| 761 |
|
| 762 |
def _hero_html(language: str = DEFAULT_LANGUAGE, status: str = "record") -> str:
|
|
@@ -1069,13 +1142,6 @@ def build_demo() -> gr.Blocks:
|
|
| 1069 |
show_copy_button=True,
|
| 1070 |
elem_classes=["dc-hidden-text"],
|
| 1071 |
)
|
| 1072 |
-
with gr.Accordion(initial_copy["debug_title"], open=False, elem_classes=["dc-debug-panel"]) as debug_panel:
|
| 1073 |
-
debug_help_html = gr.HTML(_debug_help_html(DEFAULT_LANGUAGE))
|
| 1074 |
-
debug_json = gr.Code(
|
| 1075 |
-
label=initial_copy["debug_state_label"],
|
| 1076 |
-
value=json.dumps(initial.get("debug", {}), ensure_ascii=False, indent=2),
|
| 1077 |
-
language="json",
|
| 1078 |
-
)
|
| 1079 |
|
| 1080 |
with gr.Column(elem_classes=["dc-side-panel"]):
|
| 1081 |
language = gr.Radio(
|
|
@@ -1176,6 +1242,13 @@ def build_demo() -> gr.Blocks:
|
|
| 1176 |
value=DEFAULT_ASR_LATENCY_BUDGET_MS,
|
| 1177 |
precision=0,
|
| 1178 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1179 |
|
| 1180 |
with gr.Group(visible=False):
|
| 1181 |
agent_dream_text = gr.Textbox(label="Agent dream text")
|
|
|
|
| 455 |
};
|
| 456 |
|
| 457 |
const bindProcessingButtons = () => {
|
| 458 |
+
const setBusyButton = (button, mode) => {
|
| 459 |
+
const isZh = currentLanguage() === "zh";
|
| 460 |
+
const label = mode === "submit"
|
| 461 |
+
? (isZh ? "正在整理线索..." : "Reading clues...")
|
| 462 |
+
: mode === "question"
|
| 463 |
+
? (isZh ? "正在换角度..." : "Finding another angle...")
|
| 464 |
+
: (isZh ? "正在生成 Tips..." : "Writing the tip...");
|
| 465 |
+
if (!button.dataset.originalText) {
|
| 466 |
+
button.dataset.originalText = button.textContent.trim();
|
| 467 |
+
}
|
| 468 |
+
button.textContent = label;
|
| 469 |
+
button.classList.add("dc-is-loading");
|
| 470 |
+
button.setAttribute("aria-busy", "true");
|
| 471 |
+
button.dataset.busyStarted = String(Date.now());
|
| 472 |
+
};
|
| 473 |
+
|
| 474 |
+
const resetSettledButton = (button) => {
|
| 475 |
+
const startedAt = Number(button.dataset.busyStarted || "0");
|
| 476 |
+
if (
|
| 477 |
+
button.dataset.originalText
|
| 478 |
+
&& button.getAttribute("aria-busy") === "true"
|
| 479 |
+
&& startedAt
|
| 480 |
+
&& Date.now() - startedAt > 700
|
| 481 |
+
) {
|
| 482 |
+
button.textContent = button.dataset.originalText;
|
| 483 |
+
button.classList.remove("dc-is-loading");
|
| 484 |
+
button.removeAttribute("aria-busy");
|
| 485 |
+
delete button.dataset.busyStarted;
|
| 486 |
+
}
|
| 487 |
+
};
|
| 488 |
+
|
| 489 |
const bindButton = (selector, mode) => {
|
| 490 |
document.querySelectorAll(selector).forEach((root) => {
|
| 491 |
const button = root.matches("button") ? root : root.querySelector("button");
|
| 492 |
+
if (!button) {
|
| 493 |
+
return;
|
| 494 |
+
}
|
| 495 |
+
resetSettledButton(button);
|
| 496 |
+
if (button.dataset.processingBound === "true") {
|
| 497 |
return;
|
| 498 |
}
|
| 499 |
button.dataset.processingBound = "true";
|
| 500 |
+
button.addEventListener("click", () => {
|
| 501 |
+
setProcessingCopy(mode);
|
| 502 |
+
setBusyButton(button, mode);
|
| 503 |
+
});
|
| 504 |
});
|
| 505 |
};
|
| 506 |
|
|
|
|
| 556 |
return f"<div class='{css}'>{message}</div>" if message else ""
|
| 557 |
|
| 558 |
|
| 559 |
+
def _split_question_for_display(raw_question: str) -> tuple[str, str]:
|
| 560 |
+
question = " ".join(str(raw_question or "").split())
|
| 561 |
+
if not question:
|
| 562 |
+
return "", ""
|
| 563 |
+
if "?" not in question:
|
| 564 |
+
return question, ""
|
| 565 |
+
|
| 566 |
+
question_end = question.rfind("?") + 1
|
| 567 |
+
prefix = question[:question_end]
|
| 568 |
+
suffix = question[question_end:].strip()
|
| 569 |
+
candidate = prefix
|
| 570 |
+
for marker in (": ", ":"):
|
| 571 |
+
if marker in candidate:
|
| 572 |
+
candidate = candidate.rsplit(marker, 1)[-1].strip()
|
| 573 |
+
if len(candidate) < 12:
|
| 574 |
+
candidate = prefix.strip()
|
| 575 |
+
|
| 576 |
+
context = question[: max(0, question.find(candidate))].strip(" ::")
|
| 577 |
+
if suffix:
|
| 578 |
+
context = f"{context} {suffix}".strip()
|
| 579 |
+
if context == candidate:
|
| 580 |
+
context = ""
|
| 581 |
+
return candidate, context
|
| 582 |
+
|
| 583 |
+
|
| 584 |
def _question_markdown(view: dict, language: str = DEFAULT_LANGUAGE) -> str:
|
| 585 |
copy = copy_for(language)
|
| 586 |
+
primary_question, context = _split_question_for_display(view.get("question") or "")
|
| 587 |
+
question = escape(primary_question)
|
| 588 |
+
context_html = (
|
| 589 |
+
"<details class='dc-question-context'>"
|
| 590 |
+
f"<summary>{escape(copy['question_context_label'])}</summary>"
|
| 591 |
+
f"<p>{escape(context)}</p>"
|
| 592 |
+
"</details>"
|
| 593 |
+
if context
|
| 594 |
+
else ""
|
| 595 |
+
)
|
| 596 |
anchor_items = [str(anchor).strip() for anchor in view.get("dream_anchors", [])[:3] if str(anchor).strip()]
|
| 597 |
anchor_label = "梦境锚点" if language == "zh" else "Dream anchors"
|
| 598 |
anchor_chips = "".join(f"<span>{escape(anchor)}</span>" for anchor in anchor_items)
|
|
|
|
| 605 |
else ""
|
| 606 |
)
|
| 607 |
optional_question = (
|
| 608 |
+
f"<div class='dc-question-original'><span>{copy['question_speaker']}</span><p>{question}</p></div>"
|
| 609 |
if question
|
| 610 |
else ""
|
| 611 |
)
|
|
|
|
| 616 |
<p>{copy['question_body']}</p>
|
| 617 |
{anchor_strip}
|
| 618 |
{optional_question}
|
| 619 |
+
{context_html}
|
| 620 |
<p class="dc-question-note">{copy['question_note']}</p>
|
| 621 |
</div>
|
| 622 |
""".strip()
|
|
|
|
| 829 |
|
| 830 |
|
| 831 |
def _active_step_for_status(status: str) -> int:
|
| 832 |
+
return {"record": 1, "error": 1, "ask": 3, "drafting": 3, "tip": 4}.get(status, 1)
|
| 833 |
|
| 834 |
|
| 835 |
def _hero_html(language: str = DEFAULT_LANGUAGE, status: str = "record") -> str:
|
|
|
|
| 1142 |
show_copy_button=True,
|
| 1143 |
elem_classes=["dc-hidden-text"],
|
| 1144 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1145 |
|
| 1146 |
with gr.Column(elem_classes=["dc-side-panel"]):
|
| 1147 |
language = gr.Radio(
|
|
|
|
| 1242 |
value=DEFAULT_ASR_LATENCY_BUDGET_MS,
|
| 1243 |
precision=0,
|
| 1244 |
)
|
| 1245 |
+
with gr.Accordion(initial_copy["debug_title"], open=False, elem_classes=["dc-debug-panel"]) as debug_panel:
|
| 1246 |
+
debug_help_html = gr.HTML(_debug_help_html(DEFAULT_LANGUAGE))
|
| 1247 |
+
debug_json = gr.Code(
|
| 1248 |
+
label=initial_copy["debug_state_label"],
|
| 1249 |
+
value=json.dumps(initial.get("debug", {}), ensure_ascii=False, indent=2),
|
| 1250 |
+
language="json",
|
| 1251 |
+
)
|
| 1252 |
|
| 1253 |
with gr.Group(visible=False):
|
| 1254 |
agent_dream_text = gr.Textbox(label="Agent dream text")
|
dream_customs/ui/copy.py
CHANGED
|
@@ -15,7 +15,7 @@ APP_COPY = {
|
|
| 15 |
"hero_badge": "Text · image · voice",
|
| 16 |
"hero_mobile_note": "Half-awake friendly",
|
| 17 |
"brand_subtitle": "Dream Customs",
|
| 18 |
-
"steps": ["Record", "
|
| 19 |
"notice_record": "Write one dream fragment, then use a demo chip if you want the 90-second judge path.",
|
| 20 |
"notice_ask": "Answer this one question, or skip it and turn the existing clues into a Morning Ticket.",
|
| 21 |
"notice_tip": "Your Morning Ticket is ready. Treat it as gentle reflection, not diagnosis or prophecy.",
|
|
@@ -48,12 +48,13 @@ APP_COPY = {
|
|
| 48 |
"image_paste": "Paste from Clipboard",
|
| 49 |
"demo_intro_label": "Dream slips",
|
| 50 |
"demo_intro_body": "Pick one if you want the fast judge path.",
|
| 51 |
-
"question_kicker": "
|
| 52 |
-
"question_title": "One
|
| 53 |
"question_body": "Answer in one or two lines, or skip and let the existing anchors carry the ticket.",
|
| 54 |
"question_speaker": "Morning Question Desk",
|
| 55 |
"question_note": "This step makes the tip more specific. It is not diagnosis.",
|
| 56 |
"question_anchor_label": "Sticky details already on the desk",
|
|
|
|
| 57 |
"answer_label": "Your answer",
|
| 58 |
"answer_placeholder": "Write one answer, or leave it blank and skip.",
|
| 59 |
"answer_button": "Send answer",
|
|
@@ -90,7 +91,7 @@ APP_COPY = {
|
|
| 90 |
"hero_badge": "文字 · 图片 · 语音",
|
| 91 |
"hero_mobile_note": "适合半醒时使用",
|
| 92 |
"brand_subtitle": "Dream Customs",
|
| 93 |
-
"steps": ["记录", "
|
| 94 |
"notice_record": "写一个梦境片段;想走 90 秒演示路径,也可以直接点示例 chip。",
|
| 95 |
"notice_ask": "回答这一个追问,或跳过,用已有梦境线索生成清晨小票。",
|
| 96 |
"notice_tip": "清晨小票已生成。把它当作温和参考,不是诊断或预言。",
|
|
@@ -123,12 +124,13 @@ APP_COPY = {
|
|
| 123 |
"image_paste": "从剪贴板粘贴",
|
| 124 |
"demo_intro_label": "梦境纸片",
|
| 125 |
"demo_intro_body": "想快速演示,可以先抽一张。",
|
| 126 |
-
"question_kicker": "
|
| 127 |
-
"question_title": "
|
| 128 |
"question_body": "回答一两句就好;也可以跳过,让已有锚点直接生成小票。",
|
| 129 |
"question_speaker": "清晨问讯室",
|
| 130 |
"question_note": "这个步骤是为了让最终建议更贴近你的梦,不是问诊。",
|
| 131 |
"question_anchor_label": "桌面上已经留下的细节",
|
|
|
|
| 132 |
"answer_label": "你的回答",
|
| 133 |
"answer_placeholder": "写一句回答,或留空后选择跳过。",
|
| 134 |
"answer_button": "发送回答",
|
|
|
|
| 15 |
"hero_badge": "Text · image · voice",
|
| 16 |
"hero_mobile_note": "Half-awake friendly",
|
| 17 |
"brand_subtitle": "Dream Customs",
|
| 18 |
+
"steps": ["Record", "Clarify", "Answer", "Tip"],
|
| 19 |
"notice_record": "Write one dream fragment, then use a demo chip if you want the 90-second judge path.",
|
| 20 |
"notice_ask": "Answer this one question, or skip it and turn the existing clues into a Morning Ticket.",
|
| 21 |
"notice_tip": "Your Morning Ticket is ready. Treat it as gentle reflection, not diagnosis or prophecy.",
|
|
|
|
| 48 |
"image_paste": "Paste from Clipboard",
|
| 49 |
"demo_intro_label": "Dream slips",
|
| 50 |
"demo_intro_body": "Pick one if you want the fast judge path.",
|
| 51 |
+
"question_kicker": "Step 3 of 4 · Answer or skip",
|
| 52 |
+
"question_title": "One useful question",
|
| 53 |
"question_body": "Answer in one or two lines, or skip and let the existing anchors carry the ticket.",
|
| 54 |
"question_speaker": "Morning Question Desk",
|
| 55 |
"question_note": "This step makes the tip more specific. It is not diagnosis.",
|
| 56 |
"question_anchor_label": "Sticky details already on the desk",
|
| 57 |
+
"question_context_label": "Why this question",
|
| 58 |
"answer_label": "Your answer",
|
| 59 |
"answer_placeholder": "Write one answer, or leave it blank and skip.",
|
| 60 |
"answer_button": "Send answer",
|
|
|
|
| 91 |
"hero_badge": "文字 · 图片 · 语音",
|
| 92 |
"hero_mobile_note": "适合半醒时使用",
|
| 93 |
"brand_subtitle": "Dream Customs",
|
| 94 |
+
"steps": ["记录", "明确问题", "回答追问", "Tips"],
|
| 95 |
"notice_record": "写一个梦境片段;想走 90 秒演示路径,也可以直接点示例 chip。",
|
| 96 |
"notice_ask": "回答这一个追问,或跳过,用已有梦境线索生成清晨小票。",
|
| 97 |
"notice_tip": "清晨小票已生成。把它当作温和参考,不是诊断或预言。",
|
|
|
|
| 124 |
"image_paste": "从剪贴板粘贴",
|
| 125 |
"demo_intro_label": "梦境纸片",
|
| 126 |
"demo_intro_body": "想快速演示,可以先抽一张。",
|
| 127 |
+
"question_kicker": "第 3 / 4 步 · 回答或跳过",
|
| 128 |
+
"question_title": "一个有用的追问",
|
| 129 |
"question_body": "回答一两句就好;也可以跳过,让已有锚点直接生成小票。",
|
| 130 |
"question_speaker": "清晨问讯室",
|
| 131 |
"question_note": "这个步骤是为了让最终建议更贴近你的梦,不是问诊。",
|
| 132 |
"question_anchor_label": "桌面上已经留下的细节",
|
| 133 |
+
"question_context_label": "为什么问这个",
|
| 134 |
"answer_label": "你的回答",
|
| 135 |
"answer_placeholder": "写一句回答,或留空后选择跳过。",
|
| 136 |
"answer_button": "发送回答",
|
dream_customs/ui/styles.py
CHANGED
|
@@ -1209,6 +1209,86 @@ a.built-with[href*="gradio.app"] {
|
|
| 1209 |
}
|
| 1210 |
"""
|
| 1211 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1212 |
CSS += """
|
| 1213 |
/* Reference-space polish: soft PawMap spacing, case-file step clarity, paper-note tactility. */
|
| 1214 |
:root {
|
|
|
|
| 1209 |
}
|
| 1210 |
"""
|
| 1211 |
|
| 1212 |
+
CSS += """
|
| 1213 |
+
.dc-question-original p {
|
| 1214 |
+
font-size: clamp(1.16rem, 2.4vw, 1.45rem);
|
| 1215 |
+
font-weight: 720;
|
| 1216 |
+
line-height: 1.45;
|
| 1217 |
+
margin: 4px 0 0 !important;
|
| 1218 |
+
}
|
| 1219 |
+
|
| 1220 |
+
.dc-question-context {
|
| 1221 |
+
background: rgba(255, 253, 248, 0.72);
|
| 1222 |
+
border: 1px solid rgba(215, 168, 66, 0.22);
|
| 1223 |
+
border-radius: 10px;
|
| 1224 |
+
margin-top: 12px;
|
| 1225 |
+
padding: 10px 12px;
|
| 1226 |
+
}
|
| 1227 |
+
|
| 1228 |
+
.dc-question-context summary {
|
| 1229 |
+
color: var(--dqa-sage-deep);
|
| 1230 |
+
cursor: pointer;
|
| 1231 |
+
font-size: 0.84rem;
|
| 1232 |
+
font-weight: 760;
|
| 1233 |
+
}
|
| 1234 |
+
|
| 1235 |
+
.dc-question-context p {
|
| 1236 |
+
color: var(--dqa-muted);
|
| 1237 |
+
font-size: 0.92rem;
|
| 1238 |
+
margin-top: 8px;
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
.dc-stage button.dc-is-loading {
|
| 1242 |
+
cursor: progress !important;
|
| 1243 |
+
opacity: 0.82;
|
| 1244 |
+
pointer-events: none;
|
| 1245 |
+
}
|
| 1246 |
+
|
| 1247 |
+
.dc-stage button.dc-is-loading::after {
|
| 1248 |
+
animation: dqa-loading-dot 900ms ease-in-out infinite;
|
| 1249 |
+
content: "";
|
| 1250 |
+
display: inline-block;
|
| 1251 |
+
margin-left: 8px;
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
@keyframes dqa-loading-dot {
|
| 1255 |
+
0%,
|
| 1256 |
+
100% {
|
| 1257 |
+
content: "";
|
| 1258 |
+
}
|
| 1259 |
+
33% {
|
| 1260 |
+
content: ".";
|
| 1261 |
+
}
|
| 1262 |
+
66% {
|
| 1263 |
+
content: "..";
|
| 1264 |
+
}
|
| 1265 |
+
}
|
| 1266 |
+
|
| 1267 |
+
@media (max-width: 640px) {
|
| 1268 |
+
.dc-hero {
|
| 1269 |
+
min-height: 248px !important;
|
| 1270 |
+
padding-bottom: 18px !important;
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
.dc-stepper {
|
| 1274 |
+
margin-top: 18px !important;
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
.dc-stepper span {
|
| 1278 |
+
flex-basis: 62px !important;
|
| 1279 |
+
font-size: 0.82rem !important;
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
.dc-stepper strong {
|
| 1283 |
+
height: 38px !important;
|
| 1284 |
+
width: 38px !important;
|
| 1285 |
+
}
|
| 1286 |
+
|
| 1287 |
+
.dc-side-stamp {
|
| 1288 |
+
display: none;
|
| 1289 |
+
}
|
| 1290 |
+
}
|
| 1291 |
+
"""
|
| 1292 |
CSS += """
|
| 1293 |
/* Reference-space polish: soft PawMap spacing, case-file step clarity, paper-note tactility. */
|
| 1294 |
:root {
|
tests/test_pipeline.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from datetime import date
|
| 2 |
|
| 3 |
import pytest
|
|
@@ -24,6 +25,21 @@ from dream_customs.prompts import pact_prompt, today_tip_prompt
|
|
| 24 |
from dream_customs.schema import PactCard, TodayTipCard, VisionWitness
|
| 25 |
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
def test_dated_permit_id_uses_runtime_date_and_preserves_serial():
|
| 28 |
assert dated_permit_id("DREAM2024-001", today=date(2026, 6, 6)) == "DREAM20260606-001"
|
| 29 |
assert dated_permit_id("DC-DEMO-014", today=date(2026, 6, 6)) == "DREAM20260606-014"
|
|
@@ -421,7 +437,7 @@ def test_chinese_email_tip_becomes_real_world_suggestions_and_weird_action():
|
|
| 421 |
|
| 422 |
assert "办公楼」当成允许慢慢开始的按钮" not in card.today_tip
|
| 423 |
assert "现实" in card.today_tip
|
| 424 |
-
assert "1." in card.today_tip and "2." in card.today_tip
|
| 425 |
assert "邮件" in card.today_tip
|
| 426 |
assert "第一句" in card.today_tip or "第一句话" in card.today_tip
|
| 427 |
assert "便利贴" in card.tiny_action or "纸" in card.tiny_action
|
|
@@ -498,6 +514,281 @@ def test_generate_today_tip_keeps_english_emotional_question_from_becoming_task_
|
|
| 498 |
assert "first line" not in combined
|
| 499 |
|
| 500 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
def test_generate_today_tip_adds_safety_note_for_repeated_insomnia_without_self_harm():
|
| 502 |
intake = build_intake(
|
| 503 |
dream_text="我昨晚反复梦见自己在一条漆黑的走廊里走,醒来后心跳很快,已经连续3晚都睡不好。",
|
|
@@ -652,7 +943,7 @@ def test_today_tip_prompt_requires_waking_life_suggestions_and_weird_action():
|
|
| 652 |
prompt = today_tip_prompt(state, language="zh")
|
| 653 |
|
| 654 |
assert "waking-life" in prompt
|
| 655 |
-
assert "
|
| 656 |
assert "weird little thing" in prompt
|
| 657 |
assert "real-world physics" in prompt
|
| 658 |
assert "古怪的小事" in prompt
|
|
|
|
| 1 |
+
import re
|
| 2 |
from datetime import date
|
| 3 |
|
| 4 |
import pytest
|
|
|
|
| 25 |
from dream_customs.schema import PactCard, TodayTipCard, VisionWitness
|
| 26 |
|
| 27 |
|
| 28 |
+
def _today_tip_public_text(card: TodayTipCard) -> str:
|
| 29 |
+
return "\n".join(
|
| 30 |
+
[
|
| 31 |
+
card.dream_summary,
|
| 32 |
+
card.main_question,
|
| 33 |
+
",".join(card.dream_anchors),
|
| 34 |
+
card.interpretation,
|
| 35 |
+
card.today_tip,
|
| 36 |
+
card.tiny_action,
|
| 37 |
+
card.caring_note,
|
| 38 |
+
card.safety_note,
|
| 39 |
+
]
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
def test_dated_permit_id_uses_runtime_date_and_preserves_serial():
|
| 44 |
assert dated_permit_id("DREAM2024-001", today=date(2026, 6, 6)) == "DREAM20260606-001"
|
| 45 |
assert dated_permit_id("DC-DEMO-014", today=date(2026, 6, 6)) == "DREAM20260606-014"
|
|
|
|
| 437 |
|
| 438 |
assert "办公楼」当成允许慢慢开始的按钮" not in card.today_tip
|
| 439 |
assert "现实" in card.today_tip
|
| 440 |
+
assert "1." not in card.today_tip and "2." not in card.today_tip
|
| 441 |
assert "邮件" in card.today_tip
|
| 442 |
assert "第一句" in card.today_tip or "第一句话" in card.today_tip
|
| 443 |
assert "便利贴" in card.tiny_action or "纸" in card.tiny_action
|
|
|
|
| 514 |
assert "first line" not in combined
|
| 515 |
|
| 516 |
|
| 517 |
+
def test_today_tip_rejects_hollow_productivity_advice_even_when_it_mentions_anchor():
|
| 518 |
+
class HollowProductivityClient:
|
| 519 |
+
def generate_today_tip(self, prompt):
|
| 520 |
+
return TodayTipCard(
|
| 521 |
+
dream_summary="你梦见旧图书馆里有一把打不开的钥匙。",
|
| 522 |
+
main_question="这个梦在提醒我什么?",
|
| 523 |
+
dream_anchors=["旧图书馆", "钥匙"],
|
| 524 |
+
followup_questions=["旧图书馆和钥匙里,哪个细节最抓住你?"],
|
| 525 |
+
user_answers=[],
|
| 526 |
+
interpretation="「旧图书馆」说明你需要优化效率,整理待办清单。",
|
| 527 |
+
today_tip="今天把「旧图书馆」当作提高效率的提醒,整理待办清单并保持专注。",
|
| 528 |
+
tiny_action="写三项待办事项,按优先级排序。",
|
| 529 |
+
caring_note="保持专注就会更好。",
|
| 530 |
+
safety_note="",
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
intake = build_intake(
|
| 534 |
+
dream_text="我梦见自己在旧图书馆里拿着一把旧钥匙,却找不到它能打开哪扇门。",
|
| 535 |
+
mood="困惑",
|
| 536 |
+
)
|
| 537 |
+
|
| 538 |
+
card = generate_today_tip(intake, "用户选择跳过这个追问。", HollowProductivityClient(), language="zh")
|
| 539 |
+
combined = "\n".join([card.interpretation, card.today_tip, card.tiny_action, card.caring_note])
|
| 540 |
+
|
| 541 |
+
assert "旧图书馆" in combined or "钥匙" in combined
|
| 542 |
+
assert "效率" not in combined
|
| 543 |
+
assert "待办" not in combined
|
| 544 |
+
assert "专注" not in combined
|
| 545 |
+
assert "优先级" not in combined
|
| 546 |
+
assert "保持积极" not in combined
|
| 547 |
+
|
| 548 |
+
|
| 549 |
+
def test_today_tip_absorbs_non_template_followup_answer_as_real_life_cue():
|
| 550 |
+
class AnchorOnlyClient:
|
| 551 |
+
def generate_today_tip(self, prompt):
|
| 552 |
+
return TodayTipCard(
|
| 553 |
+
dream_summary="You dreamed of a locked green room and a warm key.",
|
| 554 |
+
main_question="What is the locked room asking me to notice?",
|
| 555 |
+
dream_anchors=["locked green room", "warm key"],
|
| 556 |
+
followup_questions=["What does the locked room feel closest to in waking life?"],
|
| 557 |
+
user_answers=[],
|
| 558 |
+
interpretation="Maybe the locked green room is a private threshold.",
|
| 559 |
+
today_tip="For today, treat the locked green room as a small threshold and take one careful step.",
|
| 560 |
+
tiny_action="Draw the warm key and move it one inch.",
|
| 561 |
+
caring_note="One small threshold is enough.",
|
| 562 |
+
safety_note="",
|
| 563 |
+
)
|
| 564 |
+
|
| 565 |
+
intake = build_intake(
|
| 566 |
+
dream_text="I found a locked green room behind my kitchen. The key was warm but would not turn.",
|
| 567 |
+
mood="uneasy",
|
| 568 |
+
main_question="Why does it feel like I cannot ask for what I need?",
|
| 569 |
+
)
|
| 570 |
+
answer = "In real life it is probably the leave request I keep avoiding because I feel guilty."
|
| 571 |
+
|
| 572 |
+
card = generate_today_tip(intake, answer, AnchorOnlyClient(), language="en")
|
| 573 |
+
combined = "\n".join([card.main_question, card.interpretation, card.today_tip, card.tiny_action, card.caring_note]).lower()
|
| 574 |
+
|
| 575 |
+
assert "locked green room" in combined or "warm key" in combined
|
| 576 |
+
assert "leave request" in combined
|
| 577 |
+
assert "guilty" in combined or "guilt" in combined
|
| 578 |
+
assert "one careful step" not in card.today_tip.lower()
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
def test_today_tip_filters_absent_office_email_and_repairs_question_summary_and_numbered_tip():
|
| 582 |
+
class PollutedOfficeEmailClient:
|
| 583 |
+
def generate_today_tip(self, prompt):
|
| 584 |
+
return TodayTipCard(
|
| 585 |
+
dream_summary="I dreamed I was rushing to catch an elevator, but my phone was dead a",
|
| 586 |
+
main_question="My question: does this mean I have been too anxious lately",
|
| 587 |
+
dream_anchors=["office building", "elevator", "email"],
|
| 588 |
+
followup_questions=["Which detail felt closest?"],
|
| 589 |
+
user_answers=[],
|
| 590 |
+
interpretation=(
|
| 591 |
+
"Maybe the office building is asking you to open one thread and draft one first sentence."
|
| 592 |
+
),
|
| 593 |
+
today_tip=(
|
| 594 |
+
"1. Translate office building into a real-world communication question: what does the "
|
| 595 |
+
"overdue email need? 2. Open the draft and write the first sentence of the email. "
|
| 596 |
+
"3. Save it."
|
| 597 |
+
),
|
| 598 |
+
tiny_action="Draw the office building and write only the first sentence of the email.",
|
| 599 |
+
caring_note="The office building can wait.",
|
| 600 |
+
safety_note="",
|
| 601 |
+
)
|
| 602 |
+
|
| 603 |
+
intake = build_intake(
|
| 604 |
+
dream_text=(
|
| 605 |
+
"I dreamed I was rushing to catch an elevator, but my phone was dead and I was late for something "
|
| 606 |
+
"important. I kept pressing the elevator button and worrying I had already missed my chance. "
|
| 607 |
+
"My question: does this mean I have been too anxious lately?"
|
| 608 |
+
),
|
| 609 |
+
mood="Anxious",
|
| 610 |
+
)
|
| 611 |
+
answer = "The phone with no battery feels closest. I keep avoiding a work message, so I feel behind before I even start."
|
| 612 |
+
|
| 613 |
+
card = generate_today_tip(intake, answer, PollutedOfficeEmailClient(), language="en")
|
| 614 |
+
combined = _today_tip_public_text(card).lower()
|
| 615 |
+
|
| 616 |
+
assert "office building" not in combined
|
| 617 |
+
assert "overdue email" not in combined
|
| 618 |
+
assert "first sentence of the email" not in combined
|
| 619 |
+
assert "elevator" in combined or "phone" in combined
|
| 620 |
+
assert "work message" in combined or "message" in combined
|
| 621 |
+
assert "my question:" not in card.main_question.lower()
|
| 622 |
+
assert card.main_question == "Does this mean I have been too anxious lately?"
|
| 623 |
+
assert not card.dream_summary.rstrip(".").endswith("phone was dead a")
|
| 624 |
+
assert not re.search(r"\b[123]\.", card.today_tip)
|
| 625 |
+
|
| 626 |
+
|
| 627 |
+
def test_today_tip_keeps_friend_misunderstanding_emotional_instead_of_email_draft_template():
|
| 628 |
+
class PollutedFriendClient:
|
| 629 |
+
def generate_today_tip(self, prompt):
|
| 630 |
+
return TodayTipCard(
|
| 631 |
+
dream_summary="I dreamed my close friend misunderstood me in front of everyone. I ke",
|
| 632 |
+
main_question="My question: why did I wake up feeling so wronged and hurt",
|
| 633 |
+
dream_anchors=["room"],
|
| 634 |
+
followup_questions=["What felt closest?"],
|
| 635 |
+
user_answers=[],
|
| 636 |
+
interpretation=(
|
| 637 |
+
"Maybe the room is not asking you to answer every message at once; it points to opening "
|
| 638 |
+
"one thread and drafting one first sentence."
|
| 639 |
+
),
|
| 640 |
+
today_tip=(
|
| 641 |
+
"1. Translate room into a real-world communication question: what does the overdue email "
|
| 642 |
+
"need? 2. Open the draft and write one sentence."
|
| 643 |
+
),
|
| 644 |
+
tiny_action="Open the message thread and draft the first sentence of the email.",
|
| 645 |
+
caring_note="One thread at a time.",
|
| 646 |
+
safety_note="",
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
intake = build_intake(
|
| 650 |
+
dream_text=(
|
| 651 |
+
"I dreamed my close friend misunderstood me in front of everyone. I kept explaining, but nobody "
|
| 652 |
+
"listened, and the room got louder every time I tried. My question: why did I wake up feeling so "
|
| 653 |
+
"wronged and hurt?"
|
| 654 |
+
),
|
| 655 |
+
mood="Hurt and confused",
|
| 656 |
+
)
|
| 657 |
+
answer = "It felt like I was invisible, not angry. Yesterday I worried that a friend read my message in the worst possible way."
|
| 658 |
+
|
| 659 |
+
card = generate_today_tip(intake, answer, PollutedFriendClient(), language="en")
|
| 660 |
+
combined = _today_tip_public_text(card).lower()
|
| 661 |
+
|
| 662 |
+
assert "overdue email" not in combined
|
| 663 |
+
assert "first sentence of the email" not in combined
|
| 664 |
+
assert "draft the first sentence" not in combined
|
| 665 |
+
assert "wronged" in combined or "hurt" in combined or "invisible" in combined or "not listened" in combined
|
| 666 |
+
assert "friend" in combined or "message" in combined
|
| 667 |
+
assert "my question:" not in card.main_question.lower()
|
| 668 |
+
assert card.main_question == "Why did I wake up feeling so wronged and hurt?"
|
| 669 |
+
assert not re.search(r"\b[123]\.", card.today_tip)
|
| 670 |
+
|
| 671 |
+
|
| 672 |
+
def test_today_tip_skip_path_uses_original_anchors_without_default_email_or_broken_caring_note():
|
| 673 |
+
class PollutedSkipClient:
|
| 674 |
+
def generate_today_tip(self, prompt):
|
| 675 |
+
return TodayTipCard(
|
| 676 |
+
dream_summary="I dreamed I was late, my phone battery died, and the elevator never a",
|
| 677 |
+
main_question="What might the elevator mean?",
|
| 678 |
+
dream_anchors=["office building", "elevator", "email"],
|
| 679 |
+
followup_questions=["Which detail felt closest?"],
|
| 680 |
+
user_answers=[],
|
| 681 |
+
interpretation="Maybe the office building, elevator, and email point to a draft you need to open.",
|
| 682 |
+
today_tip=(
|
| 683 |
+
"1. Use elevator, my phone, and phone. 2. Choose one waking-life doorway action today: "
|
| 684 |
+
"open the draft."
|
| 685 |
+
),
|
| 686 |
+
tiny_action="Draw an elevator button and write only the first sentence of the email.",
|
| 687 |
+
caring_note="dream fragment , dream fragment floor.",
|
| 688 |
+
safety_note="",
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
+
intake = build_intake(
|
| 692 |
+
dream_text=(
|
| 693 |
+
"I dreamed I was late, my phone battery died, and the elevator never arrived. I want a quick tip, "
|
| 694 |
+
"but I want to skip the follow-up."
|
| 695 |
+
),
|
| 696 |
+
mood="Anxious",
|
| 697 |
+
)
|
| 698 |
+
|
| 699 |
+
card = generate_today_tip(intake, "", PollutedSkipClient(), language="en")
|
| 700 |
+
combined = _today_tip_public_text(card).lower()
|
| 701 |
+
|
| 702 |
+
assert "office building" not in combined
|
| 703 |
+
assert "email" not in combined
|
| 704 |
+
assert "open the draft" not in combined
|
| 705 |
+
assert "first sentence" not in combined
|
| 706 |
+
assert "dream fragment" not in combined
|
| 707 |
+
assert "elevator" in combined
|
| 708 |
+
assert "phone" in combined or "late" in combined
|
| 709 |
+
assert not re.search(r"\b[123]\.", card.today_tip)
|
| 710 |
+
|
| 711 |
+
|
| 712 |
+
def test_today_tip_skip_path_repairs_generic_calming_and_tiny_action_inside_tip():
|
| 713 |
+
class GenericCalmingClient:
|
| 714 |
+
def generate_today_tip(self, prompt):
|
| 715 |
+
return TodayTipCard(
|
| 716 |
+
dream_summary="I dreamed I was late, my phone battery died, and the elevator never arrived.",
|
| 717 |
+
main_question="What might the elevator be asking me to notice today?",
|
| 718 |
+
dream_anchors=["elevator", "phone"],
|
| 719 |
+
followup_questions=[],
|
| 720 |
+
user_answers=[],
|
| 721 |
+
interpretation="Maybe the elevator and phone point to the pressure of waiting.",
|
| 722 |
+
today_tip=(
|
| 723 |
+
"Connect your phone's battery to your anxious morning. If the elevator voice persists, "
|
| 724 |
+
"listen to a short, calming song. For the tiny action, hold a cold glass of water for one minute "
|
| 725 |
+
"so you feel more grounded."
|
| 726 |
+
),
|
| 727 |
+
tiny_action="Name one elevator button you can press today.",
|
| 728 |
+
caring_note="One small detail is enough.",
|
| 729 |
+
safety_note="",
|
| 730 |
+
)
|
| 731 |
+
|
| 732 |
+
intake = build_intake(
|
| 733 |
+
dream_text="I dreamed I was late, my phone battery died, and the elevator never arrived.",
|
| 734 |
+
mood="Anxious",
|
| 735 |
+
)
|
| 736 |
+
|
| 737 |
+
card = generate_today_tip(intake, "", GenericCalmingClient(), language="en")
|
| 738 |
+
combined = _today_tip_public_text(card).lower()
|
| 739 |
+
|
| 740 |
+
assert "elevator" in card.today_tip.lower() or "phone" in card.today_tip.lower()
|
| 741 |
+
assert "calming song" not in combined
|
| 742 |
+
assert "cold glass of water" not in combined
|
| 743 |
+
assert "for the tiny action" not in card.today_tip.lower()
|
| 744 |
+
assert not re.search(r"\b[123]\.", card.today_tip)
|
| 745 |
+
|
| 746 |
+
|
| 747 |
+
def test_today_tip_distress_repairs_polluted_surrounding_fields_while_keeping_safety_note():
|
| 748 |
+
class PollutedDistressClient:
|
| 749 |
+
def generate_today_tip(self, prompt):
|
| 750 |
+
return TodayTipCard(
|
| 751 |
+
dream_summary="I keep having the same frightening dream and I have barely slept for",
|
| 752 |
+
main_question="What is wrong with me?",
|
| 753 |
+
dream_anchors=["office building", "elevator", "email"],
|
| 754 |
+
followup_questions=["What help would feel possible?"],
|
| 755 |
+
user_answers=[],
|
| 756 |
+
interpretation=(
|
| 757 |
+
"Second, the office building, elevator, and email may be the dream's concrete shape."
|
| 758 |
+
),
|
| 759 |
+
today_tip=(
|
| 760 |
+
"1. Treat office building, elevator, and email as an unfinished contact. "
|
| 761 |
+
"2. Write one private unsent reply."
|
| 762 |
+
),
|
| 763 |
+
tiny_action="Write one word from office building, elevator, and email.",
|
| 764 |
+
caring_note="Start with the email contact.",
|
| 765 |
+
safety_note="Seek support.",
|
| 766 |
+
)
|
| 767 |
+
|
| 768 |
+
intake = build_intake(
|
| 769 |
+
dream_text=(
|
| 770 |
+
"I keep having the same frightening dream and I have barely slept for many nights. I feel like I "
|
| 771 |
+
"cannot function during the day and I am scared something is wrong with me."
|
| 772 |
+
),
|
| 773 |
+
mood="Scared and exhausted",
|
| 774 |
+
)
|
| 775 |
+
answer = "I do not feel unsafe right now, but I cannot keep going like this without help."
|
| 776 |
+
|
| 777 |
+
card = generate_today_tip(intake, answer, PollutedDistressClient(), language="en")
|
| 778 |
+
combined = _today_tip_public_text(card).lower()
|
| 779 |
+
|
| 780 |
+
assert card.safety_note
|
| 781 |
+
assert "office building" not in combined
|
| 782 |
+
assert "elevator" not in combined
|
| 783 |
+
assert "email" not in combined
|
| 784 |
+
assert "unsent reply" not in combined
|
| 785 |
+
assert "dream clues" not in combined
|
| 786 |
+
assert any(anchor in card.dream_anchors for anchor in ["frightening dream", "lost sleep"])
|
| 787 |
+
assert "frightening" in combined or "sleep" in combined or "scared" in combined or "help" in combined
|
| 788 |
+
assert "trusted" in card.safety_note.lower() or "professional" in card.safety_note.lower()
|
| 789 |
+
assert not re.search(r"\b[123]\.", card.today_tip)
|
| 790 |
+
|
| 791 |
+
|
| 792 |
def test_generate_today_tip_adds_safety_note_for_repeated_insomnia_without_self_harm():
|
| 793 |
intake = build_intake(
|
| 794 |
dream_text="我昨晚反复梦见自己在一条漆黑的走廊里走,醒来后心跳很快,已经连续3晚都睡不好。",
|
|
|
|
| 943 |
prompt = today_tip_prompt(state, language="zh")
|
| 944 |
|
| 945 |
assert "waking-life" in prompt
|
| 946 |
+
assert "single grounded tip" in prompt
|
| 947 |
assert "weird little thing" in prompt
|
| 948 |
assert "real-world physics" in prompt
|
| 949 |
assert "古怪的小事" in prompt
|
tests/test_ui_actions.py
CHANGED
|
@@ -10,7 +10,9 @@ from dream_customs.ui.actions import (
|
|
| 10 |
skip_to_card_action,
|
| 11 |
submit_dream_action,
|
| 12 |
)
|
|
|
|
| 13 |
import dream_customs.ui.app as ui_app
|
|
|
|
| 14 |
from dream_customs.ui.app import _reset
|
| 15 |
from dream_customs.ui.copy import DEFAULT_MOOD, PROCESSING_NOTE
|
| 16 |
from dream_customs.ui.styles import CSS
|
|
@@ -29,10 +31,12 @@ def test_runtime_settings_are_collapsed_for_public_flow():
|
|
| 29 |
flow_column_source = source.split('with gr.Column(elem_classes=["dc-flow-column"]):', 1)[1].split(
|
| 30 |
'with gr.Column(elem_classes=["dc-side-panel"]):', 1
|
| 31 |
)[0]
|
|
|
|
| 32 |
|
| 33 |
assert 'gr.Accordion("Advanced", open=False' in source
|
| 34 |
assert 'with gr.Accordion(initial_copy["debug_title"], open=False' in source
|
| 35 |
-
assert 'with gr.Accordion(initial_copy["debug_title"], open=False' in flow_column_source
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
def test_voice_input_records_audio_to_modal_asr_without_browser_speech_recognition():
|
|
@@ -109,11 +113,12 @@ def test_hero_stepper_tracks_app_status():
|
|
| 109 |
|
| 110 |
assert '<span class="dc-step is-complete"><strong>1</strong>' in ask_html
|
| 111 |
assert '<i class="dc-stepper-line is-complete" aria-hidden="true"></i>' in ask_html
|
| 112 |
-
assert '<span class="dc-step is-
|
| 113 |
-
assert '<span class="dc-step is-
|
| 114 |
-
assert '<span class="dc-step is-
|
| 115 |
-
assert
|
| 116 |
-
assert "
|
|
|
|
| 117 |
|
| 118 |
|
| 119 |
def test_hero_subtitle_explains_app_instead_of_legacy_name():
|
|
@@ -216,7 +221,10 @@ def test_processing_note_is_story_copy_not_backend_jargon():
|
|
| 216 |
def test_question_stage_shows_anchor_chips_before_the_question():
|
| 217 |
html = ui_app._question_markdown(
|
| 218 |
{
|
| 219 |
-
"question":
|
|
|
|
|
|
|
|
|
|
| 220 |
"dream_anchors": ["elevator", "floor 14", "melting button"],
|
| 221 |
},
|
| 222 |
"en",
|
|
@@ -226,8 +234,11 @@ def test_question_stage_shows_anchor_chips_before_the_question():
|
|
| 226 |
assert "dc-question-anchor-label" in html
|
| 227 |
assert "Sticky details already on the desk" in html
|
| 228 |
assert html.index("elevator") < html.index("Morning Question Desk")
|
|
|
|
| 229 |
assert "floor 14" in html
|
| 230 |
assert "melting button" in html
|
|
|
|
|
|
|
| 231 |
|
| 232 |
|
| 233 |
def test_long_running_buttons_show_processing_feedback():
|
|
@@ -241,6 +252,10 @@ def test_long_running_buttons_show_processing_feedback():
|
|
| 241 |
assert "bindProcessingButtons" in ui_app.VOICE_JS
|
| 242 |
assert "setProcessingCopy" in ui_app.VOICE_JS
|
| 243 |
assert "processingBound" in ui_app.VOICE_JS
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
assert "Asking from another angle" in ui_app.VOICE_JS
|
| 245 |
assert "one more grounded question" in ui_app.VOICE_JS
|
| 246 |
assert "js=SUBMIT_PROCESSING_JS" not in source
|
|
@@ -250,6 +265,7 @@ def test_long_running_buttons_show_processing_feedback():
|
|
| 250 |
assert "Folding the follow-up answer" in app_source
|
| 251 |
assert ".dc-notice.is-processing" in CSS
|
| 252 |
assert ".dc-processing-note.is-active" in CSS
|
|
|
|
| 253 |
assert "endpoint" not in ui_app.VOICE_JS.lower()
|
| 254 |
assert "debug" not in ui_app.VOICE_JS.lower()
|
| 255 |
|
|
@@ -331,6 +347,28 @@ def test_mobile_mvp_submit_then_skip_generates_today_tip():
|
|
| 331 |
assert "Today Tip" in view["card_html"]
|
| 332 |
|
| 333 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
def test_mobile_mvp_empty_submit_stays_on_record_with_clear_error():
|
| 335 |
state, view_json = submit_dream_action(
|
| 336 |
dream_text="",
|
|
|
|
| 10 |
skip_to_card_action,
|
| 11 |
submit_dream_action,
|
| 12 |
)
|
| 13 |
+
from dream_customs.schema import CustomsSession, TodayTipCard
|
| 14 |
import dream_customs.ui.app as ui_app
|
| 15 |
+
import dream_customs.ui.actions as ui_actions
|
| 16 |
from dream_customs.ui.app import _reset
|
| 17 |
from dream_customs.ui.copy import DEFAULT_MOOD, PROCESSING_NOTE
|
| 18 |
from dream_customs.ui.styles import CSS
|
|
|
|
| 31 |
flow_column_source = source.split('with gr.Column(elem_classes=["dc-flow-column"]):', 1)[1].split(
|
| 32 |
'with gr.Column(elem_classes=["dc-side-panel"]):', 1
|
| 33 |
)[0]
|
| 34 |
+
side_panel_source = source.split('with gr.Column(elem_classes=["dc-side-panel"]):', 1)[1]
|
| 35 |
|
| 36 |
assert 'gr.Accordion("Advanced", open=False' in source
|
| 37 |
assert 'with gr.Accordion(initial_copy["debug_title"], open=False' in source
|
| 38 |
+
assert 'with gr.Accordion(initial_copy["debug_title"], open=False' not in flow_column_source
|
| 39 |
+
assert 'with gr.Accordion(initial_copy["debug_title"], open=False' in side_panel_source
|
| 40 |
|
| 41 |
|
| 42 |
def test_voice_input_records_audio_to_modal_asr_without_browser_speech_recognition():
|
|
|
|
| 113 |
|
| 114 |
assert '<span class="dc-step is-complete"><strong>1</strong>' in ask_html
|
| 115 |
assert '<i class="dc-stepper-line is-complete" aria-hidden="true"></i>' in ask_html
|
| 116 |
+
assert '<span class="dc-step is-complete"><strong>2</strong>' in ask_html
|
| 117 |
+
assert '<span class="dc-step is-active" aria-current="step"><strong>3</strong>' in ask_html
|
| 118 |
+
assert '<span class="dc-step is-complete"><strong>3</strong>' in tip_html
|
| 119 |
+
assert '<span class="dc-step is-active" aria-current="step"><strong>4</strong>' in tip_html
|
| 120 |
+
assert tip_html.count("dc-stepper-line is-complete") == 3
|
| 121 |
+
assert "Answer" in tip_html
|
| 122 |
|
| 123 |
|
| 124 |
def test_hero_subtitle_explains_app_instead_of_legacy_name():
|
|
|
|
| 221 |
def test_question_stage_shows_anchor_chips_before_the_question():
|
| 222 |
html = ui_app._question_markdown(
|
| 223 |
{
|
| 224 |
+
"question": (
|
| 225 |
+
"What I hear first is the concrete image. One question that will shape the final tip: "
|
| 226 |
+
"When floor 14 shows up beside the overdue email, what would make opening it feel smaller?"
|
| 227 |
+
),
|
| 228 |
"dream_anchors": ["elevator", "floor 14", "melting button"],
|
| 229 |
},
|
| 230 |
"en",
|
|
|
|
| 234 |
assert "dc-question-anchor-label" in html
|
| 235 |
assert "Sticky details already on the desk" in html
|
| 236 |
assert html.index("elevator") < html.index("Morning Question Desk")
|
| 237 |
+
assert "Why this question" in html
|
| 238 |
assert "floor 14" in html
|
| 239 |
assert "melting button" in html
|
| 240 |
+
assert "What I hear first" in html
|
| 241 |
+
assert html.index("When floor 14") < html.index("Why this question")
|
| 242 |
|
| 243 |
|
| 244 |
def test_long_running_buttons_show_processing_feedback():
|
|
|
|
| 252 |
assert "bindProcessingButtons" in ui_app.VOICE_JS
|
| 253 |
assert "setProcessingCopy" in ui_app.VOICE_JS
|
| 254 |
assert "processingBound" in ui_app.VOICE_JS
|
| 255 |
+
assert "setBusyButton" in ui_app.VOICE_JS
|
| 256 |
+
assert "resetSettledButton" in ui_app.VOICE_JS
|
| 257 |
+
assert "busyStarted" in ui_app.VOICE_JS
|
| 258 |
+
assert "dc-is-loading" in ui_app.VOICE_JS
|
| 259 |
assert "Asking from another angle" in ui_app.VOICE_JS
|
| 260 |
assert "one more grounded question" in ui_app.VOICE_JS
|
| 261 |
assert "js=SUBMIT_PROCESSING_JS" not in source
|
|
|
|
| 265 |
assert "Folding the follow-up answer" in app_source
|
| 266 |
assert ".dc-notice.is-processing" in CSS
|
| 267 |
assert ".dc-processing-note.is-active" in CSS
|
| 268 |
+
assert ".dc-stage button.dc-is-loading" in CSS
|
| 269 |
assert "endpoint" not in ui_app.VOICE_JS.lower()
|
| 270 |
assert "debug" not in ui_app.VOICE_JS.lower()
|
| 271 |
|
|
|
|
| 347 |
assert "Today Tip" in view["card_html"]
|
| 348 |
|
| 349 |
|
| 350 |
+
def test_visible_tip_payload_repairs_broken_caring_note_placeholder():
|
| 351 |
+
session = CustomsSession(language="en", phase="tip")
|
| 352 |
+
session.sealed_tip = TodayTipCard(
|
| 353 |
+
dream_summary="I dreamed I was late, my phone battery died, and the elevator never arrived.",
|
| 354 |
+
main_question="What might the elevator be asking me to notice today?",
|
| 355 |
+
dream_anchors=["elevator", "phone"],
|
| 356 |
+
followup_questions=[],
|
| 357 |
+
user_answers=["The user chose to skip this question."],
|
| 358 |
+
interpretation="Maybe the elevator and phone show a small stuck point.",
|
| 359 |
+
today_tip="Use the elevator and phone to name one doorway action for today.",
|
| 360 |
+
tiny_action="Draw one elevator button.",
|
| 361 |
+
caring_note="dream fragment , dream fragment floor。",
|
| 362 |
+
safety_note="",
|
| 363 |
+
)
|
| 364 |
+
|
| 365 |
+
view = ui_actions._view_payload(session, "demo", "demo", language="en")
|
| 366 |
+
combined = "\n".join([view["card_text"], view["card_html"], view["caring_note"]]).lower()
|
| 367 |
+
|
| 368 |
+
assert "dream fragment" not in combined
|
| 369 |
+
assert "elevator" in view["caring_note"].lower()
|
| 370 |
+
|
| 371 |
+
|
| 372 |
def test_mobile_mvp_empty_submit_stays_on_record_with_clear_error():
|
| 373 |
state, view_json = submit_dream_action(
|
| 374 |
dream_text="",
|