import { createSlice, createAsyncThunk, PayloadAction } from '@reduxjs/toolkit'; import { ChatState, Message, ConversationStatus } from '../../types'; import { fetchMessages, sendMessage } from '../../api/zozochat'; import { authExpired } from "../../features/auth/authSlice"; // ---------- THUNKS ---------- type FetchMessagesResult = { conversation_status: ConversationStatus; messages: Message[]; }; // Load all messages (REST) export const fetchChatMessages = createAsyncThunk('chat/fetchMessages', async (_arg, thunkAPI) => { try { const { conversation_status, messages } = await fetchMessages(); return { conversation_status, messages }; } catch (e: any) { const status = e?.status; const code = e?.code; if (status === 401) { if (code === 'expired') { return thunkAPI.rejectWithValue('SESSION_EXPIRED'); } if (code === 'no_session') { return thunkAPI.rejectWithValue('Session expired'); } if (code === 'invalid_session') { return thunkAPI.rejectWithValue('INVALID_SESSION'); } thunkAPI.dispatch(clearChat()); // optional - to close the chat UI return thunkAPI.rejectWithValue('UNAUTHORIZED'); } return thunkAPI.rejectWithValue(e?.message ?? 'Failed to fetch messages'); } }); // sendMessage REST export const sendChatMessage = createAsyncThunk< void, { conversationId: number; email: string; content: string }, { rejectValue: string } >('chat/sendMessage', async (args, thunkAPI) => { try { await sendMessage(args.conversationId, args.email, args.content); } catch (e: any) { return thunkAPI.rejectWithValue(e.message); } }); // ---------- INITIAL STATE ---------- const initialState: ChatState = { messages: [], email: null, conversationId: null, conversationStatus: "pending_approval", chatStatus: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' error: null, }; // ---------- SLICE ---------- const chatSlice = createSlice({ name: 'chat', initialState, reducers: { // Received multiple messages (batch, e.g. {type:'messages'}) messagesReceived(state, action: PayloadAction) { state.messages = action.payload.slice().sort( (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() ); }, // Received a single message (e.g. {type:'message'}) messageReceived(state, action: PayloadAction) { const newMessage = action.payload; // 1. Check if the message already exists in the state by ID const isDuplicate = state.messages.some(m => m.id === newMessage.id); // 2. Only add if it's new if (!isDuplicate) { state.messages = [...state.messages, newMessage].sort( (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() ); } }, // Clear chat on end-session clearChat(state) { return initialState; }, }, extraReducers: (builder) => { builder // --- fetch messages --- .addCase(fetchChatMessages.pending, (state) => { state.chatStatus = 'loading'; state.error = null; }) .addCase(fetchChatMessages.fulfilled, (state, action) => { state.chatStatus = 'succeeded'; state.error = null; state.conversationStatus = action.payload.conversation_status; state.messages = action.payload.messages; }) .addCase(fetchChatMessages.rejected, (state, action) => { state.chatStatus = 'failed'; const reason = action.payload; if (reason === 'SESSION_EXPIRED') { state.error = 'Session expired'; } else if (reason === 'NO_SESSION' || reason === 'INVALID_SESSION') { state.error = 'Session ended'; } else { state.error = reason ?? 'Failed'; } }) // --- send message --- .addCase(sendChatMessage.pending, (state) => { state.chatStatus = 'loading'; state.error = null; }) .addCase(sendChatMessage.fulfilled, (state) => { state.chatStatus = 'succeeded'; state.error = null; }) .addCase(sendChatMessage.rejected, (state, action) => { state.chatStatus = 'failed'; state.error = action.payload ?? 'Failed to send message'; }); }, }); // ---------- EXPORTS ---------- export const { messagesReceived, messageReceived, clearChat } = chatSlice.actions; export default chatSlice.reducer;