mirror of
https://github.com/we-promise/sure.git
synced 2026-05-31 16:29:03 +00:00
* 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>
76 lines
1.9 KiB
JavaScript
76 lines
1.9 KiB
JavaScript
import { Controller } from "@hotwired/stimulus";
|
|
|
|
export default class extends Controller {
|
|
static targets = ["messages", "form", "input", "submit"];
|
|
|
|
connect() {
|
|
this.#configureAutoScroll();
|
|
this.#updateSubmitState();
|
|
}
|
|
|
|
disconnect() {
|
|
if (this.messagesObserver) {
|
|
this.messagesObserver.disconnect();
|
|
}
|
|
}
|
|
|
|
autoResize() {
|
|
const input = this.inputTarget;
|
|
const lineHeight = 20; // text-sm line-height (14px * 1.429 ≈ 20px)
|
|
const maxLines = 3; // 3 lines = 60px total
|
|
|
|
input.style.height = "auto";
|
|
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();
|
|
}, 200);
|
|
}
|
|
|
|
// Newlines require shift+enter, otherwise submit the form (same functionality as ChatGPT and others)
|
|
handleInputKeyDown(e) {
|
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
e.preventDefault();
|
|
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) {
|
|
this.#scrollToBottom();
|
|
}
|
|
});
|
|
|
|
// Listen to entire sidebar for changes, always try to scroll to the bottom
|
|
this.messagesObserver.observe(this.element, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
}
|
|
|
|
#scrollToBottom = () => {
|
|
this.messagesTarget.scrollTop = this.messagesTarget.scrollHeight;
|
|
};
|
|
}
|