From 84ad60d5411bfa3ae7bd6ac38811529284a4fa44 Mon Sep 17 00:00:00 2001 From: Jeff <158072326+jeffrey701@users.noreply.github.com> Date: Fri, 29 May 2026 16:30:11 -0700 Subject: [PATCH] fix(ai-chat): disable submit on empty input instead of surfacing 'Content missing' (#1697) (#1872) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- app/javascript/controllers/chat_controller.js | 19 +++++++++++++++++-- app/views/messages/_chat_form.html.erb | 3 ++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/app/javascript/controllers/chat_controller.js b/app/javascript/controllers/chat_controller.js index 7e067309c..25020f35e 100644 --- a/app/javascript/controllers/chat_controller.js +++ b/app/javascript/controllers/chat_controller.js @@ -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) { diff --git a/app/views/messages/_chat_form.html.erb b/app/views/messages/_chat_form.html.erb index 1fbc57c82..92d401e25 100644 --- a/app/views/messages/_chat_form.html.erb +++ b/app/views/messages/_chat_form.html.erb @@ -23,7 +23,8 @@ <% end %> - <%= icon("arrow-up", as_button: true, type: "submit") %> + <%= icon("arrow-up", as_button: true, type: "submit", + data: { chat_target: "submit" }) %> <% end %>