Commit ·
f79d783
1
Parent(s): 524af63
Fix space dropdown swap target, add next-step arrow cue
Browse filesThe 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.
- frontend/bridge.js +24 -1
- frontend/index.html +4 -1
- frontend/styles.css +23 -0
- icons.py +1 -0
- 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)
|
|
|
|
|
|
|
|
|
|
| 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"
|
|
|
|
| 55 |
<option value="">loading the org…</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…</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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|