RemiFabre commited on
Commit
c0e8ee8
Β·
1 Parent(s): a0065ba

Simplify recording form: Name + Duration + Record in one row

Browse files

P1 UX overhaul part 1:
- Remove description field (rarely used, cluttered the form)
- Rename "Move label (optional)" to "Name"
- Move audio source into collapsible "Options" panel
- Add horizontal radio layout for audio source options
- Add big countdown number display for countdown/recording phases
- Hide number input spinners (cleaner look)
- Update E2E tests for new layout (+4 new tests)

marionette/static/index.html CHANGED
@@ -21,61 +21,63 @@
21
  <h2>Record a Move</h2>
22
 
23
  <form id="record-form">
24
- <div class="field-group">
25
- <label for="duration">Duration (seconds)</label>
26
- <input
27
- id="duration"
28
- name="duration"
29
- type="number"
30
- min="0.5"
31
- max="300"
32
- step="any"
33
- required
34
- />
35
- </div>
36
- <div class="field-group">
37
- <label for="label">Move label (optional)</label>
38
- <input id="label" name="label" type="text" maxlength="80" placeholder="e.g. gentle-nod" />
39
- </div>
40
- <div class="field-group">
41
- <label for="description">Description (optional)</label>
42
- <textarea
43
- id="description"
44
- name="description"
45
- maxlength="500"
46
- rows="2"
47
- placeholder="Notes about the intent, feeling, or context."
48
- ></textarea>
49
- </div>
50
- <div class="field-group audio-source-group">
51
- <label>Audio source</label>
52
- <div class="radio-group">
53
- <label class="radio-option">
54
- <input type="radio" name="audio-source" id="audio-source-mic" value="mic" checked />
55
- <span>Record from robot's mic</span>
56
- </label>
57
- <label class="radio-option">
58
- <input type="radio" name="audio-source" id="audio-source-upload" value="upload" />
59
- <span>Upload audio file</span>
60
- </label>
61
- <label class="radio-option">
62
- <input type="radio" name="audio-source" id="audio-source-none" value="none" />
63
- <span>No audio (silent)</span>
64
- </label>
65
  </div>
66
- <span class="hint" id="audio-hint"></span>
67
- </div>
68
- <div class="field-group audio-upload-group" id="audio-upload-group" hidden>
69
- <label>Audio file (WAV or MP3)</label>
70
- <input type="file" id="audio-file" accept=".wav,.mp3,audio/wav,audio/mpeg" />
71
- <button type="button" class="audio-upload-btn" id="audio-upload-btn">Choose file...</button>
72
- <p class="hint" id="upload-status">The audio will be played when recording starts, so you can perform in sync.</p>
 
 
 
 
 
 
73
  </div>
74
- <button type="submit" class="primary-btn" id="record-btn">Start Recording</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  </form>
76
 
77
  <div class="progress-area" id="progress-area">
78
  <p id="primary-status">Ready.</p>
 
 
 
 
79
  <div class="meter" id="progress-meter">
80
  <div class="meter-fill" id="progress-fill"></div>
81
  </div>
 
21
  <h2>Record a Move</h2>
22
 
23
  <form id="record-form">
24
+ <div class="record-main-fields">
25
+ <div class="field-group">
26
+ <label for="label">Name</label>
27
+ <input id="label" name="label" type="text" maxlength="80" placeholder="e.g. gentle-nod" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  </div>
29
+ <div class="field-group">
30
+ <label for="duration">Duration (s)</label>
31
+ <input
32
+ id="duration"
33
+ name="duration"
34
+ type="number"
35
+ min="0.5"
36
+ max="300"
37
+ step="any"
38
+ required
39
+ />
40
+ </div>
41
+ <button type="submit" class="primary-btn record-submit" id="record-btn">Record</button>
42
  </div>
