diff --git a/frontend/src/components/MessageInput.tsx b/frontend/src/components/MessageInput.tsx index 046f46b3..1db5c2c8 100644 --- a/frontend/src/components/MessageInput.tsx +++ b/frontend/src/components/MessageInput.tsx @@ -18,7 +18,7 @@ import { removeAttachment, selectAttachments, updateAttachment, -} from '../conversation/conversationSlice'; +} from '../upload/uploadSlice'; import { useDarkTheme } from '../hooks'; import { ActiveState } from '../models/misc'; import { diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index 3d0d5ac2..a8d86049 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -27,8 +27,11 @@ import { setConversation, updateConversationId, updateQuery, - selectAttachments, } from './conversationSlice'; +import { + selectCompletedAttachments, + clearAttachments, +} from '../upload/uploadSlice'; export default function Conversation() { const { t } = useTranslation(); @@ -40,7 +43,7 @@ export default function Conversation() { const status = useSelector(selectStatus); const conversationId = useSelector(selectConversationId); const selectedAgent = useSelector(selectSelectedAgent); - const attachments = useSelector(selectAttachments); + const completedAttachments = useSelector(selectCompletedAttachments); const [uploadModalState, setUploadModalState] = useState('INACTIVE'); @@ -109,8 +112,8 @@ export default function Conversation() { const trimmedQuestion = question.trim(); if (trimmedQuestion === '') return; - const filesAttached = attachments - .filter((a) => a.id && a.status === 'completed') + const filesAttached = completedAttachments + .filter((a) => a.id) .map((a) => ({ id: a.id as string, fileName: a.fileName })); if (index !== undefined) { @@ -127,7 +130,7 @@ export default function Conversation() { handleFetchAnswer({ question: trimmedQuestion, index }); } }, - [dispatch, handleFetchAnswer, attachments], + [dispatch, handleFetchAnswer, completedAttachments], ); const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => { @@ -190,6 +193,7 @@ export default function Conversation() { query: { conversationId: null }, }), ); + dispatch(clearAttachments()); }; useEffect(() => { diff --git a/frontend/src/conversation/SharedConversation.tsx b/frontend/src/conversation/SharedConversation.tsx index 3627c048..22efae4e 100644 --- a/frontend/src/conversation/SharedConversation.tsx +++ b/frontend/src/conversation/SharedConversation.tsx @@ -23,6 +23,7 @@ import { setIdentifier, updateQuery, } from './sharedConversationSlice'; +import { selectCompletedAttachments } from '../upload/uploadSlice'; export const SharedConversation = () => { const navigate = useNavigate(); @@ -34,6 +35,7 @@ export const SharedConversation = () => { const date = useSelector(selectDate); const apiKey = useSelector(selectClientAPIKey); const status = useSelector(selectStatus); + const completedAttachments = useSelector(selectCompletedAttachments); const { t } = useTranslation(); const dispatch = useDispatch(); @@ -106,7 +108,19 @@ export const SharedConversation = () => { }) => { question = question.trim(); if (question === '') return; - !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries + + const filesAttached = completedAttachments + .filter((a) => a.id) + .map((a) => ({ id: a.id as string, fileName: a.fileName })); + + !isRetry && + dispatch( + addQuery({ + prompt: question, + attachments: filesAttached, + }), + ); //dispatch only new queries + dispatch(fetchSharedAnswer({ question })); }; useEffect(() => { diff --git a/frontend/src/conversation/conversationHandlers.ts b/frontend/src/conversation/conversationHandlers.ts index 8aa7e424..981dffc3 100644 --- a/frontend/src/conversation/conversationHandlers.ts +++ b/frontend/src/conversation/conversationHandlers.ts @@ -284,6 +284,7 @@ export function handleFetchSharedAnswerStreaming( //for shared conversations signal: AbortSignal, apiKey: string, history: Array = [], + attachments: string[] = [], // Add attachments parameter with default empty array onEvent: (event: MessageEvent) => void, ): Promise { history = history.map((item) => { @@ -300,6 +301,7 @@ export function handleFetchSharedAnswerStreaming( //for shared conversations history: JSON.stringify(history), api_key: apiKey, save_conversation: false, + attachments: attachments.length > 0 ? attachments : undefined, // Add attachments to payload }; conversationService .answerStream(payload, null, signal) @@ -355,6 +357,7 @@ export function handleFetchSharedAnswer( question: string, signal: AbortSignal, apiKey: string, + attachments?: string[], ): Promise< | { result: any; @@ -370,15 +373,18 @@ export function handleFetchSharedAnswer( title: any; } > { + const payload = { + question: question, + api_key: apiKey, + }; + + // Add attachments to payload if they exist + if (attachments && attachments.length > 0) { + payload.attachments = attachments; + } + return conversationService - .answer( - { - question: question, - api_key: apiKey, - }, - null, - signal, - ) + .answer(payload, null, signal) .then((response) => { if (response.ok) { return response.json(); diff --git a/frontend/src/conversation/conversationModels.ts b/frontend/src/conversation/conversationModels.ts index f0ccca05..b16dd6c1 100644 --- a/frontend/src/conversation/conversationModels.ts +++ b/frontend/src/conversation/conversationModels.ts @@ -22,7 +22,6 @@ export interface ConversationState { queries: Query[]; status: Status; conversationId: string | null; - attachments: Attachment[]; } export interface Answer { diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts index 961260ea..081d5871 100644 --- a/frontend/src/conversation/conversationSlice.ts +++ b/frontend/src/conversation/conversationSlice.ts @@ -3,23 +3,17 @@ import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; import { getConversations } from '../preferences/preferenceApi'; import { setConversations } from '../preferences/preferenceSlice'; import store from '../store'; +import { selectCompletedAttachments } from '../upload/uploadSlice'; import { handleFetchAnswer, handleFetchAnswerSteaming, } from './conversationHandlers'; -import { - Answer, - Query, - Status, - ConversationState, - Attachment, -} from './conversationModels'; +import { Answer, Query, Status, ConversationState } from './conversationModels'; const initialState: ConversationState = { queries: [], status: 'idle', conversationId: null, - attachments: [], }; const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true'; @@ -44,8 +38,8 @@ export const fetchAnswer = createAsyncThunk< let isSourceUpdated = false; const state = getState() as RootState; - const attachmentIds = state.conversation.attachments - .filter((a) => a.id && a.status === 'completed') + const attachmentIds = selectCompletedAttachments(state) + .filter((a) => a.id) .map((a) => a.id) as string[]; const currentConversationId = state.conversation.conversationId; const conversationIdToSend = isPreview ? null : currentConversationId; @@ -307,39 +301,11 @@ export const conversationSlice = createSlice({ const { index, message } = action.payload; state.queries[index].error = message; }, - setAttachments: (state, action: PayloadAction) => { - state.attachments = action.payload; - }, - addAttachment: (state, action: PayloadAction) => { - state.attachments.push(action.payload); - }, - updateAttachment: ( - state, - action: PayloadAction<{ - taskId: string; - updates: Partial; - }>, - ) => { - const index = state.attachments.findIndex( - (att) => att.taskId === action.payload.taskId, - ); - if (index !== -1) { - state.attachments[index] = { - ...state.attachments[index], - ...action.payload.updates, - }; - } - }, - removeAttachment: (state, action: PayloadAction) => { - state.attachments = state.attachments.filter( - (att) => att.taskId !== action.payload && att.id !== action.payload, - ); - }, + resetConversation: (state) => { state.queries = initialState.queries; state.status = initialState.status; state.conversationId = initialState.conversationId; - state.attachments = initialState.attachments; handleAbort(); }, }, @@ -365,11 +331,6 @@ export const selectQueries = (state: RootState) => state.conversation.queries; export const selectStatus = (state: RootState) => state.conversation.status; -export const selectAttachments = (state: RootState) => - state.conversation.attachments; -export const selectCompletedAttachments = (state: RootState) => - state.conversation.attachments.filter((att) => att.status === 'completed'); - export const { addQuery, updateQuery, @@ -380,10 +341,8 @@ export const { updateStreamingSource, updateToolCalls, setConversation, - setAttachments, - addAttachment, - updateAttachment, - removeAttachment, + setStatus, + raiseError, resetConversation, } = conversationSlice.actions; export default conversationSlice.reducer; diff --git a/frontend/src/conversation/sharedConversationSlice.ts b/frontend/src/conversation/sharedConversationSlice.ts index f0482fa5..ae295412 100644 --- a/frontend/src/conversation/sharedConversationSlice.ts +++ b/frontend/src/conversation/sharedConversationSlice.ts @@ -7,6 +7,7 @@ import { handleFetchSharedAnswer, handleFetchSharedAnswerStreaming, } from './conversationHandlers'; +import { selectCompletedAttachments } from '../upload/uploadSlice'; const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true'; interface SharedConversationsType { @@ -29,6 +30,10 @@ export const fetchSharedAnswer = createAsyncThunk( async ({ question }, { dispatch, getState, signal }) => { const state = getState() as RootState; + const attachmentIds = selectCompletedAttachments(state) + .filter((a) => a.id) + .map((a) => a.id) as string[]; + if (state.preference && state.sharedConversation.apiKey) { if (API_STREAMING) { await handleFetchSharedAnswerStreaming( @@ -36,7 +41,7 @@ export const fetchSharedAnswer = createAsyncThunk( signal, state.sharedConversation.apiKey, state.sharedConversation.queries, - + attachmentIds, (event) => { const data = JSON.parse(event.data); // check if the 'end' event has been received @@ -92,6 +97,7 @@ export const fetchSharedAnswer = createAsyncThunk( question, signal, state.sharedConversation.apiKey, + attachmentIds, ); if (answer) { let sourcesPrepped = []; diff --git a/frontend/src/store.ts b/frontend/src/store.ts index f26ef3d2..1f25606d 100644 --- a/frontend/src/store.ts +++ b/frontend/src/store.ts @@ -7,6 +7,7 @@ import { prefListenerMiddleware, prefSlice, } from './preferences/preferenceSlice'; +import uploadReducer from './upload/uploadSlice'; const key = localStorage.getItem('DocsGPTApiKey'); const prompt = localStorage.getItem('DocsGPTPrompt'); @@ -52,6 +53,7 @@ const store = configureStore({ preference: prefSlice.reducer, conversation: conversationSlice.reducer, sharedConversation: sharedConversationSlice.reducer, + upload: uploadReducer, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(prefListenerMiddleware.middleware), diff --git a/frontend/src/upload/uploadSlice.ts b/frontend/src/upload/uploadSlice.ts new file mode 100644 index 00000000..fff56dbd --- /dev/null +++ b/frontend/src/upload/uploadSlice.ts @@ -0,0 +1,67 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { RootState } from '../store'; + +export interface Attachment { + fileName: string; + progress: number; + status: 'uploading' | 'processing' | 'completed' | 'failed'; + taskId: string; + id?: string; + token_count?: number; +} + +interface UploadState { + attachments: Attachment[]; +} + +const initialState: UploadState = { + attachments: [], +}; + +export const uploadSlice = createSlice({ + name: 'upload', + initialState, + reducers: { + addAttachment: (state, action: PayloadAction) => { + state.attachments.push(action.payload); + }, + updateAttachment: ( + state, + action: PayloadAction<{ + taskId: string; + updates: Partial; + }>, + ) => { + const index = state.attachments.findIndex( + (att) => att.taskId === action.payload.taskId, + ); + if (index !== -1) { + state.attachments[index] = { + ...state.attachments[index], + ...action.payload.updates, + }; + } + }, + removeAttachment: (state, action: PayloadAction) => { + state.attachments = state.attachments.filter( + (att) => att.taskId !== action.payload && att.id !== action.payload, + ); + }, + clearAttachments: (state) => { + state.attachments = []; + }, + }, +}); + +export const { + addAttachment, + updateAttachment, + removeAttachment, + clearAttachments, +} = uploadSlice.actions; + +export const selectAttachments = (state: RootState) => state.upload.attachments; +export const selectCompletedAttachments = (state: RootState) => + state.upload.attachments.filter((att) => att.status === 'completed'); + +export default uploadSlice.reducer;