marinarosa commited on
Commit
f79d783
·
1 Parent(s): 524af63

Fix space dropdown swap target, add next-step arrow cue

Browse files

The space select inherited the form's hx-target=#results, so the org
options fragment rendered into the results area instead of the select;
hx-target=this scopes the swap correctly.

After step 1 lands, a floating bouncing arrow now points the user to
the review block — click scrolls to it, and an IntersectionObserver
removes the cue once step 2 is in view.

Files changed (5) hide show
  1. frontend/bridge.js +24 -1
  2. frontend/index.html +4 -1
  3. frontend/styles.css +23 -0
  4. icons.py +1 -0
  5. presenters.py +8 -1
frontend/bridge.js CHANGED
@@ -42,10 +42,33 @@ async function streamReview(block) {
42
  }
43
  }
44
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
  /* Whenever htmx swaps in new content, look for a waiting review block. */
46
  document.body.addEventListener("htmx:afterSwap", () => {
47
  const block = document.querySelector('#step-review[data-state="waiting"]');
48
- if (block) streamReview(block);
 
 
 
49
  });
50
 
51
  /* Free-text space input wins over the dropdown. */
 
42
  }
43
  }
44
 
45
+ /* Floating arrow that walks the user from step 1 down to step 2; it leaves
46
+ * once the review block scrolls into view. */
47
+ function armNextStepCue() {
48
+ const cue = document.getElementById("next-step-cue");
49
+ const review = document.getElementById("step-review");
50
+ if (!cue || !review) return;
51
+ cue.addEventListener("click", () =>
52
+ review.scrollIntoView({ behavior: "smooth", block: "start" }),
53
+ );
54
+ new IntersectionObserver(
55
+ (entries, obs) => {
56
+ if (entries.some((e) => e.isIntersecting)) {
57
+ cue.remove();
58
+ obs.disconnect();
59
+ }
60
+ },
61
+ { threshold: 0.35 },
62
+ ).observe(review);
63
+ }
64
+
65
  /* Whenever htmx swaps in new content, look for a waiting review block. */
66
  document.body.addEventListener("htmx:afterSwap", () => {
67
  const block = document.querySelector('#step-review[data-state="waiting"]');
68
+ if (block) {
69
+ streamReview(block);
70
+ armNextStepCue();
71
+ }
72
  });
73
 
74
  /* Free-text space input wins over the dropdown. */
frontend/index.html CHANGED
@@ -50,8 +50,11 @@
50
  </label>
51
  <label class="field field--grow">
52
  <span class="field__label">your space</span>
 
 
53
  <select name="space" id="space-select"
54
- hx-get="/ui/spaces" hx-trigger="load" hx-swap="innerHTML">
 
55
  <option value="">loading the org&hellip;</option>
56
  </select>
57
  </label>
 
50
  </label>
51
  <label class="field field--grow">
52
  <span class="field__label">your space</span>
53
+ <!-- hx-target="this" is required: without it the select inherits
54
+ the form's hx-target="#results" and the options land there -->
55
  <select name="space" id="space-select"
56
+ hx-get="/ui/spaces" hx-trigger="load"
57
+ hx-target="this" hx-swap="innerHTML">
58
  <option value="">loading the org&hellip;</option>
59
  </select>
60
  </label>
frontend/styles.css CHANGED
@@ -303,6 +303,29 @@ select:focus-visible, input:focus-visible, button:focus-visible {
303
  }
304
  .disclaimer .ic { flex: none; color: var(--butter-deep); transform: translateY(2px); }
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  /* ---------- error card tone ---------- */
307
  .card--fix { border-color: var(--rose-deep); }
308
 
 
303
  }
304
  .disclaimer .ic { flex: none; color: var(--butter-deep); transform: translateY(2px); }
305
 
306
+ /* ---------- floating next-step cue ---------- */
307
+ .next-cue {
308
+ position: fixed;
309
+ left: 50%; bottom: 26px;
310
+ transform: translateX(-50%);
311
+ z-index: 10;
312
+ display: grid; place-items: center;
313
+ width: 46px; height: 46px;
314
+ color: #fff; background: var(--rose);
315
+ border: 2px solid var(--ink);
316
+ border-radius: 50%;
317
+ outline: 2px dashed var(--paper);
318
+ outline-offset: -6px;
319
+ box-shadow: var(--press);
320
+ cursor: pointer;
321
+ animation: bounce 1.5s ease-in-out infinite;
322
+ }
323
+ .next-cue:hover { background: var(--rose-deep); }
324
+ @keyframes bounce {
325
+ 0%, 100% { transform: translateX(-50%) translateY(0); }
326
+ 50% { transform: translateX(-50%) translateY(7px); }
327
+ }
328
+
329
  /* ---------- error card tone ---------- */
330
  .card--fix { border-color: var(--rose-deep); }
331
 
icons.py CHANGED
@@ -6,6 +6,7 @@ size/stroke/class stay in CSS control.
6
 
7
  ICON_PATHS = {
8
  "arrow-left-right": '<path d="M8 3 4 7l4 4" /><path d="M4 7h16" /><path d="m16 21 4-4-4-4" /><path d="M20 17H4" />',
 
9
  "check": '<path d="M20 6 9 17l-5-5" />',
10
  "circle-play": '<path d="M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z" /><circle cx="12" cy="12" r="10" />',
11
  "compass": '<circle cx="12" cy="12" r="10" /><path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" />',
 
6
 
7
  ICON_PATHS = {
8
  "arrow-left-right": '<path d="M8 3 4 7l4 4" /><path d="M4 7h16" /><path d="m16 21 4-4-4-4" /><path d="M20 17H4" />',
9
+ "arrow-down": '<path d="M12 5v14" /><path d="m19 12-7 7-7-7" />',
10
  "check": '<path d="M20 6 9 17l-5-5" />',
11
  "circle-play": '<path d="M9 9.003a1 1 0 0 1 1.517-.859l4.997 2.997a1 1 0 0 1 0 1.718l-4.997 2.997A1 1 0 0 1 9 14.996z" /><circle cx="12" cy="12" r="10" />',
12
  "compass": '<circle cx="12" cy="12" r="10" /><path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" />',
presenters.py CHANGED
@@ -84,7 +84,14 @@ def render_error(message: str) -> str:
84
 
85
  def render_results(ev: Evaluation, model_id: str, model_label: str) -> str:
86
  """Block 1 (rule check) + Block 2 shell (LLM review, filled by bridge.js)."""
87
- return _render_checks_block(ev) + render_review_shell(ev.space_id, model_id, model_label)
 
 
 
 
 
 
 
88
 
89
 
90
  def _render_checks_block(ev: Evaluation) -> str:
 
84
 
85
  def render_results(ev: Evaluation, model_id: str, model_label: str) -> str:
86
  """Block 1 (rule check) + Block 2 shell (LLM review, filled by bridge.js)."""
87
+ next_cue = (
88
+ '<button id="next-step-cue" class="next-cue" type="button" '
89
+ 'aria-label="Scroll to the friendly review">'
90
+ f'{icon("arrow-down", size=22, sw=2.2)}</button>'
91
+ )
92
+ return (_render_checks_block(ev)
93
+ + render_review_shell(ev.space_id, model_id, model_label)
94
+ + next_cue)
95
 
96
 
97
  def _render_checks_block(ev: Evaluation) -> str: