|
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; |
|
|