Polish Dream QA sample dropdown and friend grounding

#44
by ADJCJH - opened
dream_customs/pipeline.py CHANGED
@@ -529,6 +529,22 @@ def _extract_dream_anchors(intake: DreamIntake) -> List[str]:
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):
@@ -839,6 +855,29 @@ def _contains_any(text: str, terms: List[str]) -> bool:
839
 
840
  def _dream_theme(intake: DreamIntake, answers: str = "") -> str:
841
  text = _story_text(intake, answers)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
842
  if _contains_any(text, ["小孩", "child", "找不到家", "lost", "home", "回家", "地铁", "subway"]):
843
  return "lost_home"
844
  if _contains_any(text, ["海", "海浪", "月牙", "moon", "sea", "wave", "dark sea", "dark water", "漆黑的海"]):
@@ -905,6 +944,7 @@ def _story_anchor_phrase(intake: DreamIntake, anchors: List[str], language: str
905
  "lost_home": "地铁站里迷路的小孩和回家的方向",
906
  "dark_water": "夜晚的海、海浪和月牙下的小人",
907
  "library_signal": "旧图书馆、红色楼梯和那张便签",
 
908
  "message_loss": "那条消息、前任和突然消失",
909
  "chased": "森林、白色猫和空白路牌",
910
  "school_pressure": "教室、作业和来不及交上的感觉",
@@ -915,6 +955,7 @@ def _story_anchor_phrase(intake: DreamIntake, anchors: List[str], language: str
915
  "lost_home": "the lost child, the subway, and the way home",
916
  "dark_water": "the dark water, the waves, and the small figure under the moon",
917
  "library_signal": "the old library, the red staircase, and the note",
 
918
  "message_loss": "the message, the former partner, and the disappearance",
919
  "chased": "the chase, the call for help, and the missing route",
920
  "school_pressure": "the classroom, the assignment, and the late feeling",
@@ -1299,6 +1340,14 @@ def _emotion_led_today_tip(intake: DreamIntake, answers: str, anchors: List[str]
1299
  ],
1300
  language,
1301
  )
 
 
 
 
 
 
 
 
1302
  if theme == "message_loss":
1303
  return _numbered_suggestions(
1304
  [
@@ -1347,6 +1396,14 @@ def _emotion_led_today_tip(intake: DreamIntake, answers: str, anchors: List[str]
1347
  ],
1348
  language,
1349
  )
 
 
 
 
 
 
 
 
1350
  if theme == "message_loss":
1351
  return _numbered_suggestions(
1352
  [
@@ -1481,6 +1538,10 @@ def _weird_little_action(intake: DreamIntake, answers: str, anchors: List[str],
1481
  f"做一张迷你借书卡,书名写「{anchor}」,到期日写“今晚不追讨”;把它夹进一本书里,像把梦暂存进图书馆。",
1482
  f"把一张便签折成小楼梯,写上「{anchor}」后让一支笔从第一阶滑到第二阶;今天只允许它上这一阶。",
1483
  ],
 
 
 
 
1484
  "message_loss": [
1485
  f"把手机旁边放一张小纸片当“消息降落伞”,写上「{anchor}」的一个词;让纸片从手机边滑下来,今天不真的发送。",
1486
  f"画一个迷你信号塔,塔顶写「{anchor}」;把手机倒扣三十秒,像给没发出的消息临时下班。",
@@ -1517,6 +1578,10 @@ def _weird_little_action(intake: DreamIntake, answers: str, anchors: List[str],
1517
  f"Make a tiny library card titled {anchor}, set the due date to Not tonight, and tuck it into a book.",
1518
  f"Fold a note into a small staircase, write {anchor} on it, and slide a pen from the first step to the second; today it only gets one step.",
1519
  ],
 
 
 
 
1520
  "message_loss": [
1521
  f"Put a paper scrap beside your phone as a message parachute, write one word from {anchor}, and let it slide down from the phone without sending anything.",
1522
  f"Draw a tiny signal tower with {anchor} at the top, then place your phone face down for thirty seconds so the unsent message can clock out.",
@@ -1567,6 +1632,14 @@ def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
1567
  ],
1568
  language,
1569
  )
 
 
 
 
 
 
 
 
1570
  if theme == "message_loss":
1571
  return _numbered_suggestions(
1572
  [
@@ -1616,6 +1689,14 @@ def _grounded_today_tip(intake: DreamIntake, language: str = "en") -> str:
1616
  ],
1617
  language,
1618
  )
 
 
 
 
 
 
 
 
1619
  if theme == "message_loss":
1620
  return _numbered_suggestions(
1621
  [
@@ -1926,7 +2007,11 @@ def _text_uses_anchor(text: str, anchors: List[str]) -> bool:
1926
  continue
1927
  if item in clean:
1928
  return True
1929
- tokens = [token for token in re.split(r"[\s,,。::;;、]+", item) if len(token) >= 3]
 
 
 
 
1930
  if any(token in clean for token in tokens):
1931
  return True
1932
  return False
@@ -2244,6 +2329,11 @@ def _decomposition_question(
2244
  "那张便签或“回家”的感觉,更像想联系某个人,还是想回到一种安定状态?",
2245
  "最后只确认今天的需要:你想整理一个现实线索,还是先给自己一点回家的感觉?",
2246
  ],
 
 
 
 
 
2247
  "message_loss": [
2248
  "那条消息最刺痛你的地方,是它出现了、消失了,还是它来自某个具体的人?",
2249
  "这个梦更像在问“我还在等什么”,还是在问“我该怎么照顾还没放下的部分”?",
@@ -2272,6 +2362,11 @@ def _decomposition_question(
2272
  "Does the stuckness feel more like being late to start, or like trying something and getting no response?",
2273
  "Last check before the tip: would one small action help most, or a sentence that lowers the pressure first?",
2274
  ],
 
 
 
 
 
2275
  }
2276
  fallback = [
2277
  f"In {story_anchor}, what feeling stayed strongest after waking?",
 
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"\b(close\s+friend|friend|best\s+friend)\b", text) and re.search(
533
+ r"\b(misunderstood|misunderstand|wronged|hurt|invisible|unseen|unheard|heard|listened|listening)\b",
534
+ text,
535
+ ):
536
+ if "close friend" in text:
537
+ candidates.append("close friend")
538
+ else:
539
+ candidates.append("friend")
540
+ if re.search(r"\bmisunderstood|misunderstand\b", text):
541
+ candidates.append("being misunderstood")
542
+ if re.search(r"\bnobody\s+(?:heard|listened)\b|\bno one\s+(?:heard|listened)\b", text):
543
+ candidates.append("nobody heard me")
544
+ if "invisible" in text:
545
+ candidates.append("feeling invisible")
546
+ if "hurt" in text or "wronged" in text:
547
+ candidates.append("feeling hurt")
548
  if re.search(r"\bfloor\s*14\b|\b14\b", text):
549
  candidates.append("floor 14")
550
  if "button" in text and re.search(r"\b(melt|melted|melting|wax)\b", text):
 
855
 
856
  def _dream_theme(intake: DreamIntake, answers: str = "") -> str:
857
  text = _story_text(intake, answers)
858
+ if _contains_any(text, ["friend", "朋友"]) and _contains_any(
859
+ text,
860
+ [
861
+ "misunderstood",
862
+ "misunderstand",
863
+ "nobody heard",
864
+ "nobody listened",
865
+ "no one heard",
866
+ "no one listened",
867
+ "invisible",
868
+ "unseen",
869
+ "unheard",
870
+ "wronged",
871
+ "hurt",
872
+ "误解",
873
+ "没人听",
874
+ "没人听见",
875
+ "看不见",
876
+ "委屈",
877
+ "受伤",
878
+ ],
879
+ ):
880
+ return "misunderstood_friend"
881
  if _contains_any(text, ["小孩", "child", "找不到家", "lost", "home", "回家", "地铁", "subway"]):
882
  return "lost_home"
883
  if _contains_any(text, ["海", "海浪", "月牙", "moon", "sea", "wave", "dark sea", "dark water", "漆黑的海"]):
 
944
  "lost_home": "地铁站里迷路的小孩和回家的方向",
945
  "dark_water": "夜晚的海、海浪和月牙下的小人",
946
  "library_signal": "旧图书馆、红色楼梯和那张便签",
947
+ "misunderstood_friend": "朋友的误解、没人听见和受伤感",
948
  "message_loss": "那条消息、前任和突然消失",
949
  "chased": "森林、白色猫和空白路牌",
950
  "school_pressure": "教室、作业和来不及交上的感觉",
 
955
  "lost_home": "the lost child, the subway, and the way home",
956
  "dark_water": "the dark water, the waves, and the small figure under the moon",
957
  "library_signal": "the old library, the red staircase, and the note",
958
+ "misunderstood_friend": "the close friend, being misunderstood, and feeling invisible",
959
  "message_loss": "the message, the former partner, and the disappearance",
960
  "chased": "the chase, the call for help, and the missing route",
961
  "school_pressure": "the classroom, the assignment, and the late feeling",
 
1340
  ],
1341
  language,
1342
  )
1343
+ if theme == "misunderstood_friend":
1344
+ return _numbered_suggestions(
1345
+ [
1346
+ f"先把「{anchor}」当成醒来后还需要被听见的受伤感,不急着证明谁对谁错。",
1347
+ "今天只做一个温和澄清动作:写下一句“我真正想被听见的是……”,先不急着发送。",
1348
+ ],
1349
+ language,
1350
+ )
1351
  if theme == "message_loss":
1352
  return _numbered_suggestions(
1353
  [
 
1396
  ],
1397
  language,
1398
  )
1399
+ if theme == "misunderstood_friend":
1400
+ return _numbered_suggestions(
1401
+ [
1402
+ f"Treat {anchor} as the hurt of not being heard, not as proof that you must fix the whole friendship today.",
1403
+ "Write one gentle clarifying sentence that starts with: What I wanted you to understand was...",
1404
+ ],
1405
+ language,
1406
+ )
1407
  if theme == "message_loss":
1408
  return _numbered_suggestions(
1409
  [
 
1538
  f"做一张迷你借书卡,书名写「{anchor}」,到期日写“今晚不追讨”;把它夹进一本书里,像把梦暂存进图书馆。",
1539
  f"把一张便签折成小楼梯,写上「{anchor}」后让一支笔从第一阶滑到第二阶;今天只允许它上这一阶。",
1540
  ],
1541
+ "misunderstood_friend": [
1542
+ f"在便签上写「{anchor}」旁边最想被听见的一句话,把便签对折放十分钟,先不决定要不要发出。",
1543
+ f"画两个小气泡,一个写「{anchor}」,一个写“我真正想说的是”;只填第二个气泡的一句话。",
1544
+ ],
1545
  "message_loss": [
1546
  f"把手机旁边放一张小纸片当“消息降落伞”,写上「{anchor}」的一个词;让纸片从手机边滑下来,今天不真的发送。",
1547
  f"画一个迷你信号塔,塔顶写「{anchor}」;把手机倒扣三十秒,像给没发出的消息临时下班。",
 
1578
  f"Make a tiny library card titled {anchor}, set the due date to Not tonight, and tuck it into a book.",
1579
  f"Fold a note into a small staircase, write {anchor} on it, and slide a pen from the first step to the second; today it only gets one step.",
1580
  ],
1581
+ "misunderstood_friend": [
1582
+ f"Write one sentence you wished had been heard beside {anchor}, fold the note once, and wait ten minutes before deciding whether it needs to be sent.",
1583
+ f"Draw two speech bubbles: one labeled {anchor}, one labeled What I meant was. Fill only the second bubble with one gentle sentence.",
1584
+ ],
1585
  "message_loss": [
1586
  f"Put a paper scrap beside your phone as a message parachute, write one word from {anchor}, and let it slide down from the phone without sending anything.",
1587
  f"Draw a tiny signal tower with {anchor} at the top, then place your phone face down for thirty seconds so the unsent message can clock out.",
 
1632
  ],
1633
  language,
1634
  )
1635
+ if theme == "misunderstood_friend":
1636
+ return _numbered_suggestions(
1637
+ [
1638
+ f"Use {anchor} to name the part that wanted to be heard.",
1639
+ "Choose one gentle clarification for today: write the sentence privately before deciding whether to say it.",
1640
+ ],
1641
+ language,
1642
+ )
1643
  if theme == "message_loss":
1644
  return _numbered_suggestions(
1645
  [
 
1689
  ],
1690
  language,
1691
  )
1692
+ if theme == "misunderstood_friend":
1693
+ return _numbered_suggestions(
1694
+ [
1695
+ f"把「{anchor}」先翻译成“我有一部分想被听见”。",
1696
+ "今天只写一句温和澄清,不急着证明整件事谁对谁错。",
1697
+ ],
1698
+ language,
1699
+ )
1700
  if theme == "message_loss":
1701
  return _numbered_suggestions(
1702
  [
 
2007
  continue
2008
  if item in clean:
2009
  return True
2010
+ tokens = [
2011
+ token
2012
+ for token in re.split(r"[\s,,。::;;、]+", item)
2013
+ if len(token) >= 3 and token not in _ANCHOR_STOPWORDS
2014
+ ]
2015
  if any(token in clean for token in tokens):
2016
  return True
2017
  return False
 
2329
  "那张便签或“回家”的感觉,更像想联系某个人,还是想回到一种安定状态?",
2330
  "最后只确认今天的需要:你想整理一个现实线索,还是先给自己一点回家的感觉?",
2331
  ],
2332
+ "misunderstood_friend": [
2333
+ f"在「{story_anchor}」里,最重的是被误解、没人听见,还是醒来后的受伤感?",
2334
+ "如果今天只照顾一个部分,你更想先被理解、先澄清一句话,还是先让自己缓一下?",
2335
+ "最后只确认今天的需要:你想要一句安慰,还是一个很小的沟通动作?",
2336
+ ],
2337
  "message_loss": [
2338
  "那条消息最刺痛你的地方,是它出现了、消失了,还是它来自某个具体的人?",
2339
  "这个梦更像在问“我还在等什么”,还是在问“我该怎么照顾还没放下的部分”?",
 
2362
  "Does the stuckness feel more like being late to start, or like trying something and getting no response?",
2363
  "Last check before the tip: would one small action help most, or a sentence that lowers the pressure first?",
2364
  ],
2365
+ "misunderstood_friend": [
2366
+ f"In {story_anchor}, what stayed strongest: being misunderstood, not being heard, or waking up hurt?",
2367
+ "If you cared for only one part today, would you want understanding, one clarifying sentence, or a pause before responding?",
2368
+ "Last check before the tip: would comfort help most, or one small communication step?",
2369
+ ],
2370
  }
2371
  fallback = [
2372
  f"In {story_anchor}, what feeling stayed strongest after waking?",
dream_customs/ui/app.py CHANGED
@@ -42,9 +42,9 @@ from dream_customs.ui.copy import (
42
  default_mood_for,
43
  mood_options_for,
44
  normalize_language,
45
- EXAMPLE_CHIPS,
46
  EXAMPLE_DREAMS,
47
  EXAMPLE_MOODS,
 
48
  )
49
  from dream_customs.ui.styles import CSS
50
 
@@ -888,10 +888,10 @@ def _section_title_html(number: int, text: str) -> str:
888
  """.strip()
889
 
890
 
891
- def _demo_chip_intro_html(language: str = DEFAULT_LANGUAGE) -> str:
892
  copy = copy_for(language)
893
  return f"""
894
- <div class="dc-demo-chip-intro">
895
  <span>{escape(copy['demo_intro_label'])}</span>
896
  <strong>{escape(copy['demo_intro_body'])}</strong>
897
  </div>
@@ -992,24 +992,18 @@ def _side_stamp_html(language: str = DEFAULT_LANGUAGE) -> str:
992
  """.strip()
993
 
994
 
995
- def _example_chip(selected_language: str, chip_key: str):
996
  selected_language = normalize_language(selected_language)
997
- chip = EXAMPLE_CHIPS[selected_language].get(chip_key)
998
- if chip:
999
- return chip
1000
- return EXAMPLE_DREAMS[selected_language], EXAMPLE_MOODS[selected_language]
1001
 
1002
 
1003
- def _example_elevator(selected_language: str):
1004
- return _example_chip(selected_language, "elevator")
1005
-
1006
-
1007
- def _example_floor14(selected_language: str):
1008
- return _example_chip(selected_language, "floor14")
1009
-
1010
-
1011
- def _example_melting(selected_language: str):
1012
- return _example_chip(selected_language, "melting")
1013
 
1014
 
1015
  def _dev_help_html(language: str = DEFAULT_LANGUAGE) -> str:
@@ -1075,29 +1069,20 @@ def build_demo() -> gr.Blocks:
1075
  )
1076
  _make_media_api_info_client_safe(audio_input)
1077
  field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
1078
- demo_chip_intro = gr.HTML(_demo_chip_intro_html(DEFAULT_LANGUAGE))
1079
  with gr.Row(elem_classes=["dc-submit-row"]):
1080
  submit_button = gr.Button(
1081
  initial_copy["submit_button"],
1082
  variant="primary",
1083
  elem_classes=["dc-submit-button"],
1084
  )
1085
- with gr.Row(elem_classes=["dc-demo-chip-row"]):
1086
- example_button = gr.Button(
1087
- initial_copy["example_button"],
1088
- variant="secondary",
1089
- elem_classes=["dc-demo-chip"],
1090
- )
1091
- example_button_2 = gr.Button(
1092
- initial_copy["example_button_2"],
1093
- variant="secondary",
1094
- elem_classes=["dc-demo-chip"],
1095
- )
1096
- example_button_3 = gr.Button(
1097
- initial_copy["example_button_3"],
1098
- variant="secondary",
1099
- elem_classes=["dc-demo-chip"],
1100
- )
1101
  processing_html = gr.HTML(_processing_html(DEFAULT_LANGUAGE))
1102
 
1103
  with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
@@ -1349,23 +1334,9 @@ def build_demo() -> gr.Blocks:
1349
  show_api=False,
1350
  )
1351
 
1352
- example_button.click(
1353
- _example_elevator,
1354
- inputs=[language],
1355
- outputs=[dream_text, mood],
1356
- api_name=False,
1357
- show_api=False,
1358
- )
1359
- example_button_2.click(
1360
- _example_floor14,
1361
- inputs=[language],
1362
- outputs=[dream_text, mood],
1363
- api_name=False,
1364
- show_api=False,
1365
- )
1366
- example_button_3.click(
1367
- _example_melting,
1368
- inputs=[language],
1369
  outputs=[dream_text, mood],
1370
  api_name=False,
1371
  show_api=False,
@@ -1384,10 +1355,12 @@ def build_demo() -> gr.Blocks:
1384
  _attachment_html(selected_language),
1385
  gr.update(label=copy["image_label"]),
1386
  _field_tip_html(selected_language),
1387
- _demo_chip_intro_html(selected_language),
1388
- gr.update(value=copy["example_button"]),
1389
- gr.update(value=copy["example_button_2"]),
1390
- gr.update(value=copy["example_button_3"]),
 
 
1391
  gr.update(value=copy["submit_button"]),
1392
  _processing_html(selected_language),
1393
  _question_markdown({"question": ""}, selected_language),
@@ -1421,10 +1394,8 @@ def build_demo() -> gr.Blocks:
1421
  attachment_html,
1422
  image_input,
1423
  field_tip_html,
1424
- demo_chip_intro,
1425
- example_button,
1426
- example_button_2,
1427
- example_button_3,
1428
  submit_button,
1429
  processing_html,
1430
  question_markdown,
 
42
  default_mood_for,
43
  mood_options_for,
44
  normalize_language,
 
45
  EXAMPLE_DREAMS,
46
  EXAMPLE_MOODS,
47
+ EXAMPLE_SLIPS,
48
  )
49
  from dream_customs.ui.styles import CSS
50
 
 
888
  """.strip()
889
 
890
 
891
+ def _demo_slip_intro_html(language: str = DEFAULT_LANGUAGE) -> str:
892
  copy = copy_for(language)
893
  return f"""
894
+ <div class="dc-example-intro">
895
  <span>{escape(copy['demo_intro_label'])}</span>
896
  <strong>{escape(copy['demo_intro_body'])}</strong>
897
  </div>
 
992
  """.strip()
993
 
994
 
995
+ def _example_choices(selected_language: str):
996
  selected_language = normalize_language(selected_language)
997
+ return [(slip[0], key) for key, slip in EXAMPLE_SLIPS[selected_language].items()]
 
 
 
998
 
999
 
1000
+ def _example_slip(selected_language: str, slip_key: str):
1001
+ selected_language = normalize_language(selected_language)
1002
+ slip = EXAMPLE_SLIPS[selected_language].get(slip_key or "")
1003
+ if slip:
1004
+ _label, dream, mood = slip
1005
+ return dream, mood
1006
+ return "", default_mood_for(selected_language)
 
 
 
1007
 
1008
 
1009
  def _dev_help_html(language: str = DEFAULT_LANGUAGE) -> str:
 
1069
  )
1070
  _make_media_api_info_client_safe(audio_input)
1071
  field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
1072
+ demo_slip_intro = gr.HTML(_demo_slip_intro_html(DEFAULT_LANGUAGE))
1073
  with gr.Row(elem_classes=["dc-submit-row"]):
1074
  submit_button = gr.Button(
1075
  initial_copy["submit_button"],
1076
  variant="primary",
1077
  elem_classes=["dc-submit-button"],
1078
  )
1079
+ example_select = gr.Dropdown(
1080
+ label=initial_copy["example_select_label"],
1081
+ choices=_example_choices(DEFAULT_LANGUAGE),
1082
+ value=None,
1083
+ elem_classes=["dc-example-select"],
1084
+ interactive=True,
1085
+ )
 
 
 
 
 
 
 
 
 
1086
  processing_html = gr.HTML(_processing_html(DEFAULT_LANGUAGE))
1087
 
1088
  with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
 
1334
  show_api=False,
1335
  )
1336
 
1337
+ example_select.change(
1338
+ _example_slip,
1339
+ inputs=[language, example_select],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1340
  outputs=[dream_text, mood],
1341
  api_name=False,
1342
  show_api=False,
 
1355
  _attachment_html(selected_language),
1356
  gr.update(label=copy["image_label"]),
1357
  _field_tip_html(selected_language),
1358
+ _demo_slip_intro_html(selected_language),
1359
+ gr.update(
1360
+ label=copy["example_select_label"],
1361
+ choices=_example_choices(selected_language),
1362
+ value=None,
1363
+ ),
1364
  gr.update(value=copy["submit_button"]),
1365
  _processing_html(selected_language),
1366
  _question_markdown({"question": ""}, selected_language),
 
1394
  attachment_html,
1395
  image_input,
1396
  field_tip_html,
1397
+ demo_slip_intro,
1398
+ example_select,
 
 
1399
  submit_button,
1400
  processing_html,
1401
  question_markdown,
dream_customs/ui/copy.py CHANGED
@@ -16,7 +16,7 @@ APP_COPY = {
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.",
22
  "notice_error": "Dream QA needs a dream fragment before it can continue.",
@@ -35,9 +35,8 @@ APP_COPY = {
35
  "voice_label": "Voice note",
36
  "voice_help": "Record or upload a short voice note. It is sent to MiMo ASR when you continue.",
37
  "field_tip": "The desk looks for three anchors: a place, an object, and the question the dream left behind.",
38
- "example_button": "elevator",
39
- "example_button_2": "floor 14",
40
- "example_button_3": "melting buttons",
41
  "submit_button": "Ask one question",
42
  "processing_note": (
43
  "After submission, the desk extracts dream anchors, asks one grounded question, then writes a Morning Ticket with one Today Tip."
@@ -66,9 +65,9 @@ APP_COPY = {
66
  "copy_label": "Copyable result",
67
  "side_title": "Waking mood",
68
  "mood_label": "Mood",
69
- "side_stamp_label": "90-second demo",
70
- "side_stamp_title": "Elevator, floor 14, first sentence",
71
- "side_stamp_body": "The strongest path ends with a tiny action tied to a real overdue email.",
72
  "desk_rule_label": "Morning desk rule",
73
  "desk_rule_title": "Do not solve the whole dream.",
74
  "desk_rule_body": "Catch the strange object, name the feeling, ask one useful question.",
@@ -92,7 +91,7 @@ APP_COPY = {
92
  "hero_mobile_note": "适合半醒时使用",
93
  "brand_subtitle": "Dream Customs",
94
  "steps": ["记录", "明确问题", "回答追问", "Tips"],
95
- "notice_record": "写一个梦境片段;想走 90 秒演示路径,也可以直接点示例 chip。",
96
  "notice_ask": "回答这一个追问,或跳过,用已有梦境线索生成清晨小票。",
97
  "notice_tip": "清晨小票已生成。把它当作温和参考,不是诊断或预言。",
98
  "notice_error": "梦境问答台还没有收到片段。",
@@ -111,9 +110,8 @@ APP_COPY = {
111
  "voice_label": "语音片段",
112
  "voice_help": "可以录音或上传一小段语音。点击继续后,会直接发送给 MiMo ASR 转写。",
113
  "field_tip": "问讯室会优先寻找三个锚点:地点、物件、以及梦醒后留下的问题。",
114
- "example_button": "电梯",
115
- "example_button_2": "14 楼",
116
- "example_button_3": "融化按钮",
117
  "submit_button": "问一个问题",
118
  "processing_note": (
119
  "提交后,问讯室会提取梦境锚点,问一个关键问题,再写成一张清晨小票。"
@@ -142,9 +140,9 @@ APP_COPY = {
142
  "copy_label": "可复制结果",
143
  "side_title": "醒来后的心情",
144
  "mood_label": "心情",
145
- "side_stamp_label": "90 秒演示",
146
- "side_stamp_title": "电梯、14 楼、第句话",
147
- "side_stamp_body": "最稳的演示路径把梦境锚点落到封迟迟没写邮件。",
148
  "desk_rule_label": "清晨桌面规则",
149
  "desk_rule_title": "不要解完整个梦。",
150
  "desk_rule_body": "先抓住奇怪的物件,说出醒来的感受,再问一个今天能用的问题。",
@@ -161,39 +159,45 @@ APP_COPY = {
161
  }
162
 
163
  EXAMPLE_DREAMS = {
164
- "en": "I dreamed I kept missing an elevator. The button for floor 14 melted like wax. I woke up anxious because it reminded me of an overdue email.",
165
- "zh": "我梦到自己一直赶不上14 楼按钮像蜡样融化醒来有点焦虑,因为它让我想封迟迟没写邮件。",
166
  }
167
 
168
  EXAMPLE_MOODS = {"en": "Uneasy", "zh": "焦虑"}
169
 
170
- EXAMPLE_CHIPS = {
171
  "en": {
172
- "elevator": (
173
- "I dreamed I kept missing an elevator. The doors opened, closed, and left me behind.",
 
174
  "Uneasy",
175
  ),
176
- "floor14": (
177
- "I dreamed I kept missing an elevator, and the floor number stayed on 14.",
 
178
  "Uneasy",
179
  ),
180
- "melting": (
181
- "I dreamed I kept missing an elevator. The button for floor 14 melted like wax. I woke up anxious because it reminded me of an overdue email.",
182
- "Uneasy",
 
183
  ),
184
  },
185
  "zh": {
186
- "elevator": (
187
- "我梦自己一直赶不上电梯,门开了又关,好像总是把我留在外面。",
 
188
  "焦虑",
189
  ),
190
- "floor14": (
191
- "我梦到自己一直赶不上电梯,楼层数字一直停在 14。",
 
192
  "焦虑",
193
  ),
194
- "melting": (
195
- "我梦到自己一直赶不上电梯,14 楼按钮像蜡一样融化。醒来有点焦虑,因为它让我想到一封迟迟没写邮件。",
196
- "焦虑",
 
197
  ),
198
  },
199
  }
 
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, or choose a sample dream slip 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.",
22
  "notice_error": "Dream QA needs a dream fragment before it can continue.",
 
35
  "voice_label": "Voice note",
36
  "voice_help": "Record or upload a short voice note. It is sent to MiMo ASR when you continue.",
37
  "field_tip": "The desk looks for three anchors: a place, an object, and the question the dream left behind.",
38
+ "example_select_label": "Sample dream",
39
+ "example_select_placeholder": "Choose a dream slip...",
 
40
  "submit_button": "Ask one question",
41
  "processing_note": (
42
  "After submission, the desk extracts dream anchors, asks one grounded question, then writes a Morning Ticket with one Today Tip."
 
65
  "copy_label": "Copyable result",
66
  "side_title": "Waking mood",
67
  "mood_label": "Mood",
68
+ "side_stamp_label": "Quick demo path",
69
+ "side_stamp_title": "Pick a slip, answer or skip",
70
+ "side_stamp_body": "The strongest path shows one grounded question and one Today Tip.",
71
  "desk_rule_label": "Morning desk rule",
72
  "desk_rule_title": "Do not solve the whole dream.",
73
  "desk_rule_body": "Catch the strange object, name the feeling, ask one useful question.",
 
91
  "hero_mobile_note": "适合半醒时使用",
92
  "brand_subtitle": "Dream Customs",
93
  "steps": ["记录", "明确问题", "回答追问", "Tips"],
94
+ "notice_record": "写一个梦境片段;想走 90 秒演示路径,也可以示例梦境里选一条。",
95
  "notice_ask": "回答这一个追问,或跳过,用已有梦境线索生成清晨小票。",
96
  "notice_tip": "清晨小票已生成。把它当作温和参考,不是诊断或预言。",
97
  "notice_error": "梦境问答台还没有收到片段。",
 
110
  "voice_label": "语音片段",
111
  "voice_help": "可以录音或上传一小段语音。点击继续后,会直接发送给 MiMo ASR 转写。",
112
  "field_tip": "问讯室会优先寻找三个锚点:地点、物件、以及梦醒后留下的问题。",
113
+ "example_select_label": "示例梦境",
114
+ "example_select_placeholder": "选择一张梦境纸片...",
 
115
  "submit_button": "问一个问题",
116
  "processing_note": (
117
  "提交后,问讯室会提取梦境锚点,问一个关键问题,再写成一张清晨小票。"
 
140
  "copy_label": "可复制结果",
141
  "side_title": "醒来后的心情",
142
  "mood_label": "心情",
143
+ "side_stamp_label": "快速演示路径",
144
+ "side_stamp_title": "张纸片,回答或跳过",
145
+ "side_stamp_body": "最稳的演示会展示个贴着细节追问和一个今日小 Tips。",
146
  "desk_rule_label": "清晨桌面规则",
147
  "desk_rule_title": "不要解完整个梦。",
148
  "desk_rule_body": "先抓住奇怪的物件,说出醒来的感受,再问一个今天能用的问题。",
 
159
  }
160
 
161
  EXAMPLE_DREAMS = {
162
+ "en": "I dreamed I was late, my phone battery died, and the elevator never arrived. I wanted a quick tip and felt stuck at the entrance.",
163
+ "zh": "我梦到自己快迟到了,手机没电,电梯直没���。我想个很快能用小建议,但梦里像被卡在入口。",
164
  }
165
 
166
  EXAMPLE_MOODS = {"en": "Uneasy", "zh": "焦虑"}
167
 
168
+ EXAMPLE_SLIPS = {
169
  "en": {
170
+ "late_elevator": (
171
+ "Late elevator and dead phone",
172
+ "I dreamed I was late, my phone battery died, and the elevator never arrived. I wanted a quick tip and felt stuck at the entrance.",
173
  "Uneasy",
174
  ),
175
+ "friend_misread": (
176
+ "A friend misunderstood me",
177
+ "I dreamed I sent a message to a friend, but every reply bubble turned into fog. I woke up worried I had been misunderstood.",
178
  "Uneasy",
179
  ),
180
+ "sleepless_loop": (
181
+ "The same sleepless room",
182
+ "I dreamed I kept waking up inside the same room, checking the clock each time, but morning never arrived.",
183
+ "Tired",
184
  ),
185
  },
186
  "zh": {
187
+ "late_elevator": (
188
+ "电梯和没电手机",
189
+ "我梦到自己快迟到了,手机没电,电梯一直没来。我想要一个很快能用的小建议,但梦里像被卡在入口。",
190
  "焦虑",
191
  ),
192
+ "friend_misread": (
193
+ "朋友误解了我",
194
+ "我梦到自己给朋友发消息,但每个回复气泡都变成雾。醒来后担心自己是不是被误解了。",
195
  "焦虑",
196
  ),
197
+ "sleepless_loop": (
198
+ "反复醒来的房间",
199
+ "我梦到自己一次次在同一个房间醒来,每次都看钟,可早晨一直没有到。",
200
+ "疲惫",
201
  ),
202
  },
203
  }
dream_customs/ui/styles.py CHANGED
@@ -1210,6 +1210,48 @@ a.built-with[href*="gradio.app"] {
1210
  """
1211
 
1212
  CSS += """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1213
  .dc-question-original p {
1214
  font-size: clamp(1.16rem, 2.4vw, 1.45rem);
1215
  font-weight: 720;
 
1210
  """
1211
 
1212
  CSS += """
1213
+ .dc-example-intro {
1214
+ align-items: baseline;
1215
+ color: var(--dqa-muted);
1216
+ display: flex;
1217
+ flex-wrap: wrap;
1218
+ gap: 8px;
1219
+ margin: 18px 0 0;
1220
+ }
1221
+
1222
+ .dc-example-intro span {
1223
+ color: var(--dqa-sage-deep);
1224
+ font-size: 0.78rem;
1225
+ font-weight: 820;
1226
+ text-transform: uppercase;
1227
+ }
1228
+
1229
+ .dc-example-intro strong {
1230
+ color: #59675f;
1231
+ font-size: 0.92rem;
1232
+ font-weight: 650;
1233
+ }
1234
+
1235
+ .dc-example-select {
1236
+ min-width: 0 !important;
1237
+ }
1238
+
1239
+ .dc-example-select .wrap,
1240
+ .dc-example-select .container,
1241
+ .dc-example-select input[role="listbox"] {
1242
+ background: #fff !important;
1243
+ border-color: rgba(79, 138, 88, 0.26) !important;
1244
+ border-radius: 12px !important;
1245
+ color: var(--dqa-ink) !important;
1246
+ min-height: 58px !important;
1247
+ }
1248
+
1249
+ .dc-example-select label,
1250
+ .dc-example-select [data-testid="block-label"] {
1251
+ color: var(--dqa-sage-deep) !important;
1252
+ font-size: 0.82rem !important;
1253
+ }
1254
+
1255
  .dc-question-original p {
1256
  font-size: clamp(1.16rem, 2.4vw, 1.45rem);
1257
  font-weight: 720;
tests/test_pipeline.py CHANGED
@@ -13,6 +13,7 @@ from dream_customs.pipeline import (
13
  create_session,
14
  dated_permit_id,
15
  draft_pact,
 
16
  generate_negotiation,
17
  generate_pact,
18
  generate_today_tip,
@@ -669,6 +670,52 @@ def test_today_tip_keeps_friend_misunderstanding_emotional_instead_of_email_draf
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):
 
13
  create_session,
14
  dated_permit_id,
15
  draft_pact,
16
+ finish_today_tip,
17
  generate_negotiation,
18
  generate_pact,
19
  generate_today_tip,
 
670
  assert not re.search(r"\b[123]\.", card.today_tip)
671
 
672
 
673
+ def test_friend_misunderstanding_skip_path_keeps_relationship_emotion_anchors():
674
+ dream_text = (
675
+ "I dreamed my close friend misunderstood me. I kept explaining, but nobody heard me, "
676
+ "and I woke up feeling hurt and invisible."
677
+ )
678
+ session = add_evidence(
679
+ create_session(),
680
+ dream_text=dream_text,
681
+ mood="Hurt and invisible",
682
+ vision_client=FakeVisionClient(),
683
+ asr_client=FakeASRClient(),
684
+ language="en",
685
+ )
686
+
687
+ assert "close friend" in session.qa_state.dream_anchors
688
+ assert "being misunderstood" in session.qa_state.dream_anchors
689
+ assert "nobody heard me" in session.qa_state.dream_anchors
690
+ assert "feeling invisible" in session.qa_state.dream_anchors
691
+ assert "feeling hurt" in session.qa_state.dream_anchors
692
+
693
+ session = ask_questions(session, FakeTextClient(), language="en")
694
+ question_text = session.question_history[0].lower()
695
+
696
+ assert "friend" in question_text
697
+ assert "misunderstood" in question_text or "heard" in question_text or "hurt" in question_text
698
+ assert "former partner" not in question_text
699
+ assert "disappearance" not in question_text
700
+ assert "dream clues" not in question_text
701
+
702
+ session = skip_question(session, language="en")
703
+ session = finish_today_tip(session, FakeTextClient(), language="en")
704
+ card = session.sealed_tip
705
+ combined = _today_tip_public_text(card).lower()
706
+
707
+ assert "friend" in combined
708
+ assert "misunderstood" in combined or "heard" in combined
709
+ assert "invisible" in combined or "hurt" in combined
710
+ assert "former partner" not in combined
711
+ assert "disappearance" not in combined
712
+ assert "dream clues" not in combined
713
+ assert "stuck before starting" not in combined
714
+ assert "overdue email" not in combined
715
+ assert "first sentence of the email" not in combined
716
+ assert not re.search(r"\b[123]\.", card.today_tip)
717
+
718
+
719
  def test_today_tip_skip_path_uses_original_anchors_without_default_email_or_broken_caring_note():
720
  class PollutedSkipClient:
721
  def generate_today_tip(self, prompt):
tests/test_ui_actions.py CHANGED
@@ -134,17 +134,33 @@ def test_hero_subtitle_explains_app_instead_of_legacy_name():
134
  assert "Dream Customs" not in hero_html
135
 
136
 
137
- def test_composer_has_three_real_demo_chips():
138
  source = inspect.getsource(ui_app.build_demo)
139
 
140
- assert 'elem_classes=["dc-demo-chip-row"]' in source
141
- assert source.count('elem_classes=["dc-demo-chip"]') == 3
142
- assert "_example_elevator" in source
143
- assert "_example_floor14" in source
144
- assert "_example_melting" in source
145
- assert ".dc-demo-chip-row" in CSS
146
- assert "Dream slips" in ui_app._demo_chip_intro_html("en")
147
- assert "梦境纸片" in ui_app._demo_chip_intro_html("zh")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
 
150
  def test_side_panel_frames_the_morning_desk_rule():
@@ -158,8 +174,13 @@ def test_side_panel_frames_the_morning_desk_rule():
158
  assert "Text" in html
159
  assert "Image" in html
160
  assert "Voice" in html
 
 
 
161
  assert "清晨桌面规则" in zh_html
162
  assert "文字" in zh_html
 
 
163
 
164
 
165
  def test_side_panel_dropdown_hover_stays_readable():
 
134
  assert "Dream Customs" not in hero_html
135
 
136
 
137
+ def test_composer_uses_one_sample_dream_dropdown():
138
  source = inspect.getsource(ui_app.build_demo)
139
 
140
+ assert 'elem_classes=["dc-example-select"]' in source
141
+ assert "example_select = gr.Dropdown(" in source
142
+ assert "_example_slip" in source
143
+ assert "_example_elevator" not in source
144
+ assert 'elem_classes=["dc-demo-chip-row"]' not in source
145
+ assert source.count('elem_classes=["dc-demo-chip"]') == 0
146
+ assert ".dc-example-select" in CSS
147
+ assert "Dream slips" in ui_app._demo_slip_intro_html("en")
148
+ assert "梦境纸片" in ui_app._demo_slip_intro_html("zh")
149
+
150
+ choices = ui_app._example_choices("en")
151
+ labels = [label for label, _key in choices]
152
+ assert labels == [
153
+ "Late elevator and dead phone",
154
+ "A friend misunderstood me",
155
+ "The same sleepless room",
156
+ ]
157
+ dream_text, mood = ui_app._example_slip("en", "friend_misread")
158
+ assert "reply bubble" in dream_text
159
+ assert mood == "Uneasy"
160
+
161
+ zh_text, zh_mood = ui_app._example_slip("zh", "sleepless_loop")
162
+ assert "同一个房间" in zh_text
163
+ assert zh_mood == "疲惫"
164
 
165
 
166
  def test_side_panel_frames_the_morning_desk_rule():
 
174
  assert "Text" in html
175
  assert "Image" in html
176
  assert "Voice" in html
177
+ assert "first sentence" not in html.lower()
178
+ assert "overdue email" not in html.lower()
179
+ assert "Pick a slip" in html
180
  assert "清晨桌面规则" in zh_html
181
  assert "文字" in zh_html
182
+ assert "第一句话" not in zh_html
183
+ assert "邮件" not in zh_html
184
 
185
 
186
  def test_side_panel_dropdown_hover_stays_readable():