Reject empty/whitespace chat messages before send.
Unit Tests / test (pull_request) Successful in 12s

Closes #51. Trim prompt validation and block sendMessage on blank text so idle/mobile tabs cannot submit empty payloads.
This commit is contained in:
2026-07-28 07:07:35 -05:00
parent 86bfbcdb6d
commit b79f2b463e
4 changed files with 55 additions and 7 deletions
@@ -41,6 +41,7 @@ type AsyncChatProps = {
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
prompt: Yup.string() prompt: Yup.string()
.trim()
.min(1, "Need to have at least one character") .min(1, "Need to have at least one character")
.required("This is requried"), .required("This is requried"),
}); });
@@ -206,20 +207,24 @@ const AsyncChat = ({
{ resetForm }: any, { resetForm }: any,
): Promise<void> => { ): Promise<void> => {
// send the prompt to be saved // send the prompt to be saved
const trimmedPrompt = (prompt ?? "").trim();
if (!trimmedPrompt) {
return;
}
console.log(fileType); console.log(fileType);
try { try {
const tempConversations: ConversationPrompt[] = [ const tempConversations: ConversationPrompt[] = [
...conversationDetails, ...conversationDetails,
new ConversationPrompt({ message: prompt, user_created: true }), new ConversationPrompt({ message: trimmedPrompt, user_created: true }),
new ConversationPrompt({ message: "", user_created: false }), new ConversationPrompt({ message: "", user_created: false }),
]; ];
conversationRef.current = tempConversations; conversationRef.current = tempConversations;
setConversationDetails(tempConversations); setConversationDetails(tempConversations);
// TODO: add the file here // TODO: add the file here
console.log( console.log(
`Sending message. ${prompt} ${selectedConversation} ${fileType}`, `Sending message. ${trimmedPrompt} ${selectedConversation} ${fileType}`,
); );
sendMessage(prompt, selectedConversation, file, fileType); sendMessage(trimmedPrompt, selectedConversation, file, fileType);
resetForm(); resetForm();
} catch (e) { } catch (e) {
console.log(`error ${e}`); console.log(`error ${e}`);
@@ -91,6 +91,13 @@ function WebSocketProvider({ children }) {
return false; return false;
} }
const trimmedMessage =
typeof message === "string" ? message.trim() : "";
if (!trimmedMessage) {
console.log("Error sending message. Message text is empty");
return false;
}
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => {
@@ -98,7 +105,7 @@ function WebSocketProvider({ children }) {
if (base64File && isSocketOpen(ws.current)) { if (base64File && isSocketOpen(ws.current)) {
ws.current.send( ws.current.send(
JSON.stringify({ JSON.stringify({
message, message: trimmedMessage,
conversation_id, conversation_id,
email: accountRef.current?.email, email: accountRef.current?.email,
file: base64File, file: base64File,
@@ -112,7 +119,7 @@ function WebSocketProvider({ children }) {
} else { } else {
current.send( current.send(
JSON.stringify({ JSON.stringify({
message, message: trimmedMessage,
conversation_id, conversation_id,
email: accountRef.current?.email, email: accountRef.current?.email,
file: null, file: null,
@@ -246,4 +246,35 @@ describe('WebSocketProvider', () => {
expect(payload.message).toBe('hi'); expect(payload.message).toBe('hi');
expect(payload.email).toBe('test@example.com'); expect(payload.email).toBe('test@example.com');
}); });
it('does not send empty or whitespace-only messages', async () => {
let sendMessage;
function Capture() {
const ctx = useContext(WebSocketContext);
sendMessage = ctx[3];
return null;
}
render(
<AccountContext.Provider
value={{ account: { email: 'test@example.com' }, setAccount: () => {} }}
>
<WebSocketProvider>
<Capture />
</WebSocketProvider>
</AccountContext.Provider>,
);
await waitFor(() => expect(MockWebSocket.instances.length).toBe(1));
await act(async () => {
MockWebSocket.instances[0].open();
});
const before = MockWebSocket.instances[0].sent.length;
await act(async () => {
expect(sendMessage(' ', 1, null, null, 'FAST')).toBe(false);
expect(sendMessage('', 1, null, null, 'FAST')).toBe(false);
});
expect(MockWebSocket.instances[0].sent.length).toBe(before);
});
}); });
@@ -273,6 +273,7 @@ const VisuallyHiddenInput = styled.input`
const validationSchema = Yup.object().shape({ const validationSchema = Yup.object().shape({
prompt: Yup.string() prompt: Yup.string()
.trim()
.min(1, "Need to have at least one character") .min(1, "Need to have at least one character")
.required("This is required"), .required("This is required"),
}); });
@@ -326,16 +327,20 @@ const AsyncDashboardInner = (): JSX.Element => {
{ prompt, file, fileType, modelName }: PromptValues, { prompt, file, fileType, modelName }: PromptValues,
{ resetForm }: any, { resetForm }: any,
): Promise<void> => { ): Promise<void> => {
const trimmedPrompt = (prompt ?? "").trim();
if (!trimmedPrompt) {
return;
}
try { try {
const tempConversations: ConversationPrompt[] = [ const tempConversations: ConversationPrompt[] = [
...conversationDetails, ...conversationDetails,
new ConversationPrompt({ message: prompt, user_created: true }), new ConversationPrompt({ message: trimmedPrompt, user_created: true }),
new ConversationPrompt({ message: "", user_created: false }), new ConversationPrompt({ message: "", user_created: false }),
]; ];
conversationRef.current = tempConversations; conversationRef.current = tempConversations;
setConversationDetails(tempConversations); setConversationDetails(tempConversations);
sendMessage(prompt, selectedConversation, file, fileType, modelName); sendMessage(trimmedPrompt, selectedConversation, file, fileType, modelName);
trackEvent(AnalyticsEvents.MESSAGE_SENT, { trackEvent(AnalyticsEvents.MESSAGE_SENT, {
hasConversation: Boolean(selectedConversation), hasConversation: Boolean(selectedConversation),
hasAttachment: Boolean(file), hasAttachment: Boolean(file),