fix(ai-chat): disable submit on empty input instead of surfacing 'Content missing' (#1697) (#1872)

* fix(ai-chat): disable submit on empty input instead of surfacing 'Content missing' (#1697)

Empty-input clicks on the chat send button posted the form, which then
failed Message's `validates :content, presence: true` and surfaced
`Content missing` to the user. The right shape per ChatGPT / Claude
UX is to prevent the submission entirely until the input contains
non-whitespace content.

Add a `submit` target on the icon button and have the existing chat
Stimulus controller:

- Initialise the button to `disabled` when no `message_hint` is set.
- Toggle disabled on every input event (re-using the existing
  `autoResize` handler) based on `input.value.trim().length > 0`.
- Pre-clear disabled when a sample question is injected.
- Short-circuit the Enter-key submit path on empty content so keyboard
  users hit the same gate.

Closes #1697

* fix(ai-chat): drop server-rendered disabled attr, keep JS-driven gate (#1697)

Codex review (P1) + @JSONbored + @jjmata called out that rendering the
submit button with `disabled: message_hint.blank?` would lock the
form out for users without working JS (asset failure, exception during
Stimulus init, etc.). Server-side validation already catches empty
submits with a real error message — server-disabling the button on top
of that turns a soft fail into a hard one.

Remove the server-render `disabled:` attribute. The chat Stimulus
controller still runs `#updateSubmitState()` on connect, on every
input event, and after sample-question injection, and `handleInputKeyDown`
still short-circuits empty Enter submits. With JS the UX is identical;
without JS the form keeps its fallback path.

---------

Co-authored-by: jeffrey701 <jeffrey701@users.noreply.github.com>
This commit is contained in:
Jeff
2026-05-29 16:30:11 -07:00
committed by GitHub
parent f397b1a722
commit 84ad60d541
2 changed files with 19 additions and 3 deletions

View File

@@ -1,10 +1,11 @@
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = ["messages", "form", "input"];
static targets = ["messages", "form", "input", "submit"];
connect() {
this.#configureAutoScroll();
this.#updateSubmitState();
}
disconnect() {
@@ -22,10 +23,13 @@ export default class extends Controller {
input.style.height = `${Math.min(input.scrollHeight, lineHeight * maxLines)}px`;
input.style.overflowY =
input.scrollHeight > lineHeight * maxLines ? "auto" : "hidden";
this.#updateSubmitState();
}
submitSampleQuestion(e) {
this.inputTarget.value = e.target.dataset.chatQuestionParam;
this.#updateSubmitState();
setTimeout(() => {
this.formTarget.requestSubmit();
@@ -36,10 +40,21 @@ export default class extends Controller {
handleInputKeyDown(e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
this.formTarget.requestSubmit();
if (this.#hasContent()) {
this.formTarget.requestSubmit();
}
}
}
#hasContent() {
return this.inputTarget.value.trim().length > 0;
}
#updateSubmitState() {
if (!this.hasSubmitTarget) return;
this.submitTarget.disabled = !this.#hasContent();
}
#configureAutoScroll() {
this.messagesObserver = new MutationObserver((_mutations) => {
if (this.hasMessagesTarget) {