Sync Dream QA composer debug UI

#27
README.md CHANGED
@@ -81,6 +81,22 @@ python app.py
81
 
82
  Open `http://127.0.0.1:7860`.
83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  ## Optional Ollama Models
85
 
86
  ```bash
 
81
 
82
  Open `http://127.0.0.1:7860`.
83
 
84
+ ## Local Space Mirror
85
+
86
+ For pre-merge UI review, run the same `app.py` entrypoint with the local Space mirror wrapper:
87
+
88
+ ```bash
89
+ .venv/bin/python scripts/local_space_mirror.py
90
+ ```
91
+
92
+ Open `http://127.0.0.1:7862`, then verify the Gradio config in another terminal:
93
+
94
+ ```bash
95
+ .venv/bin/python scripts/smoke_local_space_mirror.py
96
+ ```
97
+
98
+ Use this path before merging Hugging Face Space PRs so UI changes can be reviewed locally. Details: `docs/local-space-mirror.md`.
99
+
100
  ## Optional Ollama Models
101
 
102
  ```bash
docs/local-space-mirror.md ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Local Space Mirror
2
+
3
+ Use this when you want to review Dream QA changes before they are merged into the Hugging Face Space.
4
+
5
+ The mirror does not copy the app into a second implementation. It imports the same `app.py` entrypoint and therefore uses the same `dream_customs.ui.app.build_demo()` path as the Space. Missing Modal endpoint secrets fall back to deterministic demo behavior, just like the public app.
6
+
7
+ ## Start
8
+
9
+ ```bash
10
+ .venv/bin/python scripts/local_space_mirror.py
11
+ ```
12
+
13
+ Open:
14
+
15
+ ```text
16
+ http://127.0.0.1:7862
17
+ ```
18
+
19
+ The script prints a small manifest before starting Gradio:
20
+
21
+ - `app_file` should be `app.py`.
22
+ - default text, vision, and voice backends should be `modal`.
23
+ - endpoint/token fields are only reported as configured or not configured; values are not printed.
24
+
25
+ ## Verify
26
+
27
+ In another terminal:
28
+
29
+ ```bash
30
+ .venv/bin/python scripts/smoke_local_space_mirror.py
31
+ ```
32
+
33
+ The smoke checks `/config` for the current Dream QA app title, composer CSS markers, debug panel marker, and modal backend defaults.
34
+
35
+ ## Optional Runtime Secrets
36
+
37
+ If you want the local mirror to call the same private Modal routes as the configured Space, export these in your shell before starting the mirror:
38
+
39
+ ```bash
40
+ export DREAM_CUSTOMS_TEXT_ENDPOINT=...
41
+ export DREAM_CUSTOMS_VISION_ENDPOINT=...
42
+ export DREAM_CUSTOMS_ASR_ENDPOINT=...
43
+ export DREAM_CUSTOMS_HOSTED_TOKEN=...
44
+ ```
45
+
46
+ Do not commit these values, paste them into docs, or include them in screenshots. Without them, the app remains usable through the deterministic fallback route.
47
+
48
+ ## When To Use
49
+
50
+ Run this before creating or merging a Hugging Face Space PR whenever a change affects:
51
+
52
+ - Gradio UI layout or CSS.
53
+ - Stepper state.
54
+ - Image, voice, or text input behavior.
55
+ - Debug/runtime panels.
56
+ - Default backend behavior.
dream_customs/ui/app.py CHANGED
@@ -70,6 +70,8 @@ VOICE_JS = r"""
70
  }
71
  button.dataset.bound = "true";
72
 
 
 
73
  const setStatus = (message, mode) => {
74
  if (status) {
75
  status.textContent = message;
@@ -94,23 +96,15 @@ VOICE_JS = r"""
94
  button.addEventListener("click", async () => {
95
  const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
96
  if (!Recognition) {
97
- setStatus("This browser cannot transcribe voice here. You can still type the dream.", "error");
98
  textarea.focus();
99
  return;
100
  }
101
 
102
- try {
103
- if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
104
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
105
- stream.getTracks().forEach((track) => track.stop());
106
- }
107
- } catch (error) {
108
- setStatus("Microphone permission was not granted. Allow recording and try again.", "error");
109
- return;
110
- }
111
 
112
  const recognition = new Recognition();
113
- recognition.lang = "en-US";
114
  recognition.interimResults = true;
115
  recognition.continuous = false;
116
  recognition.maxAlternatives = 1;
@@ -118,7 +112,7 @@ VOICE_JS = r"""
118
  let latestTranscript = "";
119
 
120
  recognition.onstart = () => {
121
- setStatus("Listening. Say the dream fragment when you are ready.", "listening");
122
  };
123
 
124
  recognition.onresult = (event) => {
@@ -133,17 +127,17 @@ VOICE_JS = r"""
133
 
134
  recognition.onerror = (event) => {
135
  const message = event.error === "not-allowed"
136
- ? "Microphone permission was denied. Allow recording and try again."
137
- : "I did not catch that. Tap the microphone again if you want to retry.";
138
  setStatus(message, "error");
139
  };
140
 
141
  recognition.onend = () => {
142
  if (latestTranscript) {
143
  appendTranscript(latestTranscript);
144
- setStatus("Added to the dream note.", "done");
145
  } else if (button.dataset.mode === "listening") {
146
- setStatus("No speech detected. Tap again if you want to retry.", "idle");
147
  }
148
  };
149
 
@@ -151,8 +145,130 @@ VOICE_JS = r"""
151
  });
152
  };
153
 
154
- bindVoiceButton();
155
- const observer = new MutationObserver(bindVoiceButton);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  observer.observe(document.body, { childList: true, subtree: true });
157
  }
158
  """
@@ -335,13 +451,20 @@ def _hero_html(language: str = DEFAULT_LANGUAGE, status: str = "record") -> str:
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">
@@ -349,7 +472,7 @@ def _hero_html(language: str = DEFAULT_LANGUAGE, status: str = "record") -> str:
349
  <div class="dc-brand-lockup">
350
  <div>
351
  <h1>{copy['title']}</h1>
352
- <p class="dc-brand-subtitle">{copy['brand_subtitle']}</p>
353
  </div>
354
  </div>
355
  <div class="dc-sun-mark" aria-hidden="true">☀</div>
@@ -374,7 +497,18 @@ def _mic_html(language: str = DEFAULT_LANGUAGE) -> str:
374
  copy = copy_for(language)
375
  return f"""
376
  <div class="dc-mic-control">
377
- <button type="button" class="dc-mic-button" aria-label="{escape(copy['mic_idle'])}">
 
 
 
 
 
 
 
 
 
 
 
378
  <span class="dc-mic-glyph" aria-hidden="true"></span>
379
  </button>
380
  <div class="dc-mic-status" aria-live="polite">{escape(copy['mic_idle'])}</div>
@@ -382,6 +516,24 @@ def _mic_html(language: str = DEFAULT_LANGUAGE) -> str:
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
 
@@ -424,7 +576,7 @@ def build_demo() -> gr.Blocks:
424
  initial = _load_view(initial_view)
425
  initial_copy = copy_for(DEFAULT_LANGUAGE)
426
 
427
- with gr.Blocks(css=CSS, title=APP_TITLE) as demo:
428
  session_state = gr.State(initial_state)
429
  view_state = gr.State(initial_view)
430
 
@@ -445,21 +597,23 @@ def build_demo() -> gr.Blocks:
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")
@@ -491,6 +645,13 @@ def build_demo() -> gr.Blocks:
491
  show_copy_button=True,
492
  elem_classes=["dc-hidden-text"],
493
  )
 
 
 
 
 
 
 
494
 
495
  with gr.Column(elem_classes=["dc-side-panel"]):
496
  language = gr.Radio(
@@ -503,6 +664,27 @@ def build_demo() -> gr.Blocks:
503
  side_stamp_html = gr.HTML(_side_stamp_html(DEFAULT_LANGUAGE))
504
  with gr.Accordion("Advanced", open=False, elem_classes=["dc-dev"]):
505
  dev_help_html = gr.HTML(_dev_help_html(DEFAULT_LANGUAGE))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  text_backend = gr.Dropdown(
507
  label="Text generation",
508
  choices=[
@@ -570,41 +752,6 @@ def build_demo() -> gr.Blocks:
570
  value=DEFAULT_ASR_LATENCY_BUDGET_MS,
571
  precision=0,
572
  )
573
- text_temperature = gr.Slider(
574
- label="Text temperature",
575
- minimum=0,
576
- maximum=0.7,
577
- step=0.05,
578
- value=DEFAULT_TEXT_TEMPERATURE,
579
- )
580
- vision_temperature = gr.Slider(
581
- label="Image temperature",
582
- minimum=0,
583
- maximum=0.7,
584
- step=0.05,
585
- value=DEFAULT_VISION_TEMPERATURE,
586
- )
587
- text_max_tokens = gr.Slider(
588
- label="Text max tokens",
589
- minimum=64,
590
- maximum=1200,
591
- step=1,
592
- value=DEFAULT_TEXT_MAX_TOKENS,
593
- )
594
- vision_max_tokens = gr.Slider(
595
- label="Image max tokens",
596
- minimum=64,
597
- maximum=800,
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,
@@ -688,7 +835,7 @@ def build_demo() -> gr.Blocks:
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"]),
@@ -722,7 +869,7 @@ def build_demo() -> gr.Blocks:
722
  dream_section_html,
723
  dream_text,
724
  mic_html,
725
- image_drawer,
726
  image_input,
727
  field_tip_html,
728
  example_button,
 
70
  }
71
  button.dataset.bound = "true";
72
 
73
+ const messageFor = (key, fallback) => button.dataset[key] || fallback;
74
+
75
  const setStatus = (message, mode) => {
76
  if (status) {
77
  status.textContent = message;
 
96
  button.addEventListener("click", async () => {
97
  const Recognition = window.SpeechRecognition || window.webkitSpeechRecognition;
98
  if (!Recognition) {
99
+ setStatus(messageFor("unsupported", "This browser cannot transcribe voice here. You can still type the dream."), "error");
100
  textarea.focus();
101
  return;
102
  }
103
 
104
+ setStatus(messageFor("checking", "Checking microphone permission..."), "listening");
 
 
 
 
 
 
 
 
105
 
106
  const recognition = new Recognition();
107
+ recognition.lang = button.dataset.language === "zh" ? "zh-CN" : "en-US";
108
  recognition.interimResults = true;
109
  recognition.continuous = false;
110
  recognition.maxAlternatives = 1;
 
112
  let latestTranscript = "";
113
 
114
  recognition.onstart = () => {
115
+ setStatus(messageFor("listening", "Listening. Say the dream fragment when you are ready."), "listening");
116
  };
117
 
118
  recognition.onresult = (event) => {
 
127
 
128
  recognition.onerror = (event) => {
129
  const message = event.error === "not-allowed"
130
+ ? messageFor("permission", "Microphone permission was denied. Allow recording and try again.")
131
+ : messageFor("empty", "I did not catch that. Tap the microphone again if you want to retry.");
132
  setStatus(message, "error");
133
  };
134
 
135
  recognition.onend = () => {
136
  if (latestTranscript) {
137
  appendTranscript(latestTranscript);
138
+ setStatus(messageFor("done", "Added to the dream note."), "done");
139
  } else if (button.dataset.mode === "listening") {
140
+ setStatus(messageFor("empty", "No speech detected. Tap again if you want to retry."), "idle");
141
  }
142
  };
143
 
 
145
  });
146
  };
147
 
148
+ const bindAttachmentButton = () => {
149
+ const button = document.querySelector(".dc-attach-button");
150
+ const composer = button?.closest(".dc-composer");
151
+
152
+ if (!button || !composer || button.dataset.bound === "true") {
153
+ return;
154
+ }
155
+ button.dataset.bound = "true";
156
+
157
+ const localizeImagePopover = () => {
158
+ const control = composer.querySelector(".dc-attach-control");
159
+ const popover = composer.querySelector(".dc-image-popover");
160
+ if (!control || !popover) {
161
+ return;
162
+ }
163
+
164
+ const copy = {
165
+ imageLabel: control.dataset.imageLabel || "Image clue",
166
+ upload: control.dataset.upload || "Upload image",
167
+ paste: control.dataset.paste || "Paste from Clipboard",
168
+ };
169
+ composer.dataset.imageLanguage = control.dataset.language || "en";
170
+ const sourceButtons = popover.querySelectorAll('[data-testid="source-select"] button');
171
+ const clipboardSelected = sourceButtons.length > 1
172
+ && sourceButtons[sourceButtons.length - 1].classList.contains("selected");
173
+ const primaryCopy = clipboardSelected ? copy.paste : copy.upload;
174
+
175
+ const label = popover.querySelector('[data-testid="block-label"]');
176
+ if (label) {
177
+ let labelText = label.querySelector(".dc-image-label-copy");
178
+ if (!labelText) {
179
+ labelText = document.createElement("span");
180
+ labelText.className = "dc-image-label-copy";
181
+ label.appendChild(labelText);
182
+ }
183
+ Array.from(label.childNodes).forEach((node) => {
184
+ if (node.nodeType === Node.TEXT_NODE) {
185
+ node.textContent = "";
186
+ }
187
+ });
188
+ labelText.textContent = copy.imageLabel;
189
+ }
190
+
191
+ const uploadWrap = popover.querySelector(".upload-container button .wrap");
192
+ if (uploadWrap) {
193
+ Array.from(uploadWrap.childNodes).forEach((node) => {
194
+ if (node.nodeType === Node.TEXT_NODE) {
195
+ node.textContent = "";
196
+ }
197
+ });
198
+ let uploadText = uploadWrap.querySelector(".dc-image-upload-copy");
199
+ if (!uploadText) {
200
+ uploadText = document.createElement("strong");
201
+ uploadText.className = "dc-image-upload-copy";
202
+ uploadWrap.appendChild(uploadText);
203
+ }
204
+ uploadText.textContent = primaryCopy;
205
+ }
206
+
207
+ const uploadButton = popover.querySelector(".upload-container button");
208
+ uploadButton?.setAttribute("aria-label", primaryCopy);
209
+
210
+ if (sourceButtons[0]) {
211
+ sourceButtons[0].setAttribute("aria-label", copy.upload);
212
+ }
213
+ if (sourceButtons[sourceButtons.length - 1]) {
214
+ sourceButtons[sourceButtons.length - 1].setAttribute("aria-label", copy.paste);
215
+ }
216
+
217
+ const replaceText = (root) => {
218
+ Array.from(root.childNodes).forEach((node) => {
219
+ if (node.nodeType === Node.TEXT_NODE) {
220
+ const text = node.textContent || "";
221
+ if (text.includes("Paste from Clipboard") || text.includes("从剪贴板粘贴")) {
222
+ node.textContent = text.replace("Paste from Clipboard", copy.paste).replace("从剪贴板粘贴", copy.paste);
223
+ }
224
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
225
+ replaceText(node);
226
+ }
227
+ });
228
+ };
229
+ replaceText(popover);
230
+ };
231
+
232
+ const setOpen = (open) => {
233
+ localizeImagePopover();
234
+ composer.classList.toggle("dc-image-open", open);
235
+ button.setAttribute("aria-expanded", open ? "true" : "false");
236
+ };
237
+
238
+ button.addEventListener("click", (event) => {
239
+ event.preventDefault();
240
+ event.stopPropagation();
241
+ const shouldOpen = !composer.classList.contains("dc-image-open");
242
+ setOpen(shouldOpen);
243
+ if (shouldOpen) {
244
+ const uploadInput = composer.querySelector(".dc-image-popover input[type='file']");
245
+ uploadInput?.focus();
246
+ }
247
+ });
248
+
249
+ composer.addEventListener("click", localizeImagePopover);
250
+ localizeImagePopover();
251
+
252
+ document.addEventListener("click", (event) => {
253
+ if (!composer.contains(event.target)) {
254
+ setOpen(false);
255
+ }
256
+ });
257
+
258
+ document.addEventListener("keydown", (event) => {
259
+ if (event.key === "Escape") {
260
+ setOpen(false);
261
+ }
262
+ });
263
+ };
264
+
265
+ const bindComposerControls = () => {
266
+ bindVoiceButton();
267
+ bindAttachmentButton();
268
+ };
269
+
270
+ bindComposerControls();
271
+ const observer = new MutationObserver(bindComposerControls);
272
  observer.observe(document.body, { childList: true, subtree: true });
273
  }
274
  """
 
451
  active_step = _active_step_for_status(status)
452
  step_html = []
453
  for index, label in enumerate(steps, start=1):
454
+ classes = ["dc-step"]
455
  if index < active_step:
456
  classes.append("is-complete")
457
  if index == active_step:
458
  classes.append("is-active")
459
+ aria_current = ' aria-current="step"' if index == active_step else ""
460
+ step_html.append(
461
+ f'<span class="{" ".join(classes)}"{aria_current}><strong>{index}</strong>{label}</span>'
462
+ )
463
+ if index < len(steps):
464
+ line_classes = ["dc-stepper-line"]
465
+ if index < active_step:
466
+ line_classes.append("is-complete")
467
+ step_html.append(f'<i class="{" ".join(line_classes)}" aria-hidden="true"></i>')
468
  return f"""
469
  <header class="dc-hero">
470
  <div class="dc-hero-top">
 
472
  <div class="dc-brand-lockup">
473
  <div>
474
  <h1>{copy['title']}</h1>
475
+ <p class="dc-brand-subtitle">{copy['subtitle']}</p>
476
  </div>
477
  </div>
478
  <div class="dc-sun-mark" aria-hidden="true">☀</div>
 
497
  copy = copy_for(language)
498
  return f"""
499
  <div class="dc-mic-control">
500
+ <button
501
+ type="button"
502
+ class="dc-mic-button"
503
+ aria-label="{escape(copy['mic_idle'])}"
504
+ data-language="{escape(language)}"
505
+ data-checking="{escape('正在请求麦克风权限...' if language == 'zh' else 'Checking microphone permission...')}"
506
+ data-unsupported="{escape(copy['mic_unsupported'])}"
507
+ data-permission="{escape(copy['mic_permission'])}"
508
+ data-listening="{escape(copy['mic_listening'])}"
509
+ data-done="{escape(copy['mic_done'])}"
510
+ data-empty="{escape(copy['mic_empty'])}"
511
+ >
512
  <span class="dc-mic-glyph" aria-hidden="true"></span>
513
  </button>
514
  <div class="dc-mic-status" aria-live="polite">{escape(copy['mic_idle'])}</div>
 
516
  """.strip()
517
 
518
 
519
+ def _attachment_html(language: str = DEFAULT_LANGUAGE) -> str:
520
+ copy = copy_for(language)
521
+ label = escape(copy["image_accordion"])
522
+ return f"""
523
+ <div
524
+ class="dc-attach-control"
525
+ data-language="{escape(language)}"
526
+ data-image-label="{escape(copy['image_label'])}"
527
+ data-upload="{escape(copy['image_upload'])}"
528
+ data-paste="{escape(copy['image_paste'])}"
529
+ >
530
+ <button type="button" class="dc-attach-button" aria-label="{label}" aria-expanded="false">
531
+ <span aria-hidden="true">+</span>
532
+ </button>
533
+ </div>
534
+ """.strip()
535
+
536
+
537
  def _field_tip_html(language: str = DEFAULT_LANGUAGE) -> str:
538
  return f"<p class=\"dc-field-tip\">{escape(copy_for(language)['field_tip'])}</p>"
539
 
 
576
  initial = _load_view(initial_view)
577
  initial_copy = copy_for(DEFAULT_LANGUAGE)
578
 
579
+ with gr.Blocks(css=CSS, js=VOICE_JS, title=APP_TITLE) as demo:
580
  session_state = gr.State(initial_state)
581
  view_state = gr.State(initial_view)
582
 
 
597
  elem_classes=["dc-dream-text"],
598
  )
599
  mic_html = gr.HTML(_mic_html(DEFAULT_LANGUAGE))
600
+ attachment_html = gr.HTML(_attachment_html(DEFAULT_LANGUAGE))
601
+ image_input = gr.Image(
602
+ label=initial_copy["image_label"],
603
+ sources=["upload", "clipboard"],
604
+ type="filepath",
605
+ height=180,
606
+ elem_classes=["dc-image-popover"],
607
+ )
608
  audio_input = gr.Audio(
609
  label=initial_copy["voice_label"],
610
+ sources=["upload"],
611
  type="filepath",
612
  format="wav",
613
  elem_classes=["dc-voice-input"],
614
  visible=False,
615
  )
616
+ field_tip_html = gr.HTML(_field_tip_html(DEFAULT_LANGUAGE))
617
  with gr.Row(elem_classes=["dc-submit-row"]):
618
  example_button = gr.Button(initial_copy["example_button"], variant="secondary")
619
  submit_button = gr.Button(initial_copy["submit_button"], variant="primary")
 
645
  show_copy_button=True,
646
  elem_classes=["dc-hidden-text"],
647
  )
648
+ with gr.Accordion(initial_copy["debug_title"], open=False, elem_classes=["dc-debug-panel"]) as debug_panel:
649
+ debug_help_html = gr.HTML(_debug_help_html(DEFAULT_LANGUAGE))
650
+ debug_json = gr.Code(
651
+ label=initial_copy["debug_state_label"],
652
+ value=json.dumps(initial.get("debug", {}), ensure_ascii=False, indent=2),
653
+ language="json",
654
+ )
655
 
656
  with gr.Column(elem_classes=["dc-side-panel"]):
657
  language = gr.Radio(
 
664
  side_stamp_html = gr.HTML(_side_stamp_html(DEFAULT_LANGUAGE))
665
  with gr.Accordion("Advanced", open=False, elem_classes=["dc-dev"]):
666
  dev_help_html = gr.HTML(_dev_help_html(DEFAULT_LANGUAGE))
667
+ with gr.Group(elem_classes=["dc-dev-tuning"]):
668
+ text_temperature = gr.Number(
669
+ label="Text temperature",
670
+ value=DEFAULT_TEXT_TEMPERATURE,
671
+ precision=2,
672
+ )
673
+ vision_temperature = gr.Number(
674
+ label="Image temperature",
675
+ value=DEFAULT_VISION_TEMPERATURE,
676
+ precision=2,
677
+ )
678
+ text_max_tokens = gr.Number(
679
+ label="Text max tokens",
680
+ value=DEFAULT_TEXT_MAX_TOKENS,
681
+ precision=0,
682
+ )
683
+ vision_max_tokens = gr.Number(
684
+ label="Image max tokens",
685
+ value=DEFAULT_VISION_MAX_TOKENS,
686
+ precision=0,
687
+ )
688
  text_backend = gr.Dropdown(
689
  label="Text generation",
690
  choices=[
 
752
  value=DEFAULT_ASR_LATENCY_BUDGET_MS,
753
  precision=0,
754
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
755
 
756
  outputs = [
757
  session_state,
 
835
  _section_title_html(1, copy["dream_label"]),
836
  gr.update(label=copy["dream_label"], placeholder=copy["dream_placeholder"]),
837
  _mic_html(selected_language),
838
+ _attachment_html(selected_language),
839
  gr.update(label=copy["image_label"]),
840
  _field_tip_html(selected_language),
841
  gr.update(value=copy["example_button"]),
 
869
  dream_section_html,
870
  dream_text,
871
  mic_html,
872
+ attachment_html,
873
  image_input,
874
  field_tip_html,
875
  example_button,
dream_customs/ui/copy.py CHANGED
@@ -33,6 +33,8 @@ APP_COPY = {
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.",
@@ -87,6 +89,8 @@ APP_COPY = {
87
  ),
88
  "image_accordion": "+",
89
  "image_label": "图片线索",
 
 
90
  "question_kicker": "追问",
91
  "question_title": "在这个梦里,你最想理解的是什么呢?",
92
  "question_body": "回答一两句就好;也可以跳过,直接得到一个基于现有线索的今日小 Tips。",
 
33
  ),
34
  "image_accordion": "+",
35
  "image_label": "Image clue",
36
+ "image_upload": "Upload image",
37
+ "image_paste": "Paste from Clipboard",
38
  "question_kicker": "Question",
39
  "question_title": "What do you most want to understand in this dream?",
40
  "question_body": "Answer in one or two lines, or skip and get a Today Tip from the clues already here.",
 
89
  ),
90
  "image_accordion": "+",
91
  "image_label": "图片线索",
92
+ "image_upload": "上传图片",
93
+ "image_paste": "从剪贴板粘贴",
94
  "question_kicker": "追问",
95
  "question_title": "在这个梦里,你最想理解的是什么呢?",
96
  "question_body": "回答一两句就好;也可以跳过,直接得到一个基于现有线索的今日小 Tips。",
dream_customs/ui/styles.py CHANGED
@@ -262,7 +262,7 @@ body,
262
  }
263
 
264
  .dc-workspace-grid {
265
- align-items: stretch !important;
266
  display: grid !important;
267
  gap: 18px !important;
268
  grid-template-columns: minmax(0, 1fr) minmax(300px, 340px);
@@ -939,6 +939,11 @@ a.built-with[href*="gradio.app"] {
939
  padding: 10px !important;
940
  }
941
 
 
 
 
 
 
942
  .dc-debug-panel > button {
943
  color: var(--dc-ink) !important;
944
  font-size: 0.92rem !important;
@@ -1199,33 +1204,40 @@ body,
1199
  }
1200
 
1201
  .dc-stepper {
1202
- align-items: start;
1203
- display: grid;
1204
  gap: 0;
1205
- grid-template-columns: repeat(4, minmax(0, 1fr));
1206
  margin: clamp(28px, 5vw, 58px) auto 0;
1207
  max-width: 920px;
1208
  position: relative;
1209
  }
1210
 
1211
- .dc-stepper::before {
1212
  background: #9cadb8;
1213
- content: "";
 
 
1214
  height: 3px;
1215
- left: 12%;
1216
- position: absolute;
1217
- right: 12%;
1218
- top: 25px;
 
 
 
 
1219
  }
1220
 
1221
  .dc-stepper span {
1222
  color: #667989;
1223
  display: grid;
 
1224
  font-size: clamp(1rem, 2.3vw, 1.45rem);
1225
  gap: 10px;
1226
  justify-items: center;
 
1227
  position: relative;
1228
- z-index: 1;
1229
  }
1230
 
1231
  .dc-stepper strong {
@@ -1514,6 +1526,699 @@ button.secondary {
1514
  grid-template-columns: repeat(4, minmax(0, 1fr));
1515
  }
1516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
  @media (max-width: 900px) {
1518
  .dc-shell {
1519
  padding: 0;
@@ -1544,10 +2249,10 @@ button.secondary {
1544
  margin-top: 36px;
1545
  }
1546
 
1547
- .dc-stepper::before {
1548
- left: 10%;
1549
- right: 10%;
1550
- top: 22px;
1551
  }
1552
 
1553
  .dc-stepper strong {
@@ -1573,11 +2278,23 @@ button.secondary {
1573
  right: 20px;
1574
  }
1575
 
1576
- .dc-attachment-drawer {
1577
  bottom: 22px;
1578
  left: 20px;
1579
  }
1580
 
 
 
 
 
 
 
 
 
 
 
 
 
1581
  .dqa-tip-page {
1582
  border-radius: 0;
1583
  }
 
262
  }
263
 
264
  .dc-workspace-grid {
265
+ align-items: start !important;
266
  display: grid !important;
267
  gap: 18px !important;
268
  grid-template-columns: minmax(0, 1fr) minmax(300px, 340px);
 
939
  padding: 10px !important;
940
  }
941
 
942
+ .dc-flow-column .dc-debug-panel {
943
+ align-self: stretch;
944
+ width: 100%;
945
+ }
946
+
947
  .dc-debug-panel > button {
948
  color: var(--dc-ink) !important;
949
  font-size: 0.92rem !important;
 
1204
  }
1205
 
1206
  .dc-stepper {
1207
+ align-items: flex-start;
1208
+ display: flex;
1209
  gap: 0;
 
1210
  margin: clamp(28px, 5vw, 58px) auto 0;
1211
  max-width: 920px;
1212
  position: relative;
1213
  }
1214
 
1215
+ .dc-stepper-line {
1216
  background: #9cadb8;
1217
+ border-radius: 999px;
1218
+ display: block;
1219
+ flex: 1 1 70px;
1220
  height: 3px;
1221
+ margin-top: clamp(24px, 3.4vw, 35px);
1222
+ min-width: 34px;
1223
+ transition: background-color 180ms ease, box-shadow 180ms ease;
1224
+ }
1225
+
1226
+ .dc-stepper-line.is-complete {
1227
+ background: var(--dqa-sage);
1228
+ box-shadow: 0 0 0 1px rgba(79, 138, 88, 0.14);
1229
  }
1230
 
1231
  .dc-stepper span {
1232
  color: #667989;
1233
  display: grid;
1234
+ flex: 0 0 clamp(72px, 13vw, 128px);
1235
  font-size: clamp(1rem, 2.3vw, 1.45rem);
1236
  gap: 10px;
1237
  justify-items: center;
1238
+ min-width: 0;
1239
  position: relative;
1240
+ text-align: center;
1241
  }
1242
 
1243
  .dc-stepper strong {
 
1526
  grid-template-columns: repeat(4, minmax(0, 1fr));
1527
  }
1528
 
1529
+ .dc-stage .dc-composer {
1530
+ background: transparent !important;
1531
+ border: 0 !important;
1532
+ border-radius: 0 !important;
1533
+ box-shadow: none !important;
1534
+ min-height: 0 !important;
1535
+ overflow: visible !important;
1536
+ padding: 0 !important;
1537
+ position: relative;
1538
+ }
1539
+
1540
+ .dc-stage > .styler {
1541
+ background: transparent !important;
1542
+ }
1543
+
1544
+ .dc-stage .dc-composer .form {
1545
+ background: transparent !important;
1546
+ border: 0 !important;
1547
+ box-shadow: none !important;
1548
+ }
1549
+
1550
+ .dc-stage .dc-composer .dc-section-title {
1551
+ color: var(--dqa-ink) !important;
1552
+ margin: 0 0 14px;
1553
+ }
1554
+
1555
+ .dc-stage .dc-composer .dc-section-title strong {
1556
+ color: var(--dqa-ink) !important;
1557
+ }
1558
+
1559
+ .dc-side-panel .dc-section-title,
1560
+ .dc-side-panel .dc-section-title strong {
1561
+ color: var(--dqa-ink) !important;
1562
+ opacity: 1 !important;
1563
+ }
1564
+
1565
+ .dc-shell,
1566
+ .dc-stage,
1567
+ .dc-side-panel,
1568
+ .dc-dev,
1569
+ .dc-debug-panel {
1570
+ color-scheme: light;
1571
+ }
1572
+
1573
+ .dc-side-panel {
1574
+ background: rgba(255, 253, 248, 0.96) !important;
1575
+ border-color: var(--dqa-line) !important;
1576
+ color: var(--dqa-ink) !important;
1577
+ }
1578
+
1579
+ .dc-side-panel > .styler,
1580
+ .dc-side-panel .form,
1581
+ .dc-side-panel .block,
1582
+ .dc-side-panel .wrap,
1583
+ .dc-side-panel .container,
1584
+ .dc-side-panel .input-container {
1585
+ background: transparent !important;
1586
+ border-color: rgba(217, 226, 220, 0.88) !important;
1587
+ box-shadow: none !important;
1588
+ color: var(--dqa-ink) !important;
1589
+ }
1590
+
1591
+ .dc-side-panel .block:has(input[type="radio"]),
1592
+ .dc-side-panel .wrap:has(input[type="radio"]) {
1593
+ background: transparent !important;
1594
+ }
1595
+
1596
+ .dc-side-panel label,
1597
+ .dc-side-panel [data-testid="block-label"],
1598
+ .dc-side-panel [data-testid="block-info"] {
1599
+ color: var(--dqa-ink) !important;
1600
+ }
1601
+
1602
+ .dc-side-panel label:has(input[type="radio"]),
1603
+ .dc-side-panel .wrap label {
1604
+ background: rgba(255, 253, 248, 0.96) !important;
1605
+ border: 1px solid var(--dqa-line) !important;
1606
+ border-radius: 12px !important;
1607
+ color: var(--dqa-ink) !important;
1608
+ }
1609
+
1610
+ .dc-side-panel label:has(input[type="radio"]:checked) {
1611
+ background: var(--dqa-sage-soft) !important;
1612
+ border-color: rgba(79, 138, 88, 0.42) !important;
1613
+ color: var(--dqa-sage-deep) !important;
1614
+ }
1615
+
1616
+ .dc-side-panel input,
1617
+ .dc-side-panel select,
1618
+ .dc-side-panel button,
1619
+ .dc-side-panel .dropdown,
1620
+ .dc-side-panel .svelte-1gfkn6j,
1621
+ .dc-side-panel .svelte-1hfxrpf,
1622
+ .dc-side-panel .svelte-1b6s6s {
1623
+ color: var(--dqa-ink) !important;
1624
+ }
1625
+
1626
+ .dc-side-panel select,
1627
+ .dc-side-panel .wrap:has(select),
1628
+ .dc-side-panel .container:has(select),
1629
+ .dc-side-panel [role="listbox"],
1630
+ .dc-side-panel [aria-haspopup="listbox"] {
1631
+ background: rgba(255, 253, 248, 0.98) !important;
1632
+ border-color: var(--dqa-line) !important;
1633
+ border-radius: 12px !important;
1634
+ color: var(--dqa-ink) !important;
1635
+ }
1636
+
1637
+ .dc-side-panel svg,
1638
+ .dc-side-panel img {
1639
+ color: var(--dqa-muted) !important;
1640
+ opacity: 0.86;
1641
+ }
1642
+
1643
+ .dc-side-panel .dc-dev,
1644
+ .dc-side-panel .dc-dev > .label-wrap,
1645
+ .dc-side-panel .dc-dev > button,
1646
+ .dc-side-panel button[aria-expanded] {
1647
+ background: rgba(255, 253, 248, 0.96) !important;
1648
+ border-color: var(--dqa-line) !important;
1649
+ color: var(--dqa-muted) !important;
1650
+ }
1651
+
1652
+ .dc-side-panel .block:has(input[role="listbox"]) {
1653
+ background: rgba(255, 253, 248, 0.98) !important;
1654
+ border: 1px solid var(--dqa-line) !important;
1655
+ border-radius: 16px !important;
1656
+ box-shadow: none !important;
1657
+ padding: 20px 22px 18px !important;
1658
+ }
1659
+
1660
+ .dc-side-panel .form:has(input[role="listbox"]) {
1661
+ background: transparent !important;
1662
+ border: 0 !important;
1663
+ border-radius: 0 !important;
1664
+ box-shadow: none !important;
1665
+ padding: 0 !important;
1666
+ }
1667
+
1668
+ .dc-side-panel .block:has(input[role="listbox"]) .wrap,
1669
+ .dc-side-panel .block:has(input[role="listbox"]) .container,
1670
+ .dc-side-panel .block:has(input[role="listbox"]) .wrap-inner,
1671
+ .dc-side-panel .block:has(input[role="listbox"]) .secondary-wrap,
1672
+ .dc-side-panel .block:has(input[role="listbox"]) .input-container,
1673
+ .dc-side-panel .block:has(input[role="listbox"]) input[role="listbox"] {
1674
+ background: transparent !important;
1675
+ border: 0 !important;
1676
+ border-radius: 0 !important;
1677
+ box-shadow: none !important;
1678
+ }
1679
+
1680
+ .dc-side-panel .block:has(input[role="listbox"]) .wrap {
1681
+ min-height: 34px !important;
1682
+ }
1683
+
1684
+ .dc-side-panel .block:has(input[role="listbox"]) input[role="listbox"] {
1685
+ font-size: 1rem !important;
1686
+ min-height: 30px !important;
1687
+ }
1688
+
1689
+ .dc-side-panel .block:has(input[role="listbox"]) * {
1690
+ --input-border-width: 0px;
1691
+ --block-border-width: 0px;
1692
+ }
1693
+
1694
+ .dc-side-panel .block:has(input[role="listbox"]) .wrap,
1695
+ .dc-side-panel .block:has(input[role="listbox"]) .container,
1696
+ .dc-side-panel .block:has(input[role="listbox"]) .wrap-inner,
1697
+ .dc-side-panel .block:has(input[role="listbox"]) .secondary-wrap,
1698
+ .dc-side-panel .block:has(input[role="listbox"]) .input-container,
1699
+ .dc-side-panel .block:has(input[role="listbox"]) input[role="listbox"] {
1700
+ border: 0 !important;
1701
+ border-color: transparent !important;
1702
+ outline: 0 !important;
1703
+ }
1704
+
1705
+ .dc-side-panel .block:has(input[role="listbox"]) .icon-wrap {
1706
+ pointer-events: none !important;
1707
+ transition: color 160ms ease, transform 160ms ease;
1708
+ }
1709
+
1710
+ .dc-side-panel .block:has(input[role="listbox"]) .dropdown-arrow {
1711
+ color: var(--dqa-muted) !important;
1712
+ transition: color 160ms ease, transform 160ms ease;
1713
+ transform-origin: center;
1714
+ transform-box: fill-box;
1715
+ }
1716
+
1717
+ .dc-side-panel .block:has(> .container input[role="listbox"][aria-expanded="true"]) > .container .dropdown-arrow {
1718
+ color: var(--dqa-sage-deep) !important;
1719
+ transform: rotate(180deg) !important;
1720
+ }
1721
+
1722
+ .dc-side-panel .block:has(input[role="listbox"]:hover) .dropdown-arrow,
1723
+ .dc-side-panel .block:has(input[role="listbox"]:focus) .dropdown-arrow,
1724
+ .dc-side-panel .block:has(input[role="listbox"]) .icon-wrap:hover .dropdown-arrow {
1725
+ color: var(--dqa-sage-deep) !important;
1726
+ }
1727
+
1728
+ .dc-side-panel .dc-dev {
1729
+ background: rgba(255, 253, 248, 0.98) !important;
1730
+ border: 1px solid var(--dqa-line) !important;
1731
+ border-radius: 20px !important;
1732
+ box-shadow: none !important;
1733
+ color: var(--dqa-ink) !important;
1734
+ padding: 14px !important;
1735
+ }
1736
+
1737
+ .dc-side-panel .dc-dev > button {
1738
+ align-items: center !important;
1739
+ background: transparent !important;
1740
+ border: 0 !important;
1741
+ border-radius: 14px !important;
1742
+ box-shadow: none !important;
1743
+ color: var(--dqa-ink) !important;
1744
+ font-size: 1rem !important;
1745
+ font-weight: 680 !important;
1746
+ min-height: 46px !important;
1747
+ padding: 0 4px !important;
1748
+ }
1749
+
1750
+ .dc-side-panel .dc-dev > button:hover,
1751
+ .dc-side-panel .dc-dev > button[aria-expanded="true"] {
1752
+ background: var(--dqa-sage-soft) !important;
1753
+ color: var(--dqa-sage-deep) !important;
1754
+ }
1755
+
1756
+ .dc-side-panel .dc-dev-help {
1757
+ color: #60717a !important;
1758
+ display: grid !important;
1759
+ font-size: 0.92rem !important;
1760
+ gap: 8px !important;
1761
+ line-height: 1.5 !important;
1762
+ margin: 8px 0 14px !important;
1763
+ }
1764
+
1765
+ .dc-side-panel .dc-dev-help strong {
1766
+ color: var(--dqa-ink) !important;
1767
+ font-size: 0.94rem !important;
1768
+ font-weight: 760 !important;
1769
+ line-height: 1.42 !important;
1770
+ }
1771
+
1772
+ .dc-side-panel .dc-dev-help span {
1773
+ color: #60717a !important;
1774
+ opacity: 1 !important;
1775
+ }
1776
+
1777
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) {
1778
+ background: rgba(255, 253, 248, 0.98) !important;
1779
+ border: 1px solid var(--dqa-line) !important;
1780
+ border-radius: 14px !important;
1781
+ box-shadow: none !important;
1782
+ margin: 8px 0 !important;
1783
+ padding: 12px 14px !important;
1784
+ }
1785
+
1786
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) label,
1787
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) [data-testid="block-label"] {
1788
+ color: var(--dqa-ink) !important;
1789
+ font-size: 0.84rem !important;
1790
+ font-weight: 680 !important;
1791
+ margin-bottom: 8px !important;
1792
+ }
1793
+
1794
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) .wrap,
1795
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) .container,
1796
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) .wrap-inner,
1797
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) .secondary-wrap,
1798
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) .input-container,
1799
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) input[role="listbox"] {
1800
+ min-height: 32px !important;
1801
+ padding: 0 !important;
1802
+ }
1803
+
1804
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) input[role="listbox"] {
1805
+ color: var(--dqa-ink) !important;
1806
+ font-size: 0.94rem !important;
1807
+ font-weight: 560 !important;
1808
+ overflow: hidden !important;
1809
+ text-overflow: ellipsis !important;
1810
+ white-space: nowrap !important;
1811
+ width: 100% !important;
1812
+ }
1813
+
1814
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) .wrap-inner {
1815
+ align-items: center !important;
1816
+ display: flex !important;
1817
+ gap: 8px !important;
1818
+ }
1819
+
1820
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) .secondary-wrap {
1821
+ flex: 1 1 auto !important;
1822
+ max-width: calc(100% - 38px) !important;
1823
+ min-width: 0 !important;
1824
+ overflow: hidden !important;
1825
+ }
1826
+
1827
+ .dc-side-panel .dc-dev .block:has(input[role="listbox"]) .icon-wrap {
1828
+ flex: 0 0 28px !important;
1829
+ height: 28px !important;
1830
+ width: 28px !important;
1831
+ }
1832
+
1833
+ .dc-side-panel .dc-dev-tuning {
1834
+ background: rgba(255, 253, 248, 0.98) !important;
1835
+ border: 1px solid var(--dqa-line) !important;
1836
+ border-radius: 16px !important;
1837
+ box-shadow: none !important;
1838
+ display: grid !important;
1839
+ gap: 0 !important;
1840
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1841
+ margin: 6px 0 12px !important;
1842
+ overflow: hidden !important;
1843
+ padding: 6px !important;
1844
+ }
1845
+
1846
+ .dc-side-panel .dc-dev-tuning > .styler,
1847
+ .dc-side-panel .dc-dev-tuning .form {
1848
+ background: transparent !important;
1849
+ border: 0 !important;
1850
+ border-radius: 12px !important;
1851
+ box-shadow: none !important;
1852
+ display: grid !important;
1853
+ gap: 6px !important;
1854
+ grid-column: 1 / -1 !important;
1855
+ grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
1856
+ width: 100% !important;
1857
+ }
1858
+
1859
+ .dc-side-panel .dc-dev-tuning .block {
1860
+ background: rgba(255, 255, 255, 0.58) !important;
1861
+ border: 1px solid rgba(217, 226, 220, 0.72) !important;
1862
+ border-radius: 12px !important;
1863
+ box-shadow: none !important;
1864
+ margin: 0 !important;
1865
+ min-width: 0 !important;
1866
+ padding: 9px 10px !important;
1867
+ }
1868
+
1869
+ .dc-side-panel .dc-dev-tuning .wrap.hide {
1870
+ display: none !important;
1871
+ height: 0 !important;
1872
+ min-height: 0 !important;
1873
+ }
1874
+
1875
+ .dc-side-panel .dc-dev-tuning .container {
1876
+ min-height: 0 !important;
1877
+ padding: 0 !important;
1878
+ }
1879
+
1880
+ .dc-side-panel .dc-dev-tuning label,
1881
+ .dc-side-panel .dc-dev-tuning [data-testid="block-label"] {
1882
+ color: var(--dqa-muted) !important;
1883
+ font-size: 0.74rem !important;
1884
+ font-weight: 700 !important;
1885
+ line-height: 1.25 !important;
1886
+ margin-bottom: 0 !important;
1887
+ }
1888
+
1889
+ .dc-side-panel .dc-dev-tuning input {
1890
+ background: transparent !important;
1891
+ border: 0 !important;
1892
+ box-shadow: none !important;
1893
+ color: var(--dqa-ink) !important;
1894
+ font-size: 0.92rem !important;
1895
+ font-weight: 700 !important;
1896
+ min-height: 30px !important;
1897
+ padding: 0 !important;
1898
+ }
1899
+
1900
+ .dc-side-panel .dc-dev-advanced {
1901
+ background: rgba(255, 253, 248, 0.98) !important;
1902
+ border: 1px solid var(--dqa-line) !important;
1903
+ border-radius: 14px !important;
1904
+ box-shadow: none !important;
1905
+ margin-top: 12px !important;
1906
+ }
1907
+
1908
+ .dc-side-panel .dc-dev-advanced > button {
1909
+ color: var(--dqa-ink) !important;
1910
+ font-size: 0.94rem !important;
1911
+ font-weight: 650 !important;
1912
+ min-height: 42px !important;
1913
+ padding: 0 12px !important;
1914
+ }
1915
+
1916
+ .gradio-container [role="listbox"],
1917
+ .gradio-container .options,
1918
+ .gradio-container .dropdown-options {
1919
+ background: rgba(255, 253, 248, 0.99) !important;
1920
+ border: 1px solid var(--dqa-line) !important;
1921
+ border-radius: 16px !important;
1922
+ box-shadow: 0 16px 38px rgba(30, 42, 48, 0.12) !important;
1923
+ color: var(--dqa-ink) !important;
1924
+ overflow: hidden !important;
1925
+ }
1926
+
1927
+ .gradio-container [role="option"],
1928
+ .gradio-container .option,
1929
+ .gradio-container .item {
1930
+ background: transparent !important;
1931
+ color: var(--dqa-ink) !important;
1932
+ }
1933
+
1934
+ .gradio-container [role="option"]:hover,
1935
+ .gradio-container [role="option"][aria-selected="true"],
1936
+ .gradio-container [role="option"].selected,
1937
+ .gradio-container .option:hover,
1938
+ .gradio-container .option.selected,
1939
+ .gradio-container .item:hover,
1940
+ .gradio-container .item.selected {
1941
+ background: var(--dqa-sage-soft) !important;
1942
+ color: var(--dqa-ink) !important;
1943
+ }
1944
+
1945
+ .gradio-container [role="option"]:focus,
1946
+ .gradio-container .option:focus,
1947
+ .gradio-container .item:focus {
1948
+ background: rgba(233, 243, 231, 0.78) !important;
1949
+ color: var(--dqa-ink) !important;
1950
+ outline: 2px solid rgba(79, 138, 88, 0.34) !important;
1951
+ outline-offset: -2px;
1952
+ }
1953
+
1954
+ .dc-stage .dc-dream-text,
1955
+ .dc-stage .dc-dream-text .wrap,
1956
+ .dc-stage .dc-dream-text .container,
1957
+ .dc-stage .dc-dream-text .input-container,
1958
+ .dc-stage .dc-dream-text .form,
1959
+ .dc-stage .dc-dream-text > div {
1960
+ background: transparent !important;
1961
+ border: 0 !important;
1962
+ border-radius: 0 !important;
1963
+ box-shadow: none !important;
1964
+ margin: 0 !important;
1965
+ overflow: visible !important;
1966
+ padding: 0 !important;
1967
+ }
1968
+
1969
+ .dc-stage .dc-dream-text [data-testid="block-label"],
1970
+ .dc-stage .dc-dream-text [data-testid="block-info"],
1971
+ .dc-stage .dc-dream-text .block-label {
1972
+ display: none !important;
1973
+ }
1974
+
1975
+ .dc-stage .dc-dream-text textarea {
1976
+ background: rgba(255, 253, 248, 0.98) !important;
1977
+ border: 1px solid var(--dqa-line) !important;
1978
+ border-radius: 30px !important;
1979
+ box-shadow:
1980
+ inset 0 1px 0 rgba(255, 255, 255, 0.82),
1981
+ 0 18px 38px rgba(30, 42, 48, 0.1) !important;
1982
+ color: var(--dqa-ink) !important;
1983
+ min-height: 320px !important;
1984
+ overflow: hidden !important;
1985
+ padding: 28px 96px 82px 78px !important;
1986
+ resize: none !important;
1987
+ }
1988
+
1989
+ .dc-attach-control,
1990
+ .dc-mic-control {
1991
+ bottom: 24px !important;
1992
+ position: absolute;
1993
+ z-index: 18;
1994
+ }
1995
+
1996
+ .dc-attach-control {
1997
+ left: 24px;
1998
+ }
1999
+
2000
+ .dc-mic-control {
2001
+ display: block !important;
2002
+ height: 52px !important;
2003
+ right: 24px !important;
2004
+ width: 52px !important;
2005
+ }
2006
+
2007
+ .dc-stage .dc-attach-control .dc-attach-button,
2008
+ .dc-stage .dc-mic-control .dc-mic-button {
2009
+ align-items: center !important;
2010
+ background: rgba(255, 253, 248, 0.98) !important;
2011
+ border: 1px solid rgba(7, 60, 67, 0.22) !important;
2012
+ border-radius: 999px !important;
2013
+ box-sizing: border-box !important;
2014
+ box-shadow: 0 10px 22px rgba(7, 60, 67, 0.12) !important;
2015
+ color: var(--dc-teal-dark) !important;
2016
+ display: inline-flex !important;
2017
+ height: 52px !important;
2018
+ justify-content: center !important;
2019
+ line-height: 1 !important;
2020
+ max-height: 52px !important;
2021
+ min-height: 52px !important;
2022
+ padding: 0 !important;
2023
+ width: 52px !important;
2024
+ }
2025
+
2026
+ .dc-stage .dc-attach-control .dc-attach-button {
2027
+ cursor: pointer;
2028
+ font-size: 1.6rem !important;
2029
+ font-weight: 720 !important;
2030
+ opacity: 1 !important;
2031
+ }
2032
+
2033
+ .dc-stage .dc-attach-control .dc-attach-button span {
2034
+ color: var(--dc-teal-dark) !important;
2035
+ opacity: 1 !important;
2036
+ }
2037
+
2038
+ .dc-stage .dc-attach-control .dc-attach-button:hover,
2039
+ .dc-stage .dc-attach-control .dc-attach-button[aria-expanded="true"],
2040
+ .dc-stage .dc-mic-control .dc-mic-button:hover {
2041
+ border-color: rgba(234, 107, 44, 0.42) !important;
2042
+ color: var(--dc-coral-dark) !important;
2043
+ transform: translateY(-1px);
2044
+ }
2045
+
2046
+ .dc-stage .dc-mic-control .dc-mic-status {
2047
+ bottom: 62px;
2048
+ position: absolute;
2049
+ right: 0;
2050
+ white-space: nowrap;
2051
+ }
2052
+
2053
+ .dc-image-popover {
2054
+ bottom: 88px;
2055
+ background: rgba(255, 253, 248, 0.99) !important;
2056
+ border: 1px solid var(--dqa-line) !important;
2057
+ border-radius: 26px !important;
2058
+ border-style: solid !important;
2059
+ box-shadow: 0 18px 42px rgba(30, 42, 48, 0.16) !important;
2060
+ color: var(--dqa-ink) !important;
2061
+ height: 220px !important;
2062
+ left: 24px;
2063
+ margin: 0 !important;
2064
+ max-width: calc(100% - 48px);
2065
+ opacity: 0;
2066
+ overflow: hidden !important;
2067
+ pointer-events: none;
2068
+ position: absolute !important;
2069
+ transform: translateY(8px) scale(0.98);
2070
+ transition: opacity 160ms ease, transform 160ms ease, visibility 160ms ease;
2071
+ visibility: hidden;
2072
+ width: min(420px, calc(100% - 48px));
2073
+ z-index: 20;
2074
+ }
2075
+
2076
+ .dc-composer.dc-image-open .dc-image-popover {
2077
+ opacity: 1;
2078
+ pointer-events: auto;
2079
+ transform: translateY(0) scale(1);
2080
+ visibility: visible;
2081
+ }
2082
+
2083
+ .dc-image-popover .wrap,
2084
+ .dc-image-popover .container,
2085
+ .dc-image-popover .upload-container,
2086
+ .dc-image-popover .image-container,
2087
+ .dc-image-popover [data-testid="image"],
2088
+ .dc-image-popover [data-testid="file-upload"] {
2089
+ background: transparent !important;
2090
+ border: 0 !important;
2091
+ border-radius: 0 !important;
2092
+ box-shadow: none !important;
2093
+ }
2094
+
2095
+ .dc-image-popover [data-testid="block-label"] {
2096
+ align-items: center !important;
2097
+ background: rgba(238, 247, 236, 0.96) !important;
2098
+ border: 1px solid rgba(88, 142, 94, 0.2) !important;
2099
+ border-radius: 999px !important;
2100
+ box-shadow: none !important;
2101
+ color: var(--dqa-sage-deep) !important;
2102
+ display: inline-flex !important;
2103
+ font-family: inherit !important;
2104
+ font-size: 0.84rem !important;
2105
+ font-weight: 720 !important;
2106
+ gap: 7px !important;
2107
+ left: 16px !important;
2108
+ line-height: 1 !important;
2109
+ padding: 8px 12px !important;
2110
+ top: 14px !important;
2111
+ }
2112
+
2113
+ .dc-image-popover [data-testid="block-label"] svg {
2114
+ color: var(--dqa-sage-deep) !important;
2115
+ height: 16px !important;
2116
+ width: 16px !important;
2117
+ }
2118
+
2119
+ .dc-image-popover .image-container {
2120
+ height: 100% !important;
2121
+ overflow: hidden !important;
2122
+ }
2123
+
2124
+ .dc-image-popover .upload-container {
2125
+ height: 100% !important;
2126
+ padding: 18px 18px 64px !important;
2127
+ }
2128
+
2129
+ .dc-image-popover .upload-container button {
2130
+ align-items: center !important;
2131
+ background: transparent !important;
2132
+ border: 0 !important;
2133
+ border-radius: 22px !important;
2134
+ box-shadow: none !important;
2135
+ color: var(--dqa-ink) !important;
2136
+ display: flex !important;
2137
+ justify-content: center !important;
2138
+ min-height: 130px !important;
2139
+ width: 100% !important;
2140
+ }
2141
+
2142
+ .dc-image-popover .upload-container button:hover {
2143
+ background: rgba(238, 247, 236, 0.56) !important;
2144
+ }
2145
+
2146
+ .dc-image-popover .upload-container .wrap {
2147
+ align-items: center !important;
2148
+ color: var(--dqa-ink) !important;
2149
+ display: flex !important;
2150
+ flex-direction: column !important;
2151
+ font-size: 0 !important;
2152
+ gap: 12px !important;
2153
+ justify-content: center !important;
2154
+ }
2155
+
2156
+ .dc-image-popover .upload-container .icon-wrap,
2157
+ .dc-image-popover .upload-container svg {
2158
+ color: var(--dqa-sage-deep) !important;
2159
+ height: 32px !important;
2160
+ width: 32px !important;
2161
+ }
2162
+
2163
+ .dc-image-popover .upload-container .or {
2164
+ display: none !important;
2165
+ }
2166
+
2167
+ .dc-image-upload-copy {
2168
+ color: var(--dqa-ink) !important;
2169
+ display: block !important;
2170
+ font-size: 1rem !important;
2171
+ font-weight: 760 !important;
2172
+ letter-spacing: 0 !important;
2173
+ }
2174
+
2175
+ .dc-image-popover [data-testid="source-select"] {
2176
+ align-items: center !important;
2177
+ background: transparent !important;
2178
+ border: 0 !important;
2179
+ border-top: 0 !important;
2180
+ bottom: 16px !important;
2181
+ box-shadow: none !important;
2182
+ display: flex !important;
2183
+ gap: 12px !important;
2184
+ justify-content: center !important;
2185
+ left: 0 !important;
2186
+ padding: 0 !important;
2187
+ position: absolute !important;
2188
+ right: 0 !important;
2189
+ }
2190
+
2191
+ .dc-image-popover [data-testid="source-select"]::before,
2192
+ .dc-image-popover [data-testid="source-select"]::after {
2193
+ display: none !important;
2194
+ }
2195
+
2196
+ .dc-image-popover [data-testid="source-select"] button {
2197
+ align-items: center !important;
2198
+ background: rgba(255, 253, 248, 0.92) !important;
2199
+ border: 1px solid var(--dqa-line) !important;
2200
+ border-radius: 999px !important;
2201
+ box-shadow: none !important;
2202
+ color: var(--dqa-muted) !important;
2203
+ display: inline-flex !important;
2204
+ height: 42px !important;
2205
+ justify-content: center !important;
2206
+ min-height: 42px !important;
2207
+ padding: 0 !important;
2208
+ width: 42px !important;
2209
+ }
2210
+
2211
+ .dc-image-popover [data-testid="source-select"] button:hover,
2212
+ .dc-image-popover [data-testid="source-select"] button.selected {
2213
+ background: var(--dqa-sage-soft) !important;
2214
+ border-color: rgba(88, 142, 94, 0.26) !important;
2215
+ color: var(--dqa-sage-deep) !important;
2216
+ }
2217
+
2218
+ .dc-image-popover [aria-label="Capture from camera"] {
2219
+ display: none !important;
2220
+ }
2221
+
2222
  @media (max-width: 900px) {
2223
  .dc-shell {
2224
  padding: 0;
 
2249
  margin-top: 36px;
2250
  }
2251
 
2252
+ .dc-stepper-line {
2253
+ flex-basis: 26px;
2254
+ margin-top: 24px;
2255
+ min-width: 20px;
2256
  }
2257
 
2258
  .dc-stepper strong {
 
2278
  right: 20px;
2279
  }
2280
 
2281
+ .dc-attach-control {
2282
  bottom: 22px;
2283
  left: 20px;
2284
  }
2285
 
2286
+ .dc-stage .dc-dream-text textarea {
2287
+ border-radius: 24px !important;
2288
+ min-height: 300px !important;
2289
+ padding: 24px 78px 78px 70px !important;
2290
+ }
2291
+
2292
+ .dc-image-popover {
2293
+ bottom: 84px;
2294
+ left: 20px;
2295
+ width: min(360px, calc(100% - 40px));
2296
+ }
2297
+
2298
  .dqa-tip-page {
2299
  border-radius: 0;
2300
  }
modal_backend/dream_customs_modal.py CHANGED
@@ -35,7 +35,7 @@ health_image = (
35
 
36
  image = (
37
  modal.Image.debian_slim(python_version="3.11")
38
- .apt_install("git")
39
  .pip_install(
40
  "accelerate",
41
  "einops",
 
35
 
36
  image = (
37
  modal.Image.debian_slim(python_version="3.11")
38
+ .apt_install("ffmpeg", "git")
39
  .pip_install(
40
  "accelerate",
41
  "einops",
scripts/local_space_mirror.py ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Run the Dream QA Hugging Face Space app locally for pre-merge review."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import sys
10
+ from pathlib import Path
11
+
12
+
13
+ ROOT = Path(__file__).resolve().parents[1]
14
+ SPACE_ID = "build-small-hackathon/dream-customs"
15
+ DEFAULT_RUNTIME_ENV_JSON = Path("/tmp/dream-customs-runtime.json")
16
+ RUNTIME_ENV_KEYS = {
17
+ "DREAM_CUSTOMS_TEXT_ENDPOINT",
18
+ "DREAM_CUSTOMS_VISION_ENDPOINT",
19
+ "DREAM_CUSTOMS_ASR_ENDPOINT",
20
+ "DREAM_CUSTOMS_HOSTED_TOKEN",
21
+ }
22
+
23
+
24
+ def _repo_root_on_path() -> None:
25
+ root = str(ROOT)
26
+ if root not in sys.path:
27
+ sys.path.insert(0, root)
28
+
29
+
30
+ def _configured_env() -> dict:
31
+ return {
32
+ "space_id": os.getenv("SPACE_ID", SPACE_ID),
33
+ "text_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_TEXT_ENDPOINT", "").strip()),
34
+ "vision_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_VISION_ENDPOINT", "").strip()),
35
+ "asr_endpoint_configured": bool(os.getenv("DREAM_CUSTOMS_ASR_ENDPOINT", "").strip()),
36
+ "hosted_token_configured": bool(os.getenv("DREAM_CUSTOMS_HOSTED_TOKEN", "").strip()),
37
+ }
38
+
39
+
40
+ def load_runtime_env_json(path: Path) -> dict:
41
+ if not path.exists():
42
+ return {"loaded": False, "path": str(path), "reason": "missing"}
43
+ try:
44
+ data = json.loads(path.read_text(encoding="utf-8"))
45
+ except (OSError, json.JSONDecodeError) as exc:
46
+ return {"loaded": False, "path": str(path), "reason": exc.__class__.__name__}
47
+
48
+ loaded_keys = []
49
+ for key in sorted(RUNTIME_ENV_KEYS):
50
+ value = str(data.get(key, "")).strip()
51
+ if value:
52
+ os.environ[key] = value
53
+ loaded_keys.append(key)
54
+ return {
55
+ "loaded": bool(loaded_keys),
56
+ "path": str(path),
57
+ "configured_keys": loaded_keys,
58
+ }
59
+
60
+
61
+ def mirror_manifest(host: str, port: int) -> dict:
62
+ _repo_root_on_path()
63
+ from dream_customs.defaults import DEFAULT_ASR_BACKEND, DEFAULT_TEXT_BACKEND, DEFAULT_VISION_BACKEND
64
+
65
+ return {
66
+ "mode": "local-space-mirror",
67
+ "space_id": SPACE_ID,
68
+ "url": f"http://{host}:{port}",
69
+ "app_file": "app.py",
70
+ "default_backends": {
71
+ "text": DEFAULT_TEXT_BACKEND,
72
+ "vision": DEFAULT_VISION_BACKEND,
73
+ "asr": DEFAULT_ASR_BACKEND,
74
+ },
75
+ "env": _configured_env(),
76
+ "notes": [
77
+ "Uses the same app.py/build_demo path as the Hugging Face Space.",
78
+ "Missing hosted endpoint secrets fall back to deterministic demo behavior.",
79
+ "Do not print or store endpoint URLs or tokens in logs.",
80
+ ],
81
+ }
82
+
83
+
84
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
85
+ parser = argparse.ArgumentParser(description=__doc__)
86
+ parser.add_argument("--host", default=os.getenv("GRADIO_SERVER_NAME", "127.0.0.1"))
87
+ parser.add_argument("--port", type=int, default=int(os.getenv("GRADIO_SERVER_PORT", "7862")))
88
+ parser.add_argument("--share", action="store_true", help="Create a public Gradio share link.")
89
+ parser.add_argument(
90
+ "--manifest-only",
91
+ action="store_true",
92
+ help="Print the local mirror manifest without starting Gradio.",
93
+ )
94
+ parser.add_argument(
95
+ "--runtime-env-json",
96
+ type=Path,
97
+ default=Path(os.getenv("DREAM_CUSTOMS_RUNTIME_ENV_JSON", DEFAULT_RUNTIME_ENV_JSON)),
98
+ help=(
99
+ "Load hosted endpoint/token secrets from a local JSON file before launching. "
100
+ "Only configuration booleans are printed."
101
+ ),
102
+ )
103
+ parser.add_argument(
104
+ "--no-runtime-env-json",
105
+ action="store_true",
106
+ help="Do not auto-load the default local runtime env JSON.",
107
+ )
108
+ return parser.parse_args(argv)
109
+
110
+
111
+ def main(argv: list[str] | None = None) -> int:
112
+ args = parse_args(argv)
113
+ os.environ["GRADIO_SERVER_NAME"] = args.host
114
+ os.environ["GRADIO_SERVER_PORT"] = str(args.port)
115
+ os.environ.setdefault("SPACE_ID", SPACE_ID)
116
+ os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
117
+
118
+ runtime_env = {"loaded": False, "reason": "disabled"}
119
+ if not args.no_runtime_env_json:
120
+ runtime_env = load_runtime_env_json(args.runtime_env_json)
121
+
122
+ manifest = mirror_manifest(args.host, args.port)
123
+ manifest["runtime_env_json"] = runtime_env
124
+ print(json.dumps(manifest, ensure_ascii=False, indent=2), flush=True)
125
+ if args.manifest_only:
126
+ return 0
127
+
128
+ _repo_root_on_path()
129
+ from app import demo
130
+
131
+ demo.launch(
132
+ server_name=args.host,
133
+ server_port=args.port,
134
+ show_api=False,
135
+ show_error=True,
136
+ share=args.share,
137
+ )
138
+ return 0
139
+
140
+
141
+ if __name__ == "__main__":
142
+ raise SystemExit(main())
scripts/smoke_local_space_mirror.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Verify a running local Dream QA Space mirror."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ from typing import Any
9
+ from urllib.request import urlopen
10
+
11
+
12
+ REQUIRED_CSS_MARKERS = [
13
+ ".dc-stepper",
14
+ ".dc-mic-button",
15
+ ".dc-attach-button",
16
+ ".dc-image-popover",
17
+ ".dc-debug-panel",
18
+ ]
19
+
20
+ REQUIRED_LABELS = [
21
+ "Dream note",
22
+ "Language",
23
+ "Text generation",
24
+ "Image understanding",
25
+ "Voice input",
26
+ "Runtime state",
27
+ ]
28
+
29
+
30
+ def _component_labels(config: dict[str, Any]) -> set[str]:
31
+ labels = set()
32
+ for component in config.get("components", []):
33
+ props = component.get("props") or {}
34
+ label = props.get("label")
35
+ if isinstance(label, str):
36
+ labels.add(label)
37
+ return labels
38
+
39
+
40
+ def _component_values(config: dict[str, Any]) -> dict[str, Any]:
41
+ values = {}
42
+ for component in config.get("components", []):
43
+ props = component.get("props") or {}
44
+ label = props.get("label")
45
+ if isinstance(label, str):
46
+ values[label] = props.get("value")
47
+ return values
48
+
49
+
50
+ def inspect_config(config: dict[str, Any]) -> dict[str, Any]:
51
+ css = config.get("css") or ""
52
+ labels = _component_labels(config)
53
+ values = _component_values(config)
54
+ missing_css = [marker for marker in REQUIRED_CSS_MARKERS if marker not in css]
55
+ missing_labels = [label for label in REQUIRED_LABELS if label not in labels]
56
+ backend_defaults = {
57
+ "Text generation": values.get("Text generation"),
58
+ "Image understanding": values.get("Image understanding"),
59
+ "Voice input": values.get("Voice input"),
60
+ }
61
+ backend_failures = {
62
+ label: value for label, value in backend_defaults.items() if value != "modal"
63
+ }
64
+ title = config.get("title")
65
+ failures = {}
66
+ if title != "Dream QA":
67
+ failures["title"] = title
68
+ if missing_css:
69
+ failures["missing_css"] = missing_css
70
+ if missing_labels:
71
+ failures["missing_labels"] = missing_labels
72
+ if backend_failures:
73
+ failures["backend_defaults"] = backend_failures
74
+ return {
75
+ "title": title,
76
+ "component_count": len(config.get("components", [])),
77
+ "backend_defaults": backend_defaults,
78
+ "missing_css": missing_css,
79
+ "missing_labels": missing_labels,
80
+ "passes": not failures,
81
+ "failures": failures,
82
+ }
83
+
84
+
85
+ def fetch_config(base_url: str, timeout: float = 20.0) -> dict[str, Any]:
86
+ with urlopen(f"{base_url.rstrip('/')}/config", timeout=timeout) as response:
87
+ return json.load(response)
88
+
89
+
90
+ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
91
+ parser = argparse.ArgumentParser(description=__doc__)
92
+ parser.add_argument("--url", default="http://127.0.0.1:7862")
93
+ parser.add_argument("--timeout", type=float, default=20.0)
94
+ return parser.parse_args(argv)
95
+
96
+
97
+ def main(argv: list[str] | None = None) -> int:
98
+ args = parse_args(argv)
99
+ result = inspect_config(fetch_config(args.url, timeout=args.timeout))
100
+ print(json.dumps(result, ensure_ascii=False, indent=2))
101
+ return 0 if result["passes"] else 1
102
+
103
+
104
+ if __name__ == "__main__":
105
+ raise SystemExit(main())
tests/test_local_space_mirror.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+
3
+ from scripts.local_space_mirror import load_runtime_env_json, mirror_manifest
4
+ from scripts.smoke_local_space_mirror import inspect_config
5
+
6
+
7
+ def test_local_space_mirror_manifest_matches_space_entrypoint():
8
+ manifest = mirror_manifest("127.0.0.1", 7862)
9
+
10
+ assert manifest["mode"] == "local-space-mirror"
11
+ assert manifest["space_id"] == "build-small-hackathon/dream-customs"
12
+ assert manifest["app_file"] == "app.py"
13
+ assert manifest["url"] == "http://127.0.0.1:7862"
14
+ assert manifest["default_backends"] == {
15
+ "text": "modal",
16
+ "vision": "modal",
17
+ "asr": "modal",
18
+ }
19
+ assert manifest["env"]["text_endpoint_configured"] is False
20
+ assert manifest["env"]["vision_endpoint_configured"] is False
21
+ assert manifest["env"]["asr_endpoint_configured"] is False
22
+ assert manifest["env"]["hosted_token_configured"] is False
23
+ serialized_env = json.dumps(manifest["env"]).lower()
24
+ assert "https://" not in serialized_env
25
+ assert "secret" not in serialized_env
26
+
27
+
28
+ def test_local_space_mirror_loads_runtime_env_json_without_printing_values(tmp_path, monkeypatch):
29
+ for key in [
30
+ "DREAM_CUSTOMS_TEXT_ENDPOINT",
31
+ "DREAM_CUSTOMS_VISION_ENDPOINT",
32
+ "DREAM_CUSTOMS_ASR_ENDPOINT",
33
+ "DREAM_CUSTOMS_HOSTED_TOKEN",
34
+ ]:
35
+ monkeypatch.delenv(key, raising=False)
36
+ runtime_json = tmp_path / "runtime.json"
37
+ runtime_json.write_text(
38
+ json.dumps(
39
+ {
40
+ "DREAM_CUSTOMS_TEXT_ENDPOINT": "https://example.test/text",
41
+ "DREAM_CUSTOMS_VISION_ENDPOINT": "https://example.test/vision",
42
+ "DREAM_CUSTOMS_ASR_ENDPOINT": "https://example.test/asr",
43
+ "DREAM_CUSTOMS_HOSTED_TOKEN": "super-secret-token",
44
+ "UNRELATED": "ignored",
45
+ }
46
+ ),
47
+ encoding="utf-8",
48
+ )
49
+
50
+ load_result = load_runtime_env_json(runtime_json)
51
+ manifest = mirror_manifest("127.0.0.1", 7862)
52
+
53
+ assert load_result["loaded"] is True
54
+ assert "UNRELATED" not in load_result["configured_keys"]
55
+ assert manifest["env"]["text_endpoint_configured"] is True
56
+ assert manifest["env"]["vision_endpoint_configured"] is True
57
+ assert manifest["env"]["asr_endpoint_configured"] is True
58
+ assert manifest["env"]["hosted_token_configured"] is True
59
+ serialized = json.dumps(manifest, ensure_ascii=False)
60
+ assert "https://example.test" not in serialized
61
+ assert "super-secret-token" not in serialized
62
+
63
+
64
+ def test_local_space_mirror_config_smoke_requires_composer_debug_markers():
65
+ config = {
66
+ "title": "Dream QA",
67
+ "css": ".dc-stepper .dc-mic-button .dc-attach-button .dc-image-popover .dc-debug-panel",
68
+ "components": [
69
+ {"props": {"label": "Dream note"}},
70
+ {"props": {"label": "Language"}},
71
+ {"props": {"label": "Text generation", "value": "modal"}},
72
+ {"props": {"label": "Image understanding", "value": "modal"}},
73
+ {"props": {"label": "Voice input", "value": "modal"}},
74
+ {"props": {"label": "Runtime state"}},
75
+ ],
76
+ }
77
+
78
+ result = inspect_config(config)
79
+
80
+ assert result["passes"] is True
81
+ assert result["backend_defaults"]["Text generation"] == "modal"
82
+
83
+
84
+ def test_local_space_mirror_config_smoke_fails_on_stale_ui():
85
+ config = {
86
+ "title": "Dream QA",
87
+ "css": ".dc-stepper",
88
+ "components": [
89
+ {"props": {"label": "Dream note"}},
90
+ {"props": {"label": "Text generation", "value": "demo"}},
91
+ ],
92
+ }
93
+
94
+ result = inspect_config(config)
95
+
96
+ assert result["passes"] is False
97
+ assert ".dc-mic-button" in result["failures"]["missing_css"]
98
+ assert result["failures"]["backend_defaults"]["Text generation"] == "demo"
tests/test_ui_actions.py CHANGED
@@ -6,6 +6,7 @@ from dream_customs.ui.actions import answer_to_card_action, initial_mobile_state
6
  import dream_customs.ui.app as ui_app
7
  from dream_customs.ui.app import _reset
8
  from dream_customs.ui.copy import DEFAULT_MOOD, PROCESSING_NOTE
 
9
 
10
 
11
  def test_mobile_defaults_to_modal_backends():
@@ -18,16 +19,21 @@ def test_mobile_defaults_to_modal_backends():
18
 
19
  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
  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
@@ -38,20 +44,94 @@ def test_voice_input_keeps_modal_asr_component_but_uses_inline_mic_button():
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():
 
6
  import dream_customs.ui.app as ui_app
7
  from dream_customs.ui.app import _reset
8
  from dream_customs.ui.copy import DEFAULT_MOOD, PROCESSING_NOTE
9
+ from dream_customs.ui.styles import CSS
10
 
11
 
12
  def test_mobile_defaults_to_modal_backends():
 
19
 
20
  def test_runtime_settings_are_collapsed_for_public_flow():
21
  source = inspect.getsource(ui_app.build_demo)
22
+ flow_column_source = source.split('with gr.Column(elem_classes=["dc-flow-column"]):', 1)[1].split(
23
+ 'with gr.Column(elem_classes=["dc-side-panel"]):', 1
24
+ )[0]
25
 
26
  assert 'gr.Accordion("Advanced", open=False' in source
27
  assert 'with gr.Accordion(initial_copy["debug_title"], open=False' in source
28
+ assert 'with gr.Accordion(initial_copy["debug_title"], open=False' in flow_column_source
29
 
30
 
31
  def test_voice_input_keeps_modal_asr_component_but_uses_inline_mic_button():
32
  source = inspect.getsource(ui_app.build_demo)
33
 
34
  assert "audio_input = gr.Audio(" in source
35
+ assert 'sources=["upload"]' in source
36
+ assert 'sources=["microphone", "upload"]' not in source
37
  assert 'type="filepath"' in source
38
  assert "visible=False" in source
39
  assert "mic_html = gr.HTML(_mic_html(DEFAULT_LANGUAGE))" in source
 
44
  def test_image_upload_is_composer_plus_drawer():
45
  source = inspect.getsource(ui_app.build_demo)
46
 
47
+ assert "attachment_html = gr.HTML(_attachment_html(DEFAULT_LANGUAGE))" in source
48
+ assert 'elem_classes=["dc-image-popover"]' in source
49
+ assert 'image_input = gr.Image(' in source
50
+ assert 'sources=["upload", "clipboard"]' in source
51
+ assert "gr.Accordion(" not in source.split("image_input = gr.Image(")[0].split("with gr.Group(elem_classes=[\"dc-composer\"]):")[-1]
52
+
53
+
54
+ def test_image_popover_uses_unified_upload_clipboard_ui():
55
+ assert "data-upload" in ui_app._attachment_html("en")
56
+ assert "data-paste" in ui_app._attachment_html("zh")
57
+ assert "上传图片" in ui_app._attachment_html("zh")
58
+ assert ".dc-image-popover [data-testid=\"source-select\"]" in CSS
59
+ assert "border-top: 0 !important;" in CSS
60
+ assert '[aria-label="Capture from camera"]' in CSS
61
+ assert "display: none !important;" in CSS
62
+ assert ".dc-image-upload-copy" in CSS
63
+ assert ".dc-image-label-copy" in inspect.getsource(ui_app)
64
+ assert "clipboardSelected ? copy.paste : copy.upload" in inspect.getsource(ui_app)
65
+
66
+
67
+ def test_composer_scripts_are_attached_to_blocks():
68
+ source = inspect.getsource(ui_app.build_demo)
69
+
70
+ assert "with gr.Blocks(css=CSS, js=VOICE_JS, title=APP_TITLE)" in source
71
+ assert "mic_html = gr.HTML(_mic_html(DEFAULT_LANGUAGE))" in source
72
+ assert "attachment_html = gr.HTML(_attachment_html(DEFAULT_LANGUAGE))" in source
73
 
74
 
75
  def test_hero_stepper_tracks_app_status():
76
  ask_html = ui_app._hero_html(status="ask")
77
  tip_html = ui_app._hero_html(status="tip")
78
 
79
+ assert '<span class="dc-step is-complete"><strong>1</strong>' in ask_html
80
+ assert '<i class="dc-stepper-line is-complete" aria-hidden="true"></i>' in ask_html
81
+ assert '<span class="dc-step is-active" aria-current="step"><strong>2</strong>' in ask_html
82
+ assert '<span class="dc-step is-complete"><strong>3</strong>' in tip_html
83
+ assert '<span class="dc-step is-active" aria-current="step"><strong>4</strong>' in tip_html
84
+ assert tip_html.count("dc-stepper-line is-complete") == 3
85
+
86
+
87
+ def test_hero_subtitle_explains_app_instead_of_legacy_name():
88
+ hero_html = ui_app._hero_html()
89
+
90
+ assert "grounded Today Tip" in hero_html
91
+ assert "Dream Customs" not in hero_html
92
+
93
+
94
+ def test_side_panel_dropdown_hover_stays_readable():
95
+ assert '.dc-side-panel .dc-section-title strong' in CSS
96
+ assert '.gradio-container [role="option"]:hover' in CSS
97
+ assert 'background: var(--dqa-sage-soft) !important;' in CSS
98
+ assert 'color: var(--dqa-ink) !important;' in CSS
99
+ assert '.dc-side-panel .block:has(input[role="listbox"])' in CSS
100
+ assert 'resize: none !important;' in CSS
101
+ assert 'pointer-events: none !important;' in CSS
102
+ assert '> .container input[role="listbox"][aria-expanded="true"]) > .container .dropdown-arrow' in CSS
103
+ assert '.dc-side-panel .block:has(.wrap-inner.show_options) .dropdown-arrow' not in CSS
104
+ assert '.dc-side-panel .block:has(.options) .dropdown-arrow' not in CSS
105
+ assert 'transform: rotate(180deg) !important;' in CSS
106
+ assert '.dc-side-panel .block:has(input[role="listbox"]) .wrap-inner' in CSS
107
+ assert '.dc-side-panel .block:has(input[role="listbox"]) .secondary-wrap' in CSS
108
+ assert '.dc-side-panel .form:has(input[role="listbox"])' in CSS
109
+ assert '--input-border-width: 0px;' in CSS
110
+ assert '--block-border-width: 0px;' in CSS
111
+
112
+
113
+ def test_advanced_panel_uses_compact_readable_controls():
114
+ assert '.dc-side-panel .dc-dev-help span' in CSS
115
+ assert 'color: #60717a !important;' in CSS
116
+ assert '.dc-side-panel .dc-dev .block:has(input[role="listbox"])' in CSS
117
+ assert 'padding: 12px 14px !important;' in CSS
118
+ assert '.dc-side-panel .dc-dev .block:has(input[role="listbox"]) input[role="listbox"]' in CSS
119
+ assert 'font-size: 0.94rem !important;' in CSS
120
+ assert '.dc-side-panel .dc-dev > button' in CSS
121
+ assert 'min-height: 46px !important;' in CSS
122
+ assert '.dc-side-panel .dc-dev-tuning' in CSS
123
+ assert 'background: rgba(255, 253, 248, 0.98) !important;' in CSS
124
+ assert 'overflow: hidden !important;' in CSS
125
+ assert '.dc-side-panel .dc-dev-tuning > .styler' in CSS
126
+ assert 'background: transparent !important;' in CSS
127
+ assert '.dc-flow-column .dc-debug-panel' in CSS
128
+ assert 'align-items: start !important;' in CSS
129
+ source = inspect.getsource(ui_app.build_demo)
130
+ assert 'with gr.Group(elem_classes=["dc-dev-tuning"])' in source
131
+ assert "text_temperature = gr.Number(" in source
132
+ assert "text_temperature = gr.Slider(" not in source
133
+ assert source.index("text_temperature = gr.Number(") < source.index("text_backend = gr.Dropdown(")
134
+ assert source.index("text_temperature = gr.Number(") < source.index('with gr.Accordion("Advanced endpoints"')
135
 
136
 
137
  def test_processing_note_is_story_copy_not_backend_jargon():