43
+
44
+ <details class="record-options" id="record-options">
45
+ <summary>Options</summary>
46
+ <div class="record-options-content">
47
+ <div class="field-group">
48
+ <label>Audio source</label>
49
+ <div class="radio-group horizontal">
50
+ <label class="radio-option">
51
+ <input type="radio" name="audio-source" id="audio-source-mic" value="mic" checked />
52
+ <span>Mic</span>
53
+ </label>
54
+ <label class="radio-option">
55
+ <input type="radio" name="audio-source" id="audio-source-upload" value="upload" />
56
+ <span>Upload</span>
57
+ </label>
58
+ <label class="radio-option">
59
+ <input type="radio" name="audio-source" id="audio-source-none" value="none" />
60
+ <span>Silent</span>
61
+ </label>
62
+ </div>
63
+ <span class="hint" id="audio-hint"></span>
64
+ </div>
65
+ <div class="field-group audio-upload-group" id="audio-upload-group" hidden>
66
+ <label>Audio file (WAV or MP3)</label>
67
+ <input type="file" id="audio-file" accept=".wav,.mp3,audio/wav,audio/mpeg" />
68
+ <button type="button" class="audio-upload-btn" id="audio-upload-btn">Choose file...</button>
69
+ <p class="hint" id="upload-status">Audio plays when recording starts so you can perform in sync.</p>
70
+ </div>
71
+ </div>
72
+ </details>
73
  </form>
74
 
75
  <div class="progress-area" id="progress-area">
76
  <p id="primary-status">Ready.</p>
77
+ <div class="phase-display hidden" id="phase-display">
78
+ <div class="phase-countdown-number" id="countdown-number"></div>
79
+ <div class="phase-label" id="phase-label"></div>
80
+ </div>
81
  <div class="meter" id="progress-meter">
82
  <div class="meter-fill" id="progress-fill"></div>
83
  </div>
marionette/static/main.js CHANGED
@@ -18,8 +18,10 @@ const audioUploadGroup = document.getElementById("audio-upload-group");
18
  const audioFileInput = document.getElementById("audio-file");
19
  const audioUploadBtn = document.getElementById("audio-upload-btn");
20
  const uploadStatus = document.getElementById("upload-status");
21
- const descriptionInput = document.getElementById("description");
22
  const recordBtn = document.getElementById("record-btn");
 
 
 
23
  const refreshBtn = document.getElementById("refresh-btn");
24
  const movesList = document.getElementById("moves-list");
25
  const recordForm = document.getElementById("record-form");
