| | import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; |
| | import store from '../store'; |
| | import { fetchAnswerApi, fetchAnswerSteaming } from './conversationApi'; |
| | import { searchEndpoint } from './conversationApi'; |
| | import { Answer, ConversationState, Query, Status } from './conversationModels'; |
| | import { getConversations } from '../preferences/preferenceApi'; |
| | import { setConversations } from '../preferences/preferenceSlice'; |
| |
|
| | const initialState: ConversationState = { |
| | queries: [], |
| | status: 'idle', |
| | conversationId: null, |
| | }; |
| |
|
| | const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true'; |
| |
|
| | export const fetchAnswer = createAsyncThunk<Answer, { question: string }>( |
| | 'fetchAnswer', |
| | async ({ question }, { dispatch, getState }) => { |
| | const state = getState() as RootState; |
| | if (state.preference) { |
| | if (API_STREAMING) { |
| | await fetchAnswerSteaming( |
| | question, |
| | state.preference.apiKey, |
| | state.preference.selectedDocs!, |
| | state.conversation.queries, |
| | state.conversation.conversationId, |
| | state.preference.prompt.id, |
| | (event) => { |
| | const data = JSON.parse(event.data); |
| |
|
| |
|
| | |
| | if (data.type === 'end') { |
| | |
| | dispatch(conversationSlice.actions.setStatus('idle')); |
| | getConversations() |
| | .then((fetchedConversations) => { |
| | dispatch(setConversations(fetchedConversations)); |
| | }) |
| | .catch((error) => { |
| | console.error('Failed to fetch conversations: ', error); |
| | }); |
| |
|
| | searchEndpoint( |
| | question, |
| | state.preference.apiKey, |
| | state.preference.selectedDocs!, |
| | state.conversation.conversationId, |
| | state.conversation.queries |
| | ).then(sources => { |
| | |
| | dispatch( |
| | updateStreamingSource({ |
| | index: state.conversation.queries.length - 1, |
| | query: { sources }, |
| | }), |
| | ); |
| | }); |
| | } else if (data.type === 'id') { |
| | dispatch( |
| | updateConversationId({ |
| | query: { conversationId: data.id }, |
| | }), |
| | ); |
| | } else { |
| | const result = data.answer; |
| | dispatch( |
| | updateStreamingQuery({ |
| | index: state.conversation.queries.length - 1, |
| | query: { response: result }, |
| | }), |
| | ); |
| | } |
| | }, |
| | ); |
| | } else { |
| | const answer = await fetchAnswerApi( |
| | question, |
| | state.preference.apiKey, |
| | state.preference.selectedDocs!, |
| | state.conversation.queries, |
| | state.conversation.conversationId, |
| | state.preference.prompt.id, |
| | ); |
| | if (answer) { |
| | let sourcesPrepped = []; |
| | sourcesPrepped = answer.sources.map((source: { title: string }) => { |
| | if (source && source.title) { |
| | const titleParts = source.title.split('/'); |
| | return { |
| | ...source, |
| | title: titleParts[titleParts.length - 1], |
| | }; |
| | } |
| | return source; |
| | }); |
| |
|
| | dispatch( |
| | updateQuery({ |
| | index: state.conversation.queries.length - 1, |
| | query: { response: answer.answer, sources: sourcesPrepped }, |
| | }), |
| | ); |
| | dispatch( |
| | updateConversationId({ |
| | query: { conversationId: answer.conversationId }, |
| | }), |
| | ); |
| | dispatch(conversationSlice.actions.setStatus('idle')); |
| | getConversations() |
| | .then((fetchedConversations) => { |
| | dispatch(setConversations(fetchedConversations)); |
| | }) |
| | .catch((error) => { |
| | console.error('Failed to fetch conversations: ', error); |
| | }); |
| | } |
| | } |
| | } |
| | return { |
| | conversationId: null, |
| | title: null, |
| | answer: '', |
| | query: question, |
| | result: '', |
| | sources: [], |
| | }; |
| | }, |
| | ); |
| |
|
| | export const conversationSlice = createSlice({ |
| | name: 'conversation', |
| | initialState, |
| | reducers: { |
| | addQuery(state, action: PayloadAction<Query>) { |
| | state.queries.push(action.payload); |
| | }, |
| | setConversation(state, action: PayloadAction<Query[]>) { |
| | state.queries = action.payload; |
| | }, |
| | updateStreamingQuery( |
| | state, |
| | action: PayloadAction<{ index: number; query: Partial<Query> }>, |
| | ) { |
| | const { index, query } = action.payload; |
| | if (query.response) { |
| | state.queries[index].response = |
| | (state.queries[index].response || '') + query.response; |
| | } else { |
| | state.queries[index] = { |
| | ...state.queries[index], |
| | ...query, |
| | }; |
| | } |
| | }, |
| | updateConversationId( |
| | state, |
| | action: PayloadAction<{ query: Partial<Query> }>, |
| | ) { |
| | state.conversationId = action.payload.query.conversationId ?? null; |
| | }, |
| | updateStreamingSource( |
| | state, |
| | action: PayloadAction<{ index: number; query: Partial<Query> }>, |
| | ) { |
| |
|
| | const { index, query } = action.payload; |
| | if (!state.queries[index].sources) { |
| | state.queries[index].sources = query?.sources; |
| | } else { |
| | state.queries[index].sources!.push(query.sources![0]); |
| | } |
| | }, |
| | updateQuery( |
| | state, |
| | action: PayloadAction<{ index: number; query: Partial<Query> }>, |
| | ) { |
| | const { index, query } = action.payload; |
| | state.queries[index] = { |
| | ...state.queries[index], |
| | ...query, |
| | }; |
| | }, |
| | setStatus(state, action: PayloadAction<Status>) { |
| | state.status = action.payload; |
| | }, |
| | }, |
| | extraReducers(builder) { |
| | builder |
| | .addCase(fetchAnswer.pending, (state) => { |
| | state.status = 'loading'; |
| | }) |
| | .addCase(fetchAnswer.rejected, (state, action) => { |
| | state.status = 'failed'; |
| | state.queries[state.queries.length - 1].error = |
| | 'Something went wrong. Please try again later.'; |
| | }); |
| | }, |
| | }); |
| |
|
| | type RootState = ReturnType<typeof store.getState>; |
| |
|
| | export const selectQueries = (state: RootState) => state.conversation.queries; |
| |
|
| | export const selectStatus = (state: RootState) => state.conversation.status; |
| |
|
| | export const { |
| | addQuery, |
| | updateQuery, |
| | updateStreamingQuery, |
| | updateConversationId, |
| | updateStreamingSource, |
| | setConversation, |
| | } = conversationSlice.actions; |
| | export default conversationSlice.reducer; |
| |
|