Polish Dream QA morning desk UI

#39
by ADJCJH - opened
dream_customs/render.py CHANGED
@@ -53,6 +53,8 @@ def render_today_tip_card(card: TodayTipCard, language: str = "en") -> str:
53
  if is_zh
54
  else "MiniCPM-V reads sketches, notes, and image clues; MiniCPM5-1B writes the follow-up question, reflection, and ticket."
55
  ),
 
 
56
  }
57
  tiny_action = (
58
  f"<section class='dqa-ticket-row'><h3>{labels['tiny_action']}</h3><p>{escape(card.tiny_action)}</p></section>"
@@ -81,6 +83,10 @@ def render_today_tip_card(card: TodayTipCard, language: str = "en") -> str:
81
  <h2>{labels['page']}</h2>
82
  <p>{labels['thanks']}</p>
83
  </div>
 
 
 
 
84
  </div>
85
  <div class="dqa-anchor-chips" aria-label="{labels['anchors']}">{anchor_chips}</div>
86
  <article class="dqa-morning-ticket">
 
53
  if is_zh
54
  else "MiniCPM-V reads sketches, notes, and image clues; MiniCPM5-1B writes the follow-up question, reflection, and ticket."
55
  ),
56
+ "ticket_stamp_label": "今天只带走一张" if is_zh else "Take one note today",
57
+ "ticket_stamp_title": "贴着梦境细节" if is_zh else "Grounded in the dream",
58
  }
59
  tiny_action = (
60
  f"<section class='dqa-ticket-row'><h3>{labels['tiny_action']}</h3><p>{escape(card.tiny_action)}</p></section>"
 
83
  <h2>{labels['page']}</h2>
84
  <p>{labels['thanks']}</p>
85
  </div>
86
+ <div class="dqa-ticket-stamp" aria-hidden="true">
87
+ <span>{labels['ticket_stamp_label']}</span>
88
+ <strong>{labels['ticket_stamp_title']}</strong>
89
+ </div>
90
  </div>
91
  <div class="dqa-anchor-chips" aria-label="{labels['anchors']}">{anchor_chips}</div>
92
  <article class="dqa-morning-ticket">
dream_customs/ui/app.py CHANGED
@@ -432,7 +432,10 @@ def _question_markdown(view: dict, language: str = DEFAULT_LANGUAGE) -> str:
432
  anchor_label = "梦境锚点" if language == "zh" else "Dream anchors"
433
  anchor_chips = "".join(f"<span>{escape(anchor)}</span>" for anchor in anchor_items)
434
  anchor_strip = (
 
 
435
  f"<div class='dc-question-anchor-strip' aria-label='{anchor_label}'>{anchor_chips}</div>"
 
436
  if anchor_chips
437
  else ""
438
  )
@@ -697,6 +700,10 @@ def _hero_html(language: str = DEFAULT_LANGUAGE, status: str = "record") -> str:
697
  <div class="dc-sun-mark" aria-hidden="true"></div>
698
  </div>
699
  <p class="dc-hero-body">{copy['hero_body']}</p>
 
 
 
 
700
  <div class="dc-stepper" aria-label="Dream QA steps">
701
  {''.join(step_html)}
702
  </div>
@@ -714,12 +721,13 @@ def _section_title_html(number: int, text: str) -> str:
714
 
715
 
716
  def _demo_chip_intro_html(language: str = DEFAULT_LANGUAGE) -> str:
717
- message = (
718
- "Or tap the 90-second demo trail:"
719
- if normalize_language(language) == "en"
720
- else "也可以点一个 90 秒演示线索:"
721
- )
722
- return f'<p class="dc-demo-chip-intro">{escape(message)}</p>'
 
723
 
724
 
725
  def _mic_html(language: str = DEFAULT_LANGUAGE) -> str:
@@ -774,7 +782,13 @@ def _field_tip_html(language: str = DEFAULT_LANGUAGE, view=None) -> str:
774
  else "梦境记录需要至少一句文字、图片或语音线索,然后再继续。"
775
  )
776
  return f'<p class="dc-field-tip is-error" role="alert">{escape(message)}</p>'
777
- return f"<p class=\"dc-field-tip\">{escape(copy['field_tip'])}</p>"
 
 
 
 
 
 
778
 
779
 
780
  def _processing_html(language: str = DEFAULT_LANGUAGE) -> str:
@@ -783,11 +797,23 @@ def _processing_html(language: str = DEFAULT_LANGUAGE) -> str:
783
 
784
  def _side_stamp_html(language: str = DEFAULT_LANGUAGE) -> str:
785
  copy = copy_for(language)
 
786
  return f"""
787
- <div class="dc-side-stamp">
788
- <span>{escape(copy['side_stamp_label'])}</span>
789
- <strong>{escape(copy['side_stamp_title'])}</strong>
790
- <small>{escape(copy['side_stamp_body'])}</small>
 
 
 
 
 
 
 
 
 
 
 
791
  </div>
792
  """.strip()
793
 
 
432
  anchor_label = "梦境锚点" if language == "zh" else "Dream anchors"
433
  anchor_chips = "".join(f"<span>{escape(anchor)}</span>" for anchor in anchor_items)
434
  anchor_strip = (
435
+ "<div class='dc-question-anchor-wrap'>"
436
+ f"<span class='dc-question-anchor-label'>{escape(copy['question_anchor_label'])}</span>"
437
  f"<div class='dc-question-anchor-strip' aria-label='{anchor_label}'>{anchor_chips}</div>"
438
+ "</div>"
439
  if anchor_chips
440
  else ""
441
  )
 
700
  <div class="dc-sun-mark" aria-hidden="true"></div>
701
  </div>
702
  <p class="dc-hero-body">{copy['hero_body']}</p>
703
+ <div class="dc-hero-ribbon" aria-label="{escape(copy['hero_badge'])}">
704
+ <span>{escape(copy['hero_badge'])}</span>
705
+ <small>{escape(copy['hero_mobile_note'])}</small>
706
+ </div>
707
  <div class="dc-stepper" aria-label="Dream QA steps">
708
  {''.join(step_html)}
709
  </div>
 
721
 
722
 
723
  def _demo_chip_intro_html(language: str = DEFAULT_LANGUAGE) -> str:
724
+ copy = copy_for(language)
725
+ return f"""
726
+ <div class="dc-demo-chip-intro">
727
+ <span>{escape(copy['demo_intro_label'])}</span>
728
+ <strong>{escape(copy['demo_intro_body'])}</strong>
729
+ </div>
730
+ """.strip()
731
 
732
 
733
  def _mic_html(language: str = DEFAULT_LANGUAGE) -> str:
 
782
  else "梦境记录需要至少一句文字、图片或语音线索,然后再继续。"
783
  )
784
  return f'<p class="dc-field-tip is-error" role="alert">{escape(message)}</p>'
785
+ return f"""
786
+ <div class="dc-field-tip">
787
+ <span>{escape(copy['desk_rule_label'])}</span>
788
+ <strong>{escape(copy['desk_rule_title'])}</strong>
789
+ <p>{escape(copy['field_tip'])}</p>
790
+ </div>
791
+ """.strip()
792
 
793
 
794
  def _processing_html(language: str = DEFAULT_LANGUAGE) -> str:
 
797
 
798
  def _side_stamp_html(language: str = DEFAULT_LANGUAGE) -> str:
799
  copy = copy_for(language)
800
+ intake_items = "".join(f"<span>{escape(item)}</span>" for item in copy["intake_items"])
801
  return f"""
802
+ <div class="dc-side-stack">
803
+ <div class="dc-side-stamp">
804
+ <span>{escape(copy['side_stamp_label'])}</span>
805
+ <strong>{escape(copy['side_stamp_title'])}</strong>
806
+ <small>{escape(copy['side_stamp_body'])}</small>
807
+ </div>
808
+ <div class="dc-desk-rule">
809
+ <span>{escape(copy['desk_rule_label'])}</span>
810
+ <strong>{escape(copy['desk_rule_title'])}</strong>
811
+ <p>{escape(copy['desk_rule_body'])}</p>
812
+ </div>
813
+ <div class="dc-intake-rail" aria-label="{escape(copy['intake_label'])}">
814
+ <small>{escape(copy['intake_label'])}</small>
815
+ <div>{intake_items}</div>
816
+ </div>
817
  </div>
818
  """.strip()
819
 
dream_customs/ui/copy.py CHANGED
@@ -12,6 +12,8 @@ APP_COPY = {
12
  "hero_title": "What did the dream leave you asking?",
13
  "subtitle": "Record the dream, answer one gentle question, and leave with a grounded Morning Ticket.",
14
  "hero_body": "A small-model dream desk for the first few minutes after waking. It gathers concrete details, asks before interpreting, and writes one note for today.",
 
 
15
  "brand_subtitle": "Dream Customs",
16
  "steps": ["Record", "One Question", "Today Tip"],
17
  "notice_record": "Write one dream fragment, then use a demo chip if you want the 90-second judge path.",
@@ -40,11 +42,14 @@ APP_COPY = {
40
  "image_label": "Image clue",
41
  "image_upload": "Upload image",
42
  "image_paste": "Paste from Clipboard",
 
 
43
  "question_kicker": "Question",
44
  "question_title": "One question before the ticket",
45
  "question_body": "Answer in one or two lines, or skip and let the existing anchors carry the ticket.",
46
  "question_speaker": "Morning Question Desk",
47
  "question_note": "This step makes the tip more specific. It is not diagnosis.",
 
48
  "answer_label": "Your answer",
49
  "answer_placeholder": "Write one answer, or leave it blank and skip.",
50
  "answer_button": "Send answer",
@@ -59,6 +64,11 @@ APP_COPY = {
59
  "side_stamp_label": "90-second demo",
60
  "side_stamp_title": "Elevator, floor 14, first sentence",
61
  "side_stamp_body": "The strongest path ends with a tiny action tied to a real overdue email.",
 
 
 
 
 
62
  "language_label": "Language",
63
  "runtime_help": "Advanced controls for demos and development. Most visitors can leave these unchanged.",
64
  "debug_title": "Debug",
@@ -73,6 +83,8 @@ APP_COPY = {
73
  "hero_title": "这个梦,醒来后把什么问题留给了你?",
74
  "subtitle": "记录梦境,回答一个温和追问,最后拿到一张贴着梦境细节的清晨小票。",
75
  "hero_body": "一个适合刚醒来几分钟使用的小模型问讯台:先捡起具体细节,再问一个好问题,最后只给今天的一张小纸条。",
 
 
76
  "brand_subtitle": "Dream Customs",
77
  "steps": ["记录", "一个追问", "清晨小票"],
78
  "notice_record": "写一个梦境片段;想走 90 秒演示路径,也可以直接点示例 chip。",
@@ -101,11 +113,14 @@ APP_COPY = {
101
  "image_label": "图片线索",
102
  "image_upload": "上传图片",
103
  "image_paste": "从剪贴板粘贴",
 
 
104
  "question_kicker": "追问",
105
  "question_title": "出票前,只问这一个问题",
106
  "question_body": "回答一两句就好;也可以跳过,让已有锚点直接生成小票。",
107
  "question_speaker": "清晨问讯室",
108
  "question_note": "这个步骤是为了让最终建议更贴近你的梦,不是问诊。",
 
109
  "answer_label": "你的回答",
110
  "answer_placeholder": "写一句回答,或留空后选择跳过。",
111
  "answer_button": "发送回答",
@@ -120,6 +135,11 @@ APP_COPY = {
120
  "side_stamp_label": "90 秒演示",
121
  "side_stamp_title": "电梯、14 楼、第一句话",
122
  "side_stamp_body": "最稳的演示路径会把梦境锚点落到一封迟迟没写的邮件。",
 
 
 
 
 
123
  "language_label": "语言",
124
  "runtime_help": "高级演示与开发控制。普通体验保持默认即可。",
125
  "debug_title": "调试",
 
12
  "hero_title": "What did the dream leave you asking?",
13
  "subtitle": "Record the dream, answer one gentle question, and leave with a grounded Morning Ticket.",
14
  "hero_body": "A small-model dream desk for the first few minutes after waking. It gathers concrete details, asks before interpreting, and writes one note for today.",
15
+ "hero_badge": "Text + image + voice intake",
16
+ "hero_mobile_note": "Made for the half-awake minute",
17
  "brand_subtitle": "Dream Customs",
18
  "steps": ["Record", "One Question", "Today Tip"],
19
  "notice_record": "Write one dream fragment, then use a demo chip if you want the 90-second judge path.",
 
42
  "image_label": "Image clue",
43
  "image_upload": "Upload image",
44
  "image_paste": "Paste from Clipboard",
45
+ "demo_intro_label": "Dream slips",
46
+ "demo_intro_body": "Pick one if you want the fast judge path.",
47
  "question_kicker": "Question",
48
  "question_title": "One question before the ticket",
49
  "question_body": "Answer in one or two lines, or skip and let the existing anchors carry the ticket.",
50
  "question_speaker": "Morning Question Desk",
51
  "question_note": "This step makes the tip more specific. It is not diagnosis.",
52
+ "question_anchor_label": "Sticky details already on the desk",
53
  "answer_label": "Your answer",
54
  "answer_placeholder": "Write one answer, or leave it blank and skip.",
55
  "answer_button": "Send answer",
 
64
  "side_stamp_label": "90-second demo",
65
  "side_stamp_title": "Elevator, floor 14, first sentence",
66
  "side_stamp_body": "The strongest path ends with a tiny action tied to a real overdue email.",
67
+ "desk_rule_label": "Morning desk rule",
68
+ "desk_rule_title": "Do not solve the whole dream.",
69
+ "desk_rule_body": "Catch the strange object, name the feeling, ask one useful question.",
70
+ "intake_label": "One intake",
71
+ "intake_items": ["Text", "Image", "Voice"],
72
  "language_label": "Language",
73
  "runtime_help": "Advanced controls for demos and development. Most visitors can leave these unchanged.",
74
  "debug_title": "Debug",
 
83
  "hero_title": "这个梦,醒来后把什么问题留给了你?",
84
  "subtitle": "记录梦境,回答一个温和追问,最后拿到一张贴着梦境细节的清晨小票。",
85
  "hero_body": "一个适合刚醒来几分钟使用的小模型问讯台:先捡起具体细节,再问一个好问题,最后只给今天的一张小纸条。",
86
+ "hero_badge": "文字 + 图片 + 语音统一入口",
87
+ "hero_mobile_note": "适合半醒时的一分钟",
88
  "brand_subtitle": "Dream Customs",
89
  "steps": ["记录", "一个追问", "清晨小票"],
90
  "notice_record": "写一个梦境片段;想走 90 秒演示路径,也可以直接点示例 chip。",
 
113
  "image_label": "图片线索",
114
  "image_upload": "上传图片",
115
  "image_paste": "从剪贴板粘贴",
116
+ "demo_intro_label": "梦境纸片",
117
+ "demo_intro_body": "想快速演示,可以先抽一张。",
118
  "question_kicker": "追问",
119
  "question_title": "出票前,只问这一个问题",
120
  "question_body": "回答一两句就好;也可以跳过,让已有锚点直接生成小票。",
121
  "question_speaker": "清晨问讯室",
122
  "question_note": "这个步骤是为了让最终建议更贴近你的梦,不是问诊。",
123
+ "question_anchor_label": "桌面上已经留下的细节",
124
  "answer_label": "你的回答",
125
  "answer_placeholder": "写一句回答,或留空后选择跳过。",
126
  "answer_button": "发送回答",
 
135
  "side_stamp_label": "90 秒演示",
136
  "side_stamp_title": "电梯、14 楼、第一句话",
137
  "side_stamp_body": "最稳的演示路径会把梦境锚点落到一封迟迟没写的邮件。",
138
+ "desk_rule_label": "清晨桌面规则",
139
+ "desk_rule_title": "不要解完整个梦。",
140
+ "desk_rule_body": "先抓住奇怪的物件,说出醒来的感受,再问一个今天能用的问题。",
141
+ "intake_label": "同一个入口",
142
+ "intake_items": ["文字", "图片", "语音"],
143
  "language_label": "语言",
144
  "runtime_help": "高级演示与开发控制。普通体验保持默认即可。",
145
  "debug_title": "调试",
dream_customs/ui/styles.py CHANGED
@@ -1199,6 +1199,445 @@ a.built-with[href*="gradio.app"] {
1199
  }
1200
  """
1201
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1202
  CSS += """
1203
  .dc-hero {
1204
  align-content: end;
 
1199
  }
1200
  """
1201
 
1202
+ CSS += """
1203
+ /* Reference-space polish: soft PawMap spacing, case-file step clarity, paper-note tactility. */
1204
+ :root {
1205
+ --dqa-dawn: #eef6f1;
1206
+ --dqa-desk: #f7eddc;
1207
+ --dqa-paper-warm: #fffaf0;
1208
+ --dqa-pencil: #2c3734;
1209
+ --dqa-sage-2: #5f8f68;
1210
+ --dqa-rose: #d67b67;
1211
+ --dqa-brass: #d7a842;
1212
+ --dqa-paper-line: rgba(95, 143, 104, 0.16);
1213
+ --dqa-table-shadow: 0 18px 44px rgba(52, 66, 55, 0.14);
1214
+ --dqa-paper-shadow: 5px 6px 0 rgba(44, 55, 52, 0.08);
1215
+ }
1216
+
1217
+ html,
1218
+ body,
1219
+ .gradio-container {
1220
+ background:
1221
+ linear-gradient(90deg, rgba(95, 143, 104, 0.045) 0 1px, transparent 1px 80px),
1222
+ linear-gradient(180deg, #eef7f2 0%, #fffaf0 45%, #f6ead8 100%) !important;
1223
+ }
1224
+
1225
+ .dc-shell {
1226
+ max-width: 1320px;
1227
+ }
1228
+
1229
+ .dc-hero {
1230
+ background:
1231
+ linear-gradient(180deg, rgba(247, 251, 246, 0.84), rgba(255, 250, 240, 0.96)),
1232
+ url("https://images.unsplash.com/photo-1494438639946-1ebd1d20bf85?auto=format&fit=crop&w=1600&q=80") center 55% / cover !important;
1233
+ border-bottom: 1px solid rgba(95, 143, 104, 0.18);
1234
+ min-height: clamp(280px, 36vw, 460px);
1235
+ padding-bottom: clamp(30px, 5vw, 58px);
1236
+ }
1237
+
1238
+ .dc-hero-top {
1239
+ align-items: start;
1240
+ }
1241
+
1242
+ .dc-hero-ribbon {
1243
+ align-items: center;
1244
+ background: rgba(255, 250, 240, 0.86);
1245
+ border: 1px solid rgba(95, 143, 104, 0.22);
1246
+ border-radius: 8px;
1247
+ box-shadow: 3px 4px 0 rgba(44, 55, 52, 0.07);
1248
+ color: var(--dqa-pencil);
1249
+ display: inline-flex;
1250
+ gap: 12px;
1251
+ justify-self: center;
1252
+ margin-top: 8px;
1253
+ max-width: min(100%, 680px);
1254
+ padding: 9px 12px;
1255
+ }
1256
+
1257
+ .dc-hero-ribbon span {
1258
+ color: var(--dqa-sage-deep);
1259
+ font-size: 0.84rem;
1260
+ font-weight: 780;
1261
+ }
1262
+
1263
+ .dc-hero-ribbon small {
1264
+ color: #65746e;
1265
+ font-size: 0.82rem;
1266
+ font-weight: 650;
1267
+ }
1268
+
1269
+ .dc-workspace-grid {
1270
+ gap: 20px !important;
1271
+ grid-template-columns: minmax(0, 1fr) minmax(288px, 334px) !important;
1272
+ margin-top: -20px;
1273
+ }
1274
+
1275
+ .dc-stage,
1276
+ .dc-side-panel,
1277
+ .dc-debug-panel {
1278
+ border-radius: 8px !important;
1279
+ box-shadow: var(--dqa-table-shadow);
1280
+ }
1281
+
1282
+ .dc-stage {
1283
+ background:
1284
+ linear-gradient(180deg, rgba(255, 252, 246, 0.98), rgba(250, 241, 227, 0.98)),
1285
+ var(--dqa-desk) !important;
1286
+ overflow: hidden;
1287
+ position: relative;
1288
+ }
1289
+
1290
+ .dc-stage::after {
1291
+ background: linear-gradient(90deg, rgba(95, 143, 104, 0.24), rgba(214, 123, 103, 0.2), rgba(215, 168, 66, 0.2));
1292
+ content: "";
1293
+ height: 5px;
1294
+ left: 0;
1295
+ position: absolute;
1296
+ right: 0;
1297
+ top: 0;
1298
+ }
1299
+
1300
+ .dc-stage .dc-composer {
1301
+ padding-top: 8px !important;
1302
+ }
1303
+
1304
+ .dc-stage .dc-dream-text textarea {
1305
+ background:
1306
+ linear-gradient(180deg, rgba(255, 250, 240, 0.96), rgba(255, 253, 248, 0.98)),
1307
+ repeating-linear-gradient(180deg, transparent 0 31px, var(--dqa-paper-line) 31px 32px) !important;
1308
+ border-color: rgba(95, 143, 104, 0.2) !important;
1309
+ border-radius: 8px !important;
1310
+ box-shadow:
1311
+ inset 0 1px 0 rgba(255, 255, 255, 0.78),
1312
+ var(--dqa-paper-shadow) !important;
1313
+ color: var(--dqa-pencil) !important;
1314
+ min-height: 350px !important;
1315
+ padding-left: 72px !important;
1316
+ }
1317
+
1318
+ .dc-stage .dc-dream-text textarea:focus {
1319
+ border-color: rgba(95, 143, 104, 0.54) !important;
1320
+ box-shadow:
1321
+ inset 0 1px 0 rgba(255, 255, 255, 0.78),
1322
+ 0 0 0 3px rgba(95, 143, 104, 0.14),
1323
+ var(--dqa-paper-shadow) !important;
1324
+ }
1325
+
1326
+ .dc-field-tip {
1327
+ background: #fff7df;
1328
+ border: 1px solid rgba(215, 168, 66, 0.34);
1329
+ border-radius: 8px;
1330
+ box-shadow: 3px 4px 0 rgba(111, 92, 48, 0.08);
1331
+ display: grid;
1332
+ gap: 5px;
1333
+ margin: 16px 0 0;
1334
+ padding: 13px 15px;
1335
+ }
1336
+
1337
+ .dc-field-tip span {
1338
+ color: #8c6733;
1339
+ font-size: 0.74rem;
1340
+ font-weight: 820;
1341
+ text-transform: uppercase;
1342
+ }
1343
+
1344
+ .dc-field-tip strong {
1345
+ color: var(--dqa-pencil);
1346
+ font-size: 1rem;
1347
+ line-height: 1.25;
1348
+ }
1349
+
1350
+ .dc-field-tip p {
1351
+ color: #6f746c;
1352
+ font-size: 0.9rem;
1353
+ line-height: 1.5;
1354
+ margin: 0;
1355
+ }
1356
+
1357
+ .dc-field-tip.is-error {
1358
+ display: block;
1359
+ }
1360
+
1361
+ .dc-demo-chip-intro {
1362
+ align-items: baseline;
1363
+ color: var(--dqa-muted);
1364
+ display: flex;
1365
+ flex-wrap: wrap;
1366
+ gap: 8px;
1367
+ margin: 18px 0 0;
1368
+ }
1369
+
1370
+ .dc-demo-chip-intro span {
1371
+ color: var(--dqa-sage-deep);
1372
+ font-size: 0.78rem;
1373
+ font-weight: 820;
1374
+ text-transform: uppercase;
1375
+ }
1376
+
1377
+ .dc-demo-chip-intro strong {
1378
+ color: #59675f;
1379
+ font-size: 0.92rem;
1380
+ font-weight: 650;
1381
+ }
1382
+
1383
+ .dc-demo-chip-row .dc-demo-chip button {
1384
+ background: var(--dqa-paper-warm) !important;
1385
+ border: 1px solid rgba(95, 143, 104, 0.22) !important;
1386
+ border-radius: 8px !important;
1387
+ box-shadow: 4px 4px 0 rgba(44, 55, 52, 0.08) !important;
1388
+ min-height: 58px !important;
1389
+ padding: 10px 12px !important;
1390
+ white-space: normal !important;
1391
+ }
1392
+
1393
+ .dc-demo-chip-row .dc-demo-chip button:hover,
1394
+ .dc-demo-chip-row .dc-demo-chip button:focus-visible {
1395
+ background: #eef7e9 !important;
1396
+ border-color: rgba(95, 143, 104, 0.45) !important;
1397
+ }
1398
+
1399
+ .dc-side-panel {
1400
+ background: rgba(255, 253, 248, 0.98) !important;
1401
+ gap: 14px;
1402
+ }
1403
+
1404
+ .dc-side-stack {
1405
+ display: grid;
1406
+ gap: 12px;
1407
+ }
1408
+
1409
+ .dc-side-stamp,
1410
+ .dc-desk-rule,
1411
+ .dc-intake-rail {
1412
+ border-radius: 8px;
1413
+ box-shadow: 3px 4px 0 rgba(44, 55, 52, 0.06);
1414
+ }
1415
+
1416
+ .dc-side-stamp {
1417
+ background: linear-gradient(180deg, #fff8e2, #fffdf8) !important;
1418
+ border-color: rgba(215, 168, 66, 0.32) !important;
1419
+ }
1420
+
1421
+ .dc-desk-rule {
1422
+ background: #eff7ee;
1423
+ border: 1px solid rgba(95, 143, 104, 0.24);
1424
+ color: var(--dqa-pencil);
1425
+ display: grid;
1426
+ gap: 6px;
1427
+ padding: 16px;
1428
+ }
1429
+
1430
+ .dc-desk-rule span,
1431
+ .dc-intake-rail small {
1432
+ color: var(--dqa-sage-deep);
1433
+ font-size: 0.74rem;
1434
+ font-weight: 820;
1435
+ text-transform: uppercase;
1436
+ }
1437
+
1438
+ .dc-desk-rule strong {
1439
+ color: var(--dqa-pencil);
1440
+ font-size: 1.06rem;
1441
+ line-height: 1.25;
1442
+ }
1443
+
1444
+ .dc-desk-rule p {
1445
+ color: #607169;
1446
+ font-size: 0.92rem;
1447
+ line-height: 1.5;
1448
+ margin: 0;
1449
+ }
1450
+
1451
+ .dc-intake-rail {
1452
+ background: #fffdf8;
1453
+ border: 1px solid rgba(217, 226, 220, 0.92);
1454
+ display: grid;
1455
+ gap: 10px;
1456
+ padding: 14px;
1457
+ }
1458
+
1459
+ .dc-intake-rail div {
1460
+ display: grid;
1461
+ gap: 8px;
1462
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1463
+ }
1464
+
1465
+ .dc-intake-rail span {
1466
+ background: #f4faf2;
1467
+ border: 1px solid rgba(95, 143, 104, 0.2);
1468
+ border-radius: 999px;
1469
+ color: var(--dqa-sage-deep);
1470
+ font-size: 0.78rem;
1471
+ font-weight: 760;
1472
+ padding: 7px 6px;
1473
+ text-align: center;
1474
+ }
1475
+
1476
+ .dc-question-card {
1477
+ background:
1478
+ linear-gradient(180deg, rgba(255, 250, 240, 0.98), rgba(255, 245, 222, 0.98)),
1479
+ repeating-linear-gradient(180deg, transparent 0 30px, rgba(215, 168, 66, 0.12) 30px 31px) !important;
1480
+ border-color: rgba(215, 168, 66, 0.36);
1481
+ border-radius: 8px;
1482
+ box-shadow: var(--dqa-paper-shadow);
1483
+ position: relative;
1484
+ }
1485
+
1486
+ .dc-question-card::before {
1487
+ background: rgba(95, 143, 104, 0.16);
1488
+ border: 1px solid rgba(95, 143, 104, 0.16);
1489
+ content: "";
1490
+ height: 28px;
1491
+ left: 50%;
1492
+ position: absolute;
1493
+ top: -14px;
1494
+ transform: translateX(-50%) rotate(-2deg);
1495
+ width: 104px;
1496
+ }
1497
+
1498
+ .dc-question-anchor-wrap {
1499
+ background: rgba(255, 253, 248, 0.62);
1500
+ border: 1px dashed rgba(95, 143, 104, 0.28);
1501
+ border-radius: 8px;
1502
+ margin: 16px 0 8px;
1503
+ padding: 12px;
1504
+ }
1505
+
1506
+ .dc-question-anchor-label {
1507
+ color: var(--dqa-sage-deep);
1508
+ display: block;
1509
+ font-size: 0.76rem;
1510
+ font-weight: 820;
1511
+ margin-bottom: 8px;
1512
+ text-transform: uppercase;
1513
+ }
1514
+
1515
+ .dc-question-anchor-strip {
1516
+ margin: 0;
1517
+ }
1518
+
1519
+ .dc-question-original {
1520
+ background: rgba(255, 253, 248, 0.7);
1521
+ border-left-color: var(--dqa-brass);
1522
+ border-radius: 0 8px 8px 0;
1523
+ padding: 11px 12px;
1524
+ }
1525
+
1526
+ .dqa-tip-page {
1527
+ border-radius: 8px;
1528
+ }
1529
+
1530
+ .dqa-tip-hero {
1531
+ border-radius: 8px 8px 0 0;
1532
+ display: grid;
1533
+ gap: 18px;
1534
+ grid-template-columns: minmax(0, 1fr) minmax(140px, 190px);
1535
+ position: relative;
1536
+ }
1537
+
1538
+ .dqa-ticket-stamp {
1539
+ align-self: start;
1540
+ background: rgba(255, 250, 240, 0.9);
1541
+ border: 2px solid rgba(95, 143, 104, 0.36);
1542
+ border-radius: 8px;
1543
+ box-shadow: 3px 4px 0 rgba(44, 55, 52, 0.08);
1544
+ color: var(--dqa-sage-deep);
1545
+ display: grid;
1546
+ gap: 6px;
1547
+ justify-items: center;
1548
+ padding: 13px 12px;
1549
+ text-align: center;
1550
+ transform: rotate(3deg);
1551
+ }
1552
+
1553
+ .dqa-ticket-stamp span {
1554
+ font-size: 0.72rem;
1555
+ font-weight: 820;
1556
+ text-transform: uppercase;
1557
+ }
1558
+
1559
+ .dqa-ticket-stamp strong {
1560
+ color: var(--dqa-pencil);
1561
+ font-size: 0.96rem;
1562
+ line-height: 1.2;
1563
+ }
1564
+
1565
+ .dqa-morning-ticket {
1566
+ border-radius: 8px;
1567
+ position: relative;
1568
+ }
1569
+
1570
+ .dqa-morning-ticket::after {
1571
+ background: linear-gradient(180deg, rgba(215, 168, 66, 0.18), rgba(95, 143, 104, 0.18));
1572
+ content: "";
1573
+ height: 100%;
1574
+ left: 0;
1575
+ position: absolute;
1576
+ top: 0;
1577
+ width: 6px;
1578
+ }
1579
+
1580
+ .dqa-ticket-row.is-primary-tip {
1581
+ background:
1582
+ linear-gradient(90deg, rgba(239, 247, 238, 0.96), rgba(255, 248, 226, 0.96)),
1583
+ repeating-linear-gradient(180deg, transparent 0 34px, rgba(95, 143, 104, 0.1) 34px 35px);
1584
+ }
1585
+
1586
+ .dqa-safety-note,
1587
+ .dqa-care-note,
1588
+ .dqa-qa-history,
1589
+ .dqa-small-model-note {
1590
+ border-radius: 8px;
1591
+ }
1592
+
1593
+ @media (max-width: 900px) {
1594
+ .dc-workspace-grid {
1595
+ margin-top: -14px;
1596
+ }
1597
+
1598
+ .dc-hero-ribbon {
1599
+ justify-self: stretch;
1600
+ }
1601
+
1602
+ .dc-stage .dc-dream-text textarea {
1603
+ min-height: 320px !important;
1604
+ }
1605
+
1606
+ .dqa-tip-hero {
1607
+ grid-template-columns: 1fr;
1608
+ }
1609
+
1610
+ .dqa-ticket-stamp {
1611
+ justify-self: start;
1612
+ transform: none;
1613
+ }
1614
+ }
1615
+
1616
+ @media (max-width: 640px) {
1617
+ .dc-hero {
1618
+ min-height: 430px;
1619
+ }
1620
+
1621
+ .dc-hero-ribbon {
1622
+ align-items: flex-start;
1623
+ display: grid;
1624
+ }
1625
+
1626
+ .dc-stage .dc-dream-text textarea {
1627
+ padding-left: 58px !important;
1628
+ }
1629
+
1630
+ .dc-intake-rail div {
1631
+ grid-template-columns: 1fr;
1632
+ }
1633
+
1634
+ .dc-question-card::before {
1635
+ left: 24px;
1636
+ transform: rotate(-2deg);
1637
+ }
1638
+ }
1639
+ """
1640
+
1641
  CSS += """
1642
  .dc-hero {
1643
  align-content: end;
tests/test_render.py CHANGED
@@ -99,6 +99,8 @@ def test_today_tip_card_prioritizes_tip_before_interpretation():
99
  assert "Morning Ticket" in html
100
  assert "Because your dream kept returning to" in html
101
  assert "dqa-anchor-chips" in html
 
 
102
  assert "floor 14" in html
103
  assert "How this was made small" in html
104
 
 
99
  assert "Morning Ticket" in html
100
  assert "Because your dream kept returning to" in html
101
  assert "dqa-anchor-chips" in html
102
+ assert "dqa-ticket-stamp" in html
103
+ assert "Take one note today" in html
104
  assert "floor 14" in html
105
  assert "How this was made small" in html
106
 
tests/test_ui_actions.py CHANGED
@@ -103,6 +103,8 @@ def test_hero_subtitle_explains_app_instead_of_legacy_name():
103
  assert "The Morning Question Desk" in hero_html
104
  assert "What did the dream leave you asking?" in hero_html
105
  assert "grounded Morning Ticket" in hero_html
 
 
106
  assert "Dream Customs" not in hero_html
107
 
108
 
@@ -115,6 +117,23 @@ def test_composer_has_three_real_demo_chips():
115
  assert "_example_floor14" in source
116
  assert "_example_melting" in source
117
  assert ".dc-demo-chip-row" in CSS
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
 
120
  def test_side_panel_dropdown_hover_stays_readable():
@@ -183,6 +202,8 @@ def test_question_stage_shows_anchor_chips_before_the_question():
183
  )
184
 
185
  assert "dc-question-anchor-strip" in html
 
 
186
  assert html.index("elevator") < html.index("Morning Question Desk")
187
  assert "floor 14" in html
188
  assert "melting button" in html
 
103
  assert "The Morning Question Desk" in hero_html
104
  assert "What did the dream leave you asking?" in hero_html
105
  assert "grounded Morning Ticket" in hero_html
106
+ assert "dc-hero-ribbon" in hero_html
107
+ assert "Text + image + voice intake" in hero_html
108
  assert "Dream Customs" not in hero_html
109
 
110
 
 
117
  assert "_example_floor14" in source
118
  assert "_example_melting" in source
119
  assert ".dc-demo-chip-row" in CSS
120
+ assert "Dream slips" in ui_app._demo_chip_intro_html("en")
121
+ assert "梦境纸片" in ui_app._demo_chip_intro_html("zh")
122
+
123
+
124
+ def test_side_panel_frames_the_morning_desk_rule():
125
+ html = ui_app._side_stamp_html("en")
126
+ zh_html = ui_app._side_stamp_html("zh")
127
+
128
+ assert "dc-side-stack" in html
129
+ assert "dc-desk-rule" in html
130
+ assert "Do not solve the whole dream." in html
131
+ assert "dc-intake-rail" in html
132
+ assert "Text" in html
133
+ assert "Image" in html
134
+ assert "Voice" in html
135
+ assert "清晨桌面规则" in zh_html
136
+ assert "文字" in zh_html
137
 
138
 
139
  def test_side_panel_dropdown_hover_stays_readable():
 
202
  )
203
 
204
  assert "dc-question-anchor-strip" in html
205
+ assert "dc-question-anchor-label" in html
206
+ assert "Sticky details already on the desk" in html
207
  assert html.index("elevator") < html.index("Morning Question Desk")
208
  assert "floor 14" in html
209
  assert "melting button" in html