A question that offers real choices, and says how many you may take

Three things about `ask_user`, all of them about the card being answerable
rather than about the tool being callable.

Options are required now, and they are objects: a label, and a line of
description where the label alone does not say what choosing it would mean.
"Rewrite it" and "Patch it" are two words that do not tell you which one loses
your uncommitted work. They stack one per line, because a row of chips has
nowhere to put the second line and no room to read the first.

The model says whether they are exclusive. Only it knows whether its options are
alternatives or a set, and the card has to show which -- a radio group offered
where checkboxes were meant loses every answer but one. Exclusive is the
default, being the cheaper mistake. A `multiple` question posts the same field
name once per ticked box, so the endpoint gathers choices into a list; the
`setdefault` it did before kept the first and dropped the rest, which is an
answer that says something the reader did not.

And "Something else" is added here, on every question, with the box behind it
revealed by `:has()` and no JavaScript at all. The model is told never to write
an "other" option of its own, because its version would be a choice with no box
behind it -- a word submitted that means nothing. It carries a sentinel rather
than an answer, and the endpoint swaps in what was typed beside it, or drops it
when the box was left empty rather than telling the model the answer is
"__other__".

Typing no longer beats picking. That rule belonged to a box that was always
visible next to the options; this one only exists once its own option is chosen,
so picking is the answer and the box is one of the things you can pick.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-04 19:34:47 +02:00
parent 74a3c0f9d3
commit 3065878bd0
9 changed files with 524 additions and 44 deletions
+60 -1
View File
@@ -494,9 +494,68 @@
border-top: 1px solid var(--border);
}
.interaction__title { margin: 0; padding: 0; color: var(--ink); font-weight: 500; }
.interaction__options { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
/* Stacked, one per line. A row of chips was fine while an option was two words
and nothing else; an option now carries a description as well, and a row has
nowhere to put the second and no room to read the first. */
.interaction__options { display: flex; flex-direction: column; gap: var(--sp-2); }
.interaction__note { color: var(--ink-faint); font-size: var(--text-xs); }
/* One option: the control, then a label and an optional line under it. The
whole row is the target -- it is a `<label>`, so the description is as
clickable as the name, which is what makes a long option readable rather
than a small circle to aim at. */
.interaction__option {
display: flex;
align-items: flex-start;
gap: var(--sp-3);
padding: var(--sp-2) var(--sp-3);
border: 1px solid var(--border-strong);
border-radius: var(--radius-sm);
background: var(--surface-raised);
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast);
}
.interaction__option:hover { background: var(--surface-hover); }
/* The native control, kept: it carries the exclusive-versus-multiple meaning
that the whole feature turns on, and a radio and a checkbox have to look
different or the card lies about how many answers it will take. */
.interaction__option input {
margin: 0;
margin-top: 0.15rem;
flex: none;
accent-color: var(--accent);
}
.interaction__option:has(input:checked) {
border-color: var(--accent);
background: var(--surface-active);
}
.interaction__option:has(input:focus-visible) {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.interaction__option-body {
display: flex;
flex-direction: column;
gap: var(--sp-1);
min-width: 0;
}
.interaction__option-name { color: var(--ink); font-size: var(--text-sm); }
.interaction__option-note {
color: var(--ink-muted);
font-size: var(--text-xs);
line-height: var(--leading-normal);
}
/* The box behind "Something else". Hidden until that row is chosen, revealed by
`:has()` on the row itself -- no JavaScript, and nothing that can fall out of
step with the control. It is always in the DOM and always submitted; the
endpoint uses it only when the sentinel beside it was actually chosen. */
.interaction__other-input { display: none; margin-top: var(--sp-1); }
.interaction__option--other:has(input:checked) .interaction__other-input {
display: block;
}
/* An option. A radio, so picking one unpicks the last, but shaped like the
button it reads as. */
.chip { position: relative; display: inline-flex; }