fix dream qa composer debug ui

#26
CONTEXT.md ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dream QA Context
2
+
3
+ Dream QA is the current product context for this hackathon app. It keeps the deployed Dream Customs repository and Space continuity while defining the user-facing experience as guided dream Q&A.
4
+
5
+ ## Language
6
+
7
+ **Dream QA**:
8
+ A guided dream reflection app where a user records a dream, answers or skips a gentle follow-up question, and leaves with one grounded Today Tip.
9
+ _Avoid_: Dream Customs as the primary product name, dream permit, sealed pact
10
+
11
+ **Unified composer**:
12
+ The single input area where dream text, image clues, and voice dictation belong together as one dream intake.
13
+ _Avoid_: Separate upload module, separate voice module, three independent forms
14
+
15
+ **Today Tip**:
16
+ The one primary final suggestion or caring note for the user's day, grounded in concrete details from the dream.
17
+ _Avoid_: Multiple recommendations, generic wellness advice, diagnosis
18
+
19
+ **Developer debug panel**:
20
+ A collapsed support area for inspecting runtime state and backend route behavior without becoming part of the ordinary user flow.
21
+ _Avoid_: Main user setting, visible model control path
docs/smoke/2026-06-11-composer-debug-stepper-smoke.md ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dream QA Composer / Debug / Stepper Smoke - 2026-06-11
2
+
3
+ Goal: verify the Dream QA UI pass that moves image and voice entry into the main composer, makes the progress stepper state-driven, and adds a collapsed debug panel at the bottom of the page.
4
+
5
+ ## Local Runtime
6
+
7
+ - Command: `GRADIO_SERVER_NAME=127.0.0.1 GRADIO_SERVER_PORT=7862 .venv/bin/python app.py`
8
+ - URL: `http://127.0.0.1:7862`
9
+
10
+ ## Automated Checks
11
+
12
+ - `.venv/bin/python -m pytest tests/test_ui_actions.py -q` -> 13 passed.
13
+ - `.venv/bin/python -m pytest tests/test_app_logic.py::test_debug_settings_do_not_expose_hosted_secrets -q` -> 1 passed.
14
+ - `.venv/bin/python -m pytest -q` -> 103 passed.
15
+ - `.venv/bin/python scripts/evaluate_today_tip_quality.py` -> `{"case_count": 11, "failures": {}, "passes": true}`.
16
+ - `git diff --check` -> pass.
17
+
18
+ ## Chrome Smoke
19
+
20
+ - First screen shows one inline microphone button inside the Dream note composer.
21
+ - The previous visible `Voice note` Gradio module is hidden from the ordinary path.
22
+ - The image entry is a collapsed `+` drawer inside the composer.
23
+ - The bottom of the page shows a collapsed `Debug` panel.
24
+ - After submitting a text dream, the stepper changes from `1 Record` active to `2 Question` active with `1 Record` complete.
25
+ - After skipping the follow-up question, the stepper marks `1 Record`, `2 Question`, and `3 Interpret` complete, with `4 Today Tip` active.
26
+ - Expanding `Debug` shows runtime JSON with backend state and configured flags, without exposing endpoint or token values.
dream_customs/ui/app.py CHANGED
@@ -197,6 +197,7 @@ def _updates(state: str, view_json: str):
197
  return (
198
  state,
199
  view_json,
 
200
  _notice_html(view),
201
  _question_markdown(view, language),
202
  view.get("card_html", ""),
@@ -324,9 +325,23 @@ def _reset(language, text_backend, vision_backend, *settings_values):
324
  return (*_updates(state, view_json), "", "", None, None, default_mood_for(language))
325
 
326
 
327
- def _hero_html(language: str = DEFAULT_LANGUAGE) -> str:
 
 
 
 
328
  copy = copy_for(language)
329
  steps = copy["steps"]
 
 
 
 
 
 
 
 
 
 
330
  return f"""
331
  <header class="dc-hero">
332
  <div class="dc-hero-top">
@@ -340,10 +355,7 @@ def _hero_html(language: str = DEFAULT_LANGUAGE) -> str:
340
  <div class="dc-sun-mark" aria-hidden="true">☀</div>
341
  </div>
342
  <div class="dc-stepper" aria-label="Dream QA steps">
343
- <span class="is-active"><strong>1</strong>{steps[0]}</span>
344
- <span><strong>2</strong>{steps[1]}</span>
345
- <span><strong>3</strong>{steps[2]}</span>
346
- <span><strong>4</strong>{steps[3]}</span>
347
  </div>
348
  </header>
349
  """.strip()
@@ -370,10 +382,6 @@ def _mic_html(language: str = DEFAULT_LANGUAGE) -> str:
370
  """.strip()
371
 
372
 
373
- def _voice_help_html(language: str = DEFAULT_LANGUAGE) -> str:
374
- return f"<p class=\"dc-voice-help\">{escape(copy_for(language)['voice_help'])}</p>"
375
-
376
-
377
  def _field_tip_html(language: str = DEFAULT_LANGUAGE) -> str:
378
  return f"<p class=\"dc-field-tip\">{escape(copy_for(language)['field_tip'])}</p>"
379
 
@@ -402,6 +410,15 @@ def _dev_help_html(language: str = DEFAULT_LANGUAGE) -> str:
402
  """.strip()
403
 
404
 
 
 
 
 
 
 
 
 
 
405
  def build_demo() -> gr.Blocks:
406
  initial_state, initial_view = initial_mobile_state(language=DEFAULT_LANGUAGE)
407
  initial = _load_view(initial_view)
@@ -427,21 +444,26 @@ def build_demo() -> gr.Blocks:
427
  value="",
428
  elem_classes=["dc-dream-text"],
429
  )
430
- voice_help_html = gr.HTML(_voice_help_html(DEFAULT_LANGUAGE))
 
 
 
 
 
 
431
  audio_input = gr.Audio(
432
  label=initial_copy["voice_label"],
433
  sources=["microphone", "upload"],
434
  type="filepath",
435
  format="wav",
436
  elem_classes=["dc-voice-input"],
 
437
  )
438
  field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
439
  with gr.Row(elem_classes=["dc-submit-row"]):
440
  example_button = gr.Button(initial_copy["example_button"], variant="secondary")
441
  submit_button = gr.Button(initial_copy["submit_button"], variant="primary")
442
  processing_html = gr.HTML(_processing_html(DEFAULT_LANGUAGE))
443
- with gr.Accordion(initial_copy["image_accordion"], open=False, elem_classes=["dc-attachment-drawer"]):
444
- image_input = gr.Image(label=initial_copy["image_label"], type="filepath", height=160)
445
 
446
  with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
447
  question_markdown = gr.HTML(_question_markdown(initial, DEFAULT_LANGUAGE))
@@ -576,16 +598,18 @@ def build_demo() -> gr.Blocks:
576
  step=1,
577
  value=DEFAULT_VISION_MAX_TOKENS,
578
  )
579
- debug_json = gr.Code(
580
- label="Current state",
581
- value=json.dumps(initial.get("debug", {}), ensure_ascii=False, indent=2),
582
- language="json",
583
- visible=False,
584
- )
 
585
 
586
  outputs = [
587
  session_state,
588
  view_state,
 
589
  notice,
590
  question_markdown,
591
  card_html,
@@ -663,7 +687,9 @@ def build_demo() -> gr.Blocks:
663
  _notice_html({"notice": copy["notice_record"], "status": "record"}),
664
  _section_title_html(1, copy["dream_label"]),
665
  gr.update(label=copy["dream_label"], placeholder=copy["dream_placeholder"]),
666
- _voice_help_html(selected_language),
 
 
667
  _field_tip_html(selected_language),
668
  gr.update(value=copy["example_button"]),
669
  gr.update(value=copy["submit_button"]),
@@ -682,6 +708,9 @@ def build_demo() -> gr.Blocks:
682
  gr.update(label=copy["mood_label"], choices=moods, value=moods[0]),
683
  _side_stamp_html(selected_language),
684
  _dev_help_html(selected_language),
 
 
 
685
  )
686
 
687
  language.change(
@@ -692,7 +721,9 @@ def build_demo() -> gr.Blocks:
692
  notice,
693
  dream_section_html,
694
  dream_text,
695
- voice_help_html,
 
 
696
  field_tip_html,
697
  example_button,
698
  submit_button,
@@ -711,6 +742,9 @@ def build_demo() -> gr.Blocks:
711
  mood,
712
  side_stamp_html,
713
  dev_help_html,
 
 
 
714
  ],
715
  )
716
 
 
197
  return (
198
  state,
199
  view_json,
200
+ _hero_html(language, status),
201
  _notice_html(view),
202
  _question_markdown(view, language),
203
  view.get("card_html", ""),
 
325
  return (*_updates(state, view_json), "", "", None, None, default_mood_for(language))
326
 
327
 
328
+ def _active_step_for_status(status: str) -> int:
329
+ return {"record": 1, "error": 1, "ask": 2, "drafting": 3, "tip": 4}.get(status, 1)
330
+
331
+
332
+ def _hero_html(language: str = DEFAULT_LANGUAGE, status: str = "record") -> str:
333
  copy = copy_for(language)
334
  steps = copy["steps"]
335
+ active_step = _active_step_for_status(status)
336
+ step_html = []
337
+ for index, label in enumerate(steps, start=1):
338
+ classes = []
339
+ if index < active_step:
340
+ classes.append("is-complete")
341
+ if index == active_step:
342
+ classes.append("is-active")
343
+ class_attr = f' class="{" ".join(classes)}"' if classes else ""
344
+ step_html.append(f"<span{class_attr}><strong>{index}</strong>{label}</span>")
345
  return f"""
346
  <header class="dc-hero">
347
  <div class="dc-hero-top">
 
355
  <div class="dc-sun-mark" aria-hidden="true">☀</div>
356
  </div>
357
  <div class="dc-stepper" aria-label="Dream QA steps">
358
+ {''.join(step_html)}
 
 
 
359
  </div>
360
  </header>
361
  """.strip()
 
382
  """.strip()
383
 
384
 
 
 
 
 
385
  def _field_tip_html(language: str = DEFAULT_LANGUAGE) -> str:
386
  return f"<p class=\"dc-field-tip\">{escape(copy_for(language)['field_tip'])}</p>"
387
 
 
410
  """.strip()
411
 
412
 
413
+ def _debug_help_html(language: str = DEFAULT_LANGUAGE) -> str:
414
+ return f"""
415
+ <div class="dc-debug-help">
416
+ <strong>{escape(copy_for(language)['debug_title'])}</strong>
417
+ <span>{escape(copy_for(language)['debug_help'])}</span>
418
+ </div>
419
+ """.strip()
420
+
421
+
422
  def build_demo() -> gr.Blocks:
423
  initial_state, initial_view = initial_mobile_state(language=DEFAULT_LANGUAGE)
424
  initial = _load_view(initial_view)
 
444
  value="",
445
  elem_classes=["dc-dream-text"],
446
  )
447
+ mic_html = gr.HTML(_mic_html(DEFAULT_LANGUAGE))
448
+ with gr.Accordion(
449
+ initial_copy["image_accordion"],
450
+ open=False,
451
+ elem_classes=["dc-attachment-drawer"],
452
+ ) as image_drawer:
453
+ image_input = gr.Image(label=initial_copy["image_label"], type="filepath", height=160)
454
  audio_input = gr.Audio(
455
  label=initial_copy["voice_label"],
456
  sources=["microphone", "upload"],
457
  type="filepath",
458
  format="wav",
459
  elem_classes=["dc-voice-input"],
460
+ visible=False,
461
  )
462
  field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
463
  with gr.Row(elem_classes=["dc-submit-row"]):
464
  example_button = gr.Button(initial_copy["example_button"], variant="secondary")
465
  submit_button = gr.Button(initial_copy["submit_button"], variant="primary")
466
  processing_html = gr.HTML(_processing_html(DEFAULT_LANGUAGE))
 
 
467
 
468
  with gr.Group(visible=False, elem_classes=["dc-stage", "dc-question"]) as question_group:
469
  question_markdown = gr.HTML(_question_markdown(initial, DEFAULT_LANGUAGE))
 
598
  step=1,
599
  value=DEFAULT_VISION_MAX_TOKENS,
600
  )
601
+ with gr.Accordion(initial_copy["debug_title"], open=False, elem_classes=["dc-debug-panel"]) as debug_panel:
602
+ debug_help_html = gr.HTML(_debug_help_html(DEFAULT_LANGUAGE))
603
+ debug_json = gr.Code(
604
+ label=initial_copy["debug_state_label"],
605
+ value=json.dumps(initial.get("debug", {}), ensure_ascii=False, indent=2),
606
+ language="json",
607
+ )
608
 
609
  outputs = [
610
  session_state,
611
  view_state,
612
+ hero_html,
613
  notice,
614
  question_markdown,
615
  card_html,
 
687
  _notice_html({"notice": copy["notice_record"], "status": "record"}),
688
  _section_title_html(1, copy["dream_label"]),
689
  gr.update(label=copy["dream_label"], placeholder=copy["dream_placeholder"]),
690
+ _mic_html(selected_language),
691
+ gr.update(label=copy["image_accordion"]),
692
+ gr.update(label=copy["image_label"]),
693
  _field_tip_html(selected_language),
694
  gr.update(value=copy["example_button"]),
695
  gr.update(value=copy["submit_button"]),
 
708
  gr.update(label=copy["mood_label"], choices=moods, value=moods[0]),
709
  _side_stamp_html(selected_language),
710
  _dev_help_html(selected_language),
711
+ gr.update(label=copy["debug_title"]),
712
+ _debug_help_html(selected_language),
713
+ gr.update(label=copy["debug_state_label"]),
714
  )
715
 
716
  language.change(
 
721
  notice,
722
  dream_section_html,
723
  dream_text,
724
+ mic_html,
725
+ image_drawer,
726
+ image_input,
727
  field_tip_html,
728
  example_button,
729
  submit_button,
 
742
  mood,
743
  side_stamp_html,
744
  dev_help_html,
745
+ debug_panel,
746
+ debug_help_html,
747
+ debug_json,
748
  ],
749
  )
750
 
dream_customs/ui/copy.py CHANGED
@@ -31,8 +31,8 @@ APP_COPY = {
31
  "processing_note": (
32
  "After submission, Dream QA asks one grounded question, then turns the dream into a small Today Tip."
33
  ),
34
- "image_accordion": "Add image clue",
35
- "image_label": "Upload a sketch, note, or screenshot",
36
  "question_kicker": "Question",
37
  "question_title": "What do you most want to understand in this dream?",
38
  "question_body": "Answer in one or two lines, or skip and get a Today Tip from the clues already here.",
@@ -54,6 +54,9 @@ APP_COPY = {
54
  "side_stamp_body": "It helps the final suggestion stay grounded in your dream.",
55
  "language_label": "Language",
56
  "runtime_help": "Advanced controls for demos and development. Most visitors can leave these unchanged.",
 
 
 
57
  "card_title": "Today Tip",
58
  "error_state": "The interface state could not be read. Please start over.",
59
  },
@@ -82,8 +85,8 @@ APP_COPY = {
82
  "processing_note": (
83
  "提交后,梦境助手会先追问一个关键细节,再把梦境整理成一个今日小 Tips。"
84
  ),
85
- "image_accordion": "添加图片线索",
86
- "image_label": "上传草、便签或截图",
87
  "question_kicker": "追问",
88
  "question_title": "在这个梦里,你最想理解的是什么呢?",
89
  "question_body": "回答一两句就好;也可以跳过,直接得到一个基于现有线索的今日小 Tips。",
@@ -105,6 +108,9 @@ APP_COPY = {
105
  "side_stamp_body": "有助于更准确地理解梦境。",
106
  "language_label": "语言",
107
  "runtime_help": "高级演示与开发控制。普通体验保持默认即可。",
 
 
 
108
  "card_title": "今日小 Tips",
109
  "error_state": "界面状态读取失败,请重新开始。",
110
  },
 
31
  "processing_note": (
32
  "After submission, Dream QA asks one grounded question, then turns the dream into a small Today Tip."
33
  ),
34
+ "image_accordion": "",
35
+ "image_label": "Image clue",
36
  "question_kicker": "Question",
37
  "question_title": "What do you most want to understand in this dream?",
38
  "question_body": "Answer in one or two lines, or skip and get a Today Tip from the clues already here.",
 
54
  "side_stamp_body": "It helps the final suggestion stay grounded in your dream.",
55
  "language_label": "Language",
56
  "runtime_help": "Advanced controls for demos and development. Most visitors can leave these unchanged.",
57
+ "debug_title": "Debug",
58
+ "debug_help": "Inspect the current session, backend route, latency budgets, and Space/Modal status without exposing secrets.",
59
+ "debug_state_label": "Runtime state",
60
  "card_title": "Today Tip",
61
  "error_state": "The interface state could not be read. Please start over.",
62
  },
 
85
  "processing_note": (
86
  "提交后,梦境助手会先追问一个关键细节,再把梦境整理成一个今日小 Tips。"
87
  ),
88
+ "image_accordion": "",
89
+ "image_label": "图片线索",
90
  "question_kicker": "追问",
91
  "question_title": "在这个梦里,你最想理解的是什么呢?",
92
  "question_body": "回答一两句就好;也可以跳过,直接得到一个基于现有线索的今日小 Tips。",
 
108
  "side_stamp_body": "有助于更准确地理解梦境。",
109
  "language_label": "语言",
110
  "runtime_help": "高级演示与开发控制。普通体验保持默认即可。",
111
+ "debug_title": "调试",
112
+ "debug_help": "查看当前会话、后端路由、延迟预算和 Space/Modal 状态;不会暴露密钥。",
113
+ "debug_state_label": "运行状态",
114
  "card_title": "今日小 Tips",
115
  "error_state": "界面状态读取失败,请重新开始。",
116
  },
dream_customs/ui/styles.py CHANGED
@@ -394,15 +394,8 @@ body,
394
  display: none !important;
395
  }
396
 
397
- .dc-voice-help {
398
- color: var(--dc-muted);
399
- font-size: 0.86rem;
400
- line-height: 1.4;
401
- margin: 12px 0 6px;
402
- }
403
-
404
  .dc-voice-input {
405
- margin-top: 0 !important;
406
  }
407
 
408
  .dc-voice-input .wrap,
@@ -416,31 +409,31 @@ body,
416
 
417
  .dc-mic-control {
418
  align-items: end;
419
- bottom: 68px;
420
  display: grid;
421
  gap: 8px;
422
  justify-items: end;
423
  max-width: 280px;
424
  position: absolute;
425
- right: 34px;
426
  z-index: 8;
427
  }
428
 
429
  .dc-mic-button {
430
  align-items: center;
431
  background: rgba(255, 249, 238, 0.96) !important;
432
- border: 2px solid var(--dc-teal) !important;
433
  border-radius: 999px !important;
434
- box-shadow: 0 10px 22px rgba(7, 60, 67, 0.14);
435
  cursor: pointer;
436
  display: inline-flex;
437
- height: 64px !important;
438
  justify-content: center;
439
- min-height: 64px !important;
440
- min-width: 64px !important;
441
  padding: 0 !important;
442
  transition: background 160ms ease, transform 160ms ease, border-color 160ms ease;
443
- width: 64px !important;
444
  }
445
 
446
  .dc-mic-button:hover {
@@ -454,33 +447,33 @@ body,
454
  }
455
 
456
  .dc-mic-glyph {
457
- border: 3px solid var(--dc-teal);
458
  border-radius: 14px;
459
- height: 28px;
460
  position: relative;
461
- width: 16px;
462
  }
463
 
464
  .dc-mic-glyph::before {
465
- border: 3px solid var(--dc-teal);
466
  border-top: 0;
467
  border-radius: 0 0 18px 18px;
468
  content: "";
469
- height: 15px;
470
- left: -9px;
471
  position: absolute;
472
- top: 15px;
473
- width: 28px;
474
  }
475
 
476
  .dc-mic-glyph::after {
477
  background: var(--dc-teal);
478
- bottom: -19px;
479
  content: "";
480
- height: 13px;
481
- left: 5px;
482
  position: absolute;
483
- width: 3px;
484
  }
485
 
486
  .dc-mic-button[data-mode="listening"] .dc-mic-glyph,
@@ -583,10 +576,10 @@ body,
583
  .dc-stage .dc-mic-button {
584
  border-radius: 999px !important;
585
  font-family: "Avenir Next", "Gill Sans", sans-serif !important;
586
- height: 64px !important;
587
- min-height: 64px !important;
588
- min-width: 64px !important;
589
- width: 64px !important;
590
  }
591
 
592
  .dc-stage .primary button,
@@ -626,11 +619,43 @@ button.secondary {
626
  }
627
 
628
  .dc-attachment-drawer {
629
- background: rgba(230, 221, 208, 0.52) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
630
  border: 1px solid var(--dc-line) !important;
631
- border-radius: var(--dc-radius-sm) !important;
632
- box-shadow: inset 0 1px 2px rgba(19, 41, 47, 0.05) !important;
633
- margin-top: 18px;
 
634
  }
635
 
636
  .dc-row {
@@ -905,6 +930,41 @@ a.built-with[href*="gradio.app"] {
905
  margin-top: 10px;
906
  }
907
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
908
  @keyframes dc-mic-pulse {
909
  0% {
910
  box-shadow: 0 0 0 0 rgba(11, 91, 100, 0.32);
@@ -1013,22 +1073,28 @@ a.built-with[href*="gradio.app"] {
1013
 
1014
  .dc-dream-text textarea {
1015
  min-height: 300px !important;
1016
- padding-right: 20px !important;
1017
  padding-bottom: 96px !important;
1018
  }
1019
 
1020
  .dc-mic-control {
1021
- bottom: 72px;
1022
- left: 20px;
1023
- max-width: calc(100% - 40px);
1024
  right: 20px;
1025
  }
1026
 
1027
  .dc-mic-button {
1028
- height: 58px !important;
1029
- min-height: 58px !important;
1030
- min-width: 58px !important;
1031
- width: 58px !important;
 
 
 
 
 
 
1032
  }
1033
 
1034
  .dc-pass-topline {
@@ -1185,6 +1251,16 @@ body,
1185
  color: #fff;
1186
  }
1187
 
 
 
 
 
 
 
 
 
 
 
1188
  .dc-workspace-grid {
1189
  display: grid !important;
1190
  gap: 18px !important;
@@ -1493,8 +1569,13 @@ button.secondary {
1493
  }
1494
 
1495
  .dc-mic-control {
1496
- bottom: 78px;
1497
- right: 28px;
 
 
 
 
 
1498
  }
1499
 
1500
  .dqa-tip-page {
 
394
  display: none !important;
395
  }
396
 
 
 
 
 
 
 
 
397
  .dc-voice-input {
398
+ display: none !important;
399
  }
400
 
401
  .dc-voice-input .wrap,
 
409
 
410
  .dc-mic-control {
411
  align-items: end;
412
+ bottom: 28px;
413
  display: grid;
414
  gap: 8px;
415
  justify-items: end;
416
  max-width: 280px;
417
  position: absolute;
418
+ right: 28px;
419
  z-index: 8;
420
  }
421
 
422
  .dc-mic-button {
423
  align-items: center;
424
  background: rgba(255, 249, 238, 0.96) !important;
425
+ border: 1px solid rgba(7, 60, 67, 0.26) !important;
426
  border-radius: 999px !important;
427
+ box-shadow: 0 8px 18px rgba(7, 60, 67, 0.12);
428
  cursor: pointer;
429
  display: inline-flex;
430
+ height: 48px !important;
431
  justify-content: center;
432
+ min-height: 48px !important;
433
+ min-width: 48px !important;
434
  padding: 0 !important;
435
  transition: background 160ms ease, transform 160ms ease, border-color 160ms ease;
436
+ width: 48px !important;
437
  }
438
 
439
  .dc-mic-button:hover {
 
447
  }
448
 
449
  .dc-mic-glyph {
450
+ border: 2px solid var(--dc-teal);
451
  border-radius: 14px;
452
+ height: 22px;
453
  position: relative;
454
+ width: 13px;
455
  }
456
 
457
  .dc-mic-glyph::before {
458
+ border: 2px solid var(--dc-teal);
459
  border-top: 0;
460
  border-radius: 0 0 18px 18px;
461
  content: "";
462
+ height: 12px;
463
+ left: -7px;
464
  position: absolute;
465
+ top: 13px;
466
+ width: 23px;
467
  }
468
 
469
  .dc-mic-glyph::after {
470
  background: var(--dc-teal);
471
+ bottom: -15px;
472
  content: "";
473
+ height: 10px;
474
+ left: 4px;
475
  position: absolute;
476
+ width: 2px;
477
  }
478
 
479
  .dc-mic-button[data-mode="listening"] .dc-mic-glyph,
 
576
  .dc-stage .dc-mic-button {
577
  border-radius: 999px !important;
578
  font-family: "Avenir Next", "Gill Sans", sans-serif !important;
579
+ height: 48px !important;
580
+ min-height: 48px !important;
581
+ min-width: 48px !important;
582
+ width: 48px !important;
583
  }
584
 
585
  .dc-stage .primary button,
 
619
  }
620
 
621
  .dc-attachment-drawer {
622
+ background: transparent !important;
623
+ border: 0 !important;
624
+ bottom: 28px;
625
+ box-shadow: none !important;
626
+ left: 28px;
627
+ margin: 0;
628
+ max-width: min(420px, calc(100% - 96px));
629
+ padding: 0 !important;
630
+ position: absolute;
631
+ z-index: 9;
632
+ }
633
+
634
+ .dc-attachment-drawer > button {
635
+ align-items: center !important;
636
+ background: rgba(255, 249, 238, 0.96) !important;
637
+ border: 1px solid rgba(7, 60, 67, 0.26) !important;
638
+ border-radius: 999px !important;
639
+ box-shadow: 0 8px 18px rgba(7, 60, 67, 0.12);
640
+ color: var(--dc-teal-dark) !important;
641
+ display: inline-flex !important;
642
+ font-family: "Avenir Next", "Gill Sans", sans-serif !important;
643
+ font-size: 1.5rem !important;
644
+ height: 48px !important;
645
+ justify-content: center !important;
646
+ line-height: 1 !important;
647
+ min-height: 48px !important;
648
+ padding: 0 !important;
649
+ width: 48px !important;
650
+ }
651
+
652
+ .dc-attachment-drawer > div {
653
+ background: rgba(255, 253, 248, 0.98) !important;
654
  border: 1px solid var(--dc-line) !important;
655
+ border-radius: var(--dc-radius-md) !important;
656
+ box-shadow: var(--dc-soft-shadow);
657
+ margin-top: 8px;
658
+ padding: 12px !important;
659
  }
660
 
661
  .dc-row {
 
930
  margin-top: 10px;
931
  }
932
 
933
+ .dc-debug-panel {
934
+ background: rgba(255, 253, 248, 0.94) !important;
935
+ border: 1px solid var(--dqa-line, var(--dc-line)) !important;
936
+ border-radius: 12px !important;
937
+ box-shadow: var(--dqa-shadow, var(--dc-soft-shadow));
938
+ margin-top: 18px;
939
+ padding: 10px !important;
940
+ }
941
+
942
+ .dc-debug-panel > button {
943
+ color: var(--dc-ink) !important;
944
+ font-size: 0.92rem !important;
945
+ min-height: 46px !important;
946
+ }
947
+
948
+ .dc-debug-help {
949
+ color: var(--dc-muted);
950
+ display: grid;
951
+ gap: 4px;
952
+ font-size: 0.84rem;
953
+ line-height: 1.5;
954
+ margin-bottom: 10px;
955
+ }
956
+
957
+ .dc-debug-help strong {
958
+ color: var(--dc-ink);
959
+ }
960
+
961
+ .dc-debug-panel textarea,
962
+ .dc-debug-panel code,
963
+ .dc-debug-panel pre {
964
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important;
965
+ font-size: 0.82rem !important;
966
+ }
967
+
968
  @keyframes dc-mic-pulse {
969
  0% {
970
  box-shadow: 0 0 0 0 rgba(11, 91, 100, 0.32);
 
1073
 
1074
  .dc-dream-text textarea {
1075
  min-height: 300px !important;
1076
+ padding-right: 78px !important;
1077
  padding-bottom: 96px !important;
1078
  }
1079
 
1080
  .dc-mic-control {
1081
+ bottom: 22px;
1082
+ left: auto;
1083
+ max-width: calc(100% - 112px);
1084
  right: 20px;
1085
  }
1086
 
1087
  .dc-mic-button {
1088
+ height: 48px !important;
1089
+ min-height: 48px !important;
1090
+ min-width: 48px !important;
1091
+ width: 48px !important;
1092
+ }
1093
+
1094
+ .dc-attachment-drawer {
1095
+ bottom: 22px;
1096
+ left: 20px;
1097
+ max-width: calc(100% - 96px);
1098
  }
1099
 
1100
  .dc-pass-topline {
 
1251
  color: #fff;
1252
  }
1253
 
1254
+ .dc-stepper .is-complete {
1255
+ color: var(--dqa-sage-deep);
1256
+ }
1257
+
1258
+ .dc-stepper .is-complete strong {
1259
+ background: rgba(91, 138, 97, 0.18);
1260
+ border-color: rgba(91, 138, 97, 0.42);
1261
+ color: var(--dqa-sage-deep);
1262
+ }
1263
+
1264
  .dc-workspace-grid {
1265
  display: grid !important;
1266
  gap: 18px !important;
 
1569
  }
1570
 
1571
  .dc-mic-control {
1572
+ bottom: 22px;
1573
+ right: 20px;
1574
+ }
1575
+
1576
+ .dc-attachment-drawer {
1577
+ bottom: 22px;
1578
+ left: 20px;
1579
  }
1580
 
1581
  .dqa-tip-page {
tests/test_ui_actions.py CHANGED
@@ -20,18 +20,40 @@ def test_runtime_settings_are_collapsed_for_public_flow():
20
  source = inspect.getsource(ui_app.build_demo)
21
 
22
  assert 'gr.Accordion("Advanced", open=False' in source
 
23
 
24
 
25
- def test_voice_input_uses_gradio_audio_file_for_modal_asr():
26
  source = inspect.getsource(ui_app.build_demo)
27
 
28
  assert "audio_input = gr.Audio(" in source
29
  assert 'sources=["microphone", "upload"]' in source
30
  assert 'type="filepath"' in source
 
 
31
  assert "audio_input = gr.State(None)" not in source
32
  assert 'value=DEFAULT_ASR_BACKEND' in source
33
 
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def test_processing_note_is_story_copy_not_backend_jargon():
36
  lowered = PROCESSING_NOTE.lower()
37
 
 
20
  source = inspect.getsource(ui_app.build_demo)
21
 
22
  assert 'gr.Accordion("Advanced", open=False' in source
23
+ assert 'with gr.Accordion(initial_copy["debug_title"], open=False' in source
24
 
25
 
26
+ def test_voice_input_keeps_modal_asr_component_but_uses_inline_mic_button():
27
  source = inspect.getsource(ui_app.build_demo)
28
 
29
  assert "audio_input = gr.Audio(" in source
30
  assert 'sources=["microphone", "upload"]' in source
31
  assert 'type="filepath"' in source
32
+ assert "visible=False" in source
33
+ assert "mic_html = gr.HTML(_mic_html(DEFAULT_LANGUAGE))" in source
34
  assert "audio_input = gr.State(None)" not in source
35
  assert 'value=DEFAULT_ASR_BACKEND' in source
36
 
37
 
38
+ def test_image_upload_is_composer_plus_drawer():
39
+ source = inspect.getsource(ui_app.build_demo)
40
+
41
+ assert 'initial_copy["image_accordion"]' in source
42
+ assert "open=False" in source
43
+ assert 'elem_classes=["dc-attachment-drawer"]' in source
44
+ assert 'image_input = gr.Image(label=initial_copy["image_label"]' in source
45
+
46
+
47
+ def test_hero_stepper_tracks_app_status():
48
+ ask_html = ui_app._hero_html(status="ask")
49
+ tip_html = ui_app._hero_html(status="tip")
50
+
51
+ assert '<span class="is-complete"><strong>1</strong>' in ask_html
52
+ assert '<span class="is-active"><strong>2</strong>' in ask_html
53
+ assert '<span class="is-complete"><strong>3</strong>' in tip_html
54
+ assert '<span class="is-active"><strong>4</strong>' in tip_html
55
+
56
+
57
  def test_processing_note_is_story_copy_not_backend_jargon():
58
  lowered = PROCESSING_NOTE.lower()
59