Reject empty/whitespace chat messages before send (#52)
## Summary - Closes #51 - Trim prompt Yup validation; refuse empty/whitespace in submit handlers and `sendMessage` - Add WebSocketContext test for empty send rejection ## Test plan - [x] `WebSocketContext.test.js` empty/whitespace send cases - [ ] Manually confirm whitespace-only input cannot submit on desktop + mobile viewport - [ ] Confirm normal messages still send Related BE: https://git.aimloperations.com/ai_ml_operations/chat_backend/issues/31Reviewed-on: #52
This commit was merged in pull request #52.
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user