diff --git a/jam-ui/src/store/features/sessionChatSlice.js b/jam-ui/src/store/features/sessionChatSlice.js index 612c11d76..0de7d90c5 100644 --- a/jam-ui/src/store/features/sessionChatSlice.js +++ b/jam-ui/src/store/features/sessionChatSlice.js @@ -1,5 +1,5 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; -import { getChatMessages } from '../../helpers/rest'; +import { getChatMessages, sendChatMessage } from '../../helpers/rest'; /** * Async thunk to fetch chat history for a channel @@ -20,6 +20,28 @@ export const fetchChatHistory = createAsyncThunk( } ); +/** + * Async thunk to send a chat message with optimistic UI updates + * @param {Object} params - Request parameters + * @param {string} params.channel - Channel ID (session ID, lesson ID, or 'global') + * @param {string} params.sessionId - Session ID for session channels + * @param {string} params.message - Message content + * @param {string} params.optimisticId - Temporary ID for optimistic update + * @param {string} params.userId - Current user ID + * @param {string} params.userName - Current user name + */ +export const sendMessage = createAsyncThunk( + 'sessionChat/sendMessage', + async ({ channel, sessionId, message }) => { + const response = await sendChatMessage({ + channel: sessionId ? 'session' : 'global', + sessionId, + message + }); + return response; + } +); + /** * Initial state for session chat * @type {Object} @@ -198,6 +220,66 @@ const sessionChatSlice = createSlice({ const channel = action.meta.arg.channel; state.fetchStatus[channel] = 'failed'; state.fetchError[channel] = action.error.message; + }) + // sendMessage pending - optimistic update + .addCase(sendMessage.pending, (state, action) => { + state.sendStatus = 'loading'; + state.sendError = null; + + // Optimistic update: add message immediately + const { channel, message, optimisticId, userId, userName } = action.meta.arg; + if (!state.messagesByChannel[channel]) { + state.messagesByChannel[channel] = []; + } + + state.messagesByChannel[channel].push({ + id: optimisticId, + senderId: userId, + senderName: userName, + message, + createdAt: new Date().toISOString(), + channel, + isOptimistic: true + }); + }) + // sendMessage fulfilled - replace optimistic message + .addCase(sendMessage.fulfilled, (state, action) => { + state.sendStatus = 'succeeded'; + state.sendError = null; + + // Replace optimistic message with real one + const { channel, optimisticId } = action.meta.arg; + const realMessage = action.payload.message; + + const messages = state.messagesByChannel[channel]; + if (messages) { + const index = messages.findIndex(m => m.id === optimisticId); + if (index !== -1) { + messages[index] = { + id: realMessage.id, + senderId: realMessage.sender_id, + senderName: realMessage.sender_name, + message: realMessage.message, + createdAt: realMessage.created_at, + channel: realMessage.channel + }; + } + } + }) + // sendMessage rejected - remove optimistic message + .addCase(sendMessage.rejected, (state, action) => { + state.sendStatus = 'failed'; + state.sendError = action.error.message; + + // Remove optimistic message on failure + const { channel, optimisticId } = action.meta.arg; + const messages = state.messagesByChannel[channel]; + if (messages) { + const index = messages.findIndex(m => m.id === optimisticId); + if (index !== -1) { + messages.splice(index, 1); + } + } }); } });