@@ -351,7 +353,6 @@ async function queueRecording(event) {
351
 
352
  const duration = parseFloat(durationInput.value || lastState?.config?.default_duration || 5);
353
  const label = labelInput.value.trim() || null;
354
- const description = descriptionInput.value.trim() || null;
355
 
356
  // Determine audio settings from radio selection
357
  const audioSource = document.querySelector('input[name="audio-source"]:checked')?.value || "mic";
@@ -368,7 +369,7 @@ async function queueRecording(event) {
368
  recordBtn.textContent = "Queuing…";
369
 
370
  try {
371
- const payload = { duration, record_audio: recordAudio, label, description };
372
  if (audioId) {
373
  payload.uploaded_audio_id = audioId;
374
  }
@@ -761,6 +762,10 @@ function updateProgressDisplay() {
761
  if (phaseStartAt == null || phaseEndAt == null) {
762
  progressFill.style.width = "0%";
763
  countdownEl.textContent = "";
 
 
 
 
764
  return;
765
  }
766
 
@@ -774,10 +779,26 @@ function updateProgressDisplay() {
774
 
775
  if (phaseMode === "countdown") {
776
  countdownEl.textContent = `Get ready… ${remaining.toFixed(1)}s`;
 
 
 
 
 
 
777
  } else if (phaseMode === "recording") {
778
  countdownEl.textContent = `Recording… ${remaining.toFixed(1)}s`;
 
 
 
 
 
 
779
  } else {
780
  countdownEl.textContent = "";
 
 
 
 
781
  }
782
  }
783
 
 
18
  const audioFileInput = document.getElementById("audio-file");
19
  const audioUploadBtn = document.getElementById("audio-upload-btn");
20
  const uploadStatus = document.getElementById("upload-status");
 
21
  const recordBtn = document.getElementById("record-btn");
22
+ const phaseDisplay = document.getElementById("phase-display");
23
+ const countdownNumber = document.getElementById("countdown-number");
24
+ const phaseLabel = document.getElementById("phase-label");
25
  const refreshBtn = document.getElementById("refresh-btn");
26
  const movesList = document.getElementById("moves-list");
27
  const recordForm = document.getElementById("record-form");
 
353
 
354
  const duration = parseFloat(durationInput.value || lastState?.config?.default_duration || 5);
355
  const label = labelInput.value.trim() || null;
 
356
 
357
  // Determine audio settings from radio selection
358
  const audioSource = document.querySelector('input[name="audio-source"]:checked')?.value || "mic";
 
369
  recordBtn.textContent = "Queuing…";
370
 
371
  try {
372
+ const payload = { duration, record_audio: recordAudio, label };
373
  if (audioId) {
374
  payload.uploaded_audio_id = audioId;
375
  }
 
762
  if (phaseStartAt == null || phaseEndAt == null) {
763
  progressFill.style.width = "0%";
764
  countdownEl.textContent = "";
765
+ if (phaseDisplay) {
766
+ phaseDisplay.classList.add("hidden");
767
+ phaseDisplay.classList.remove("countdown", "recording");
768
+ }
769
  return;
770
  }
771
 
 
779
 
780
  if (phaseMode === "countdown") {
781
  countdownEl.textContent = `Get ready… ${remaining.toFixed(1)}s`;
782
+ if (phaseDisplay) {
783
+ phaseDisplay.classList.remove("hidden", "recording");
784
+ phaseDisplay.classList.add("countdown");
785
+ countdownNumber.textContent = Math.ceil(remaining);
786
+ phaseLabel.textContent = "Get ready";
787
+ }
788
  } else if (phaseMode === "recording") {
789
  countdownEl.textContent = `Recording… ${remaining.toFixed(1)}s`;
790
+ if (phaseDisplay) {
791
+ phaseDisplay.classList.remove("hidden", "countdown");
792
+ phaseDisplay.classList.add("recording");
793
+ countdownNumber.textContent = remaining.toFixed(1);
794
+ phaseLabel.textContent = "Recording";
795
+ }
796
  } else {
797
  countdownEl.textContent = "";
798
+ if (phaseDisplay) {
799
+ phaseDisplay.classList.add("hidden");
800
+ phaseDisplay.classList.remove("countdown", "recording");
801
+ }
802
  }
803
  }
804
 
marionette/static/style.css CHANGED
@@ -90,12 +90,125 @@ h1 {
90
  }
91
 
92
  .recording-panel form {
93
- display: grid;
94
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
95
  gap: 1rem;
96
  margin-bottom: 1.25rem;
97
  }
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  .dataset-controls {
100
  display: flex;
101
  align-items: flex-end;
@@ -295,12 +408,7 @@ textarea {
295
  cursor: pointer;
296
  }
297
 
298
- .audio-source-group {
299
- grid-column: 1 / -1;
300
- }
301
-
302
  .audio-upload-group {
303
- grid-column: 1 / -1;
304
  padding: 1rem;
305
  background: rgba(249, 115, 22, 0.08);
306
  border: 1px dashed rgba(249, 115, 22, 0.3);
@@ -658,8 +766,17 @@ code {
658
  padding: 1.5rem 1rem 2rem;
659
  }
660
 
661
- .recording-panel form {
662
- grid-template-columns: 1fr;
 
 
 
 
 
 
 
 
 
663
  }
664
 
665
  .moves-list li {
 
90
  }
91
 
92
  .recording-panel form {
93
+ display: flex;
94
+ flex-direction: column;
95
  gap: 1rem;
96
  margin-bottom: 1.25rem;
97
  }
98
 
99
+ .record-main-fields {
100
+ display: flex;
101
+ align-items: flex-end;
102
+ gap: 0.75rem;
103
+ }
104
+
105
+ .record-main-fields .field-group {
106
+ flex: 1;
107
+ min-width: 0;
108
+ }
109
+
110
+ .record-main-fields .record-submit {
111
+ flex-shrink: 0;
112
+ white-space: nowrap;
113
+ }
114
+
115
+ /* Hide number input spinners */
116
+ input[type="number"]::-webkit-outer-spin-button,
117
+ input[type="number"]::-webkit-inner-spin-button {
118
+ -webkit-appearance: none;
119
+ margin: 0;
120
+ }
121
+ input[type="number"] {
122
+ -moz-appearance: textfield;
123
+ }
124
+
125
+ /* Collapsible options */
126
+ .record-options {
127
+ border: 1px solid var(--border);
128
+ border-radius: 14px;
129
+ padding: 0;
130
+ overflow: hidden;
131
+ }
132
+
133
+ .record-options summary {
134
+ font-size: 0.85rem;
135
+ font-weight: 600;
136
+ text-transform: uppercase;
137
+ letter-spacing: 0.1em;
138
+ color: var(--muted);
139
+ cursor: pointer;
140
+ list-style: none;
141
+ padding: 0.75rem 1rem;
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 0.5rem;
145
+ }
146
+
147
+ .record-options summary::after {
148
+ content: "β–Ύ";
149
+ font-size: 0.75rem;
150
+ margin-left: auto;
151
+ }
152
+
153
+ .record-options[open] summary::after {
154
+ content: "β–΄";
155
+ }
156
+
157
+ .record-options-content {
158
+ padding: 0 1rem 1rem;
159
+ display: flex;
160
+ flex-direction: column;
161
+ gap: 0.75rem;
162
+ }
163
+
164
+ .radio-group.horizontal {
165
+ flex-direction: row;
166
+ gap: 0.5rem;
167
+ flex-wrap: wrap;
168
+ }
169
+
170
+ .radio-group.horizontal .radio-option {
171
+ flex: 1;
172
+ min-width: 0;
173
+ justify-content: center;
174
+ text-align: center;
175
+ }
176
+
177
+ /* Big phase display for countdown / recording */
178
+ .phase-display {
179
+ text-align: center;
180
+ padding: 1.5rem 0;
181
+ }
182
+
183
+ .phase-display.hidden {
184
+ display: none;
185
+ }
186
+
187
+ .phase-countdown-number {
188
+ font-size: clamp(3rem, 8vw, 5rem);
189
+ font-weight: 800;
190
+ line-height: 1;
191
+ letter-spacing: -0.04em;
192
+ font-variant-numeric: tabular-nums;
193
+ }
194
+
195
+ .phase-display.countdown .phase-countdown-number {
196
+ color: #38bdf8;
197
+ }
198
+
199
+ .phase-display.recording .phase-countdown-number {
200
+ color: #ef4444;
201
+ }
202
+
203
+ .phase-label {
204
+ font-size: 1rem;
205
+ font-weight: 600;
206
+ text-transform: uppercase;
207
+ letter-spacing: 0.15em;
208
+ color: var(--muted);
209
+ margin-top: 0.25rem;
210
+ }
211
+
212
  .dataset-controls {
213
  display: flex;
214
  align-items: flex-end;
 
408
  cursor: pointer;
409
  }
410
 
 
 
 
 
411
  .audio-upload-group {
 
412
  padding: 1rem;
413
  background: rgba(249, 115, 22, 0.08);
414
  border: 1px dashed rgba(249, 115, 22, 0.3);
 
766
  padding: 1.5rem 1rem 2rem;
767
  }
768
 
769
+ .record-main-fields {
770
+ flex-direction: column;
771
+ align-items: stretch;
772
+ }
773
+
774
+ .record-main-fields .record-submit {
775
+ width: 100%;
776
+ }
777
+
778
+ .radio-group.horizontal {
779
+ flex-direction: column;
780
  }
781
 
782
  .moves-list li {
tests/e2e/test_ui.py CHANGED
@@ -59,10 +59,33 @@ class TestIdleState:
59
  width = fill.evaluate("el => el.style.width")
60
  assert width == "0%" or width == ""
61
 
 
 
 
 
 
62
 
63
  # ──────── Form validation tests ───────────────────────────────────────
64
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  class TestFormValidation:
67
  def test_duration_field_exists(self, page: Page, base_url: str):
68
  page.goto(base_url)
@@ -91,6 +114,8 @@ class TestFormValidation:
91
 
92
  def test_audio_source_radios_exist(self, page: Page, base_url: str):
93
  page.goto(base_url)
 
 
94
  expect(page.locator("#audio-source-mic")).to_be_visible()
95
  expect(page.locator("#audio-source-none")).to_be_visible()
96
 
@@ -107,7 +132,8 @@ class TestRecordingSubmission:
107
  page.goto(base_url)
108
  page.wait_for_timeout(2000)
109
 
110
- # Set audio source to "none" to avoid audio backend requirement
 
111
  page.locator("#audio-source-none").check()
112
  page.locator("#duration").fill("2")
113
  page.locator("#label").fill("e2e-test")
 
59
  width = fill.evaluate("el => el.style.width")
60
  assert width == "0%" or width == ""
61
 
62
+ def test_phase_display_hidden_when_idle(self, page: Page, base_url: str):
63
+ page.goto(base_url)
64
+ page.wait_for_timeout(2000)
65
+ expect(page.locator("#phase-display")).to_be_hidden()
66
+
67
 
68
  # ──────── Form validation tests ───────────────────────────────────────
69
 
70
 
71
+ class TestSimplifiedForm:
72
+ def test_name_and_duration_visible_by_default(self, page: Page, base_url: str):
73
+ page.goto(base_url)
74
+ expect(page.locator("#label")).to_be_visible()
75
+ expect(page.locator("#duration")).to_be_visible()
76
+ expect(page.locator("#record-btn")).to_be_visible()
77
+
78
+ def test_options_collapsed_by_default(self, page: Page, base_url: str):
79
+ """Audio source should be hidden in Options until user expands it."""
80
+ page.goto(base_url)
81
+ expect(page.locator("#audio-source-mic")).to_be_hidden()
82
+
83
+ def test_description_field_removed(self, page: Page, base_url: str):
84
+ """P1: description field was removed to simplify the form."""
85
+ page.goto(base_url)
86
+ assert page.locator("#description").count() == 0
87
+
88
+
89
  class TestFormValidation:
90
  def test_duration_field_exists(self, page: Page, base_url: str):
91
  page.goto(base_url)
 
114
 
115
  def test_audio_source_radios_exist(self, page: Page, base_url: str):
116
  page.goto(base_url)
117
+ # Audio source is inside collapsible Options β€” open it first
118
+ page.locator("#record-options summary").click()
119
  expect(page.locator("#audio-source-mic")).to_be_visible()
120
  expect(page.locator("#audio-source-none")).to_be_visible()
121
 
 
132
  page.goto(base_url)
133
  page.wait_for_timeout(2000)
134
 
135
+ # Open Options and set audio source to "none" to avoid audio backend requirement
136
+ page.locator("#record-options summary").click()
137
  page.locator("#audio-source-none").check()
138
  page.locator("#duration").fill("2")
139
  page.locator("#label").fill("e2e-test")