mirror of
https://github.com/arc53/DocsGPT.git
synced 2025-11-29 08:33:20 +00:00
integrated stream for shared(prompt) conv
This commit is contained in:
@@ -10,7 +10,7 @@ import { useState } from 'react';
|
||||
import Setting from './settings';
|
||||
import './locale/i18n';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import SharedConversation from './conversation/SharedConversation';
|
||||
import { SharedConversation } from './conversation/SharedConversation';
|
||||
import { useDarkTheme } from './hooks';
|
||||
inject();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Query } from './conversationModels';
|
||||
import { Fragment, useEffect, useRef } from 'react';
|
||||
import { Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
@@ -7,9 +7,19 @@ import conversationService from '../api/services/conversationService';
|
||||
import ConversationBubble from './ConversationBubble';
|
||||
import Send from '../assets/send.svg';
|
||||
import Spinner from '../assets/spinner.svg';
|
||||
import { selectClientAPIKey, setClientApiKey } from './sharedConversationSlice';
|
||||
import {
|
||||
selectClientAPIKey,
|
||||
setClientApiKey,
|
||||
updateQuery,
|
||||
addQuery,
|
||||
fetchSharedAnswer,
|
||||
selectStatus,
|
||||
} from './sharedConversationSlice';
|
||||
import { setIdentifier, setFetchedData } from './sharedConversationSlice';
|
||||
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { AppDispatch } from '../store';
|
||||
|
||||
import {
|
||||
selectDate,
|
||||
selectTitle,
|
||||
@@ -17,19 +27,39 @@ import {
|
||||
} from './sharedConversationSlice';
|
||||
import { useSelector } from 'react-redux';
|
||||
const apiHost = import.meta.env.VITE_API_HOST || 'https://docsapi.arc53.com';
|
||||
const SharedConversation = () => {
|
||||
const params = useParams();
|
||||
|
||||
export const SharedConversation = () => {
|
||||
const navigate = useNavigate();
|
||||
const { identifier } = params; //identifier is a uuid, not conversationId
|
||||
const { identifier } = useParams(); //identifier is a uuid, not conversationId
|
||||
|
||||
const queries = useSelector(selectQueries);
|
||||
const title = useSelector(selectTitle);
|
||||
const date = useSelector(selectDate);
|
||||
const apiKey = useSelector(selectClientAPIKey);
|
||||
const status = useSelector(selectStatus);
|
||||
|
||||
const inputRef = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch<AppDispatch>();
|
||||
|
||||
const [lastQueryReturnedErr, setLastQueryReturnedErr] = useState(false);
|
||||
const [eventInterrupt, setEventInterrupt] = useState(false);
|
||||
const endMessageRef = useRef<HTMLDivElement>(null);
|
||||
const handleUserInterruption = () => {
|
||||
if (!eventInterrupt && status === 'loading') setEventInterrupt(true);
|
||||
};
|
||||
useEffect(() => {
|
||||
!eventInterrupt && scrollIntoView();
|
||||
}, [queries.length, queries[queries.length - 1]]);
|
||||
|
||||
useEffect(() => {
|
||||
identifier && dispatch(setIdentifier(identifier));
|
||||
const element = document.getElementById('inputbox') as HTMLInputElement;
|
||||
if (element) {
|
||||
element.focus();
|
||||
}
|
||||
}, []);
|
||||
|
||||
function formatISODate(isoDateStr: string) {
|
||||
const date = new Date(isoDateStr);
|
||||
|
||||
@@ -62,7 +92,21 @@ const SharedConversation = () => {
|
||||
const formattedDate = `Published ${month} ${day}, ${year} at ${hours}:${minutesStr} ${ampm}`;
|
||||
return formattedDate;
|
||||
}
|
||||
const fetchQueris = () => {
|
||||
useEffect(() => {
|
||||
if (queries.length) {
|
||||
queries[queries.length - 1].error && setLastQueryReturnedErr(true);
|
||||
queries[queries.length - 1].response && setLastQueryReturnedErr(false); //considering a query that initially returned error can later include a response property on retry
|
||||
}
|
||||
}, [queries[queries.length - 1]]);
|
||||
|
||||
const scrollIntoView = () => {
|
||||
endMessageRef?.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
});
|
||||
};
|
||||
|
||||
const fetchQueries = () => {
|
||||
identifier &&
|
||||
conversationService
|
||||
.getSharedConversation(identifier || '')
|
||||
@@ -113,8 +157,44 @@ const SharedConversation = () => {
|
||||
}
|
||||
return responseView;
|
||||
};
|
||||
const handleQuestionSubmission = () => {
|
||||
if (inputRef.current?.textContent && status !== 'loading') {
|
||||
if (lastQueryReturnedErr) {
|
||||
// update last failed query with new prompt
|
||||
dispatch(
|
||||
updateQuery({
|
||||
index: queries.length - 1,
|
||||
query: {
|
||||
prompt: inputRef.current.textContent,
|
||||
},
|
||||
}),
|
||||
);
|
||||
handleQuestion({
|
||||
question: queries[queries.length - 1].prompt,
|
||||
isRetry: true,
|
||||
});
|
||||
} else {
|
||||
handleQuestion({ question: inputRef.current.textContent });
|
||||
}
|
||||
inputRef.current.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuestion = ({
|
||||
question,
|
||||
isRetry = false,
|
||||
}: {
|
||||
question: string;
|
||||
isRetry?: boolean;
|
||||
}) => {
|
||||
question = question.trim();
|
||||
if (question === '') return;
|
||||
setEventInterrupt(false);
|
||||
!isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
|
||||
dispatch(fetchSharedAnswer({ question: '' }));
|
||||
};
|
||||
useEffect(() => {
|
||||
fetchQueris();
|
||||
fetchQueries();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -169,6 +249,7 @@ const SharedConversation = () => {
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleQuestionSubmission();
|
||||
}
|
||||
}}
|
||||
></div>
|
||||
@@ -180,6 +261,7 @@ const SharedConversation = () => {
|
||||
) : (
|
||||
<div className="mx-1 cursor-pointer rounded-full p-3 text-center hover:bg-gray-3000">
|
||||
<img
|
||||
onClick={handleQuestionSubmission}
|
||||
className="ml-[4px] h-6 w-6 text-white filter dark:invert"
|
||||
src={Send}
|
||||
></img>
|
||||
|
||||
@@ -212,7 +212,7 @@ export function handleSendFeedback(
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchSharedAnswerSteaming( //for shared conversations
|
||||
export function handleFetchSharedAnswerStreaming( //for shared conversations
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
apiKey: string,
|
||||
@@ -224,19 +224,13 @@ export function fetchSharedAnswerSteaming( //for shared conversations
|
||||
});
|
||||
|
||||
return new Promise<Answer>((resolve, reject) => {
|
||||
const body = {
|
||||
const payload = {
|
||||
question: question,
|
||||
history: JSON.stringify(history),
|
||||
apiKey: apiKey,
|
||||
api_key: apiKey,
|
||||
};
|
||||
fetch(apiHost + '/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
})
|
||||
conversationService
|
||||
.answerStream(payload, signal)
|
||||
.then((response) => {
|
||||
if (!response.body) throw Error('No response body');
|
||||
|
||||
@@ -284,3 +278,48 @@ export function fetchSharedAnswerSteaming( //for shared conversations
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function handleFetchSharedAnswer(
|
||||
question: string,
|
||||
signal: AbortSignal,
|
||||
apiKey: string,
|
||||
): Promise<
|
||||
| {
|
||||
result: any;
|
||||
answer: any;
|
||||
sources: any;
|
||||
query: string;
|
||||
}
|
||||
| {
|
||||
result: any;
|
||||
answer: any;
|
||||
sources: any;
|
||||
query: string;
|
||||
title: any;
|
||||
}
|
||||
> {
|
||||
return conversationService
|
||||
.answer(
|
||||
{
|
||||
question: question,
|
||||
api_key: apiKey,
|
||||
},
|
||||
signal,
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
} else {
|
||||
return Promise.reject(new Error(response.statusText));
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
const result = data.answer;
|
||||
return {
|
||||
answer: result,
|
||||
query: question,
|
||||
result,
|
||||
sources: data.sources,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import store from '../store';
|
||||
import { Query, Status } from '../conversation/conversationModels';
|
||||
import { Query, Status, Answer } from '../conversation/conversationModels';
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import {
|
||||
handleFetchSharedAnswer,
|
||||
handleFetchSharedAnswerStreaming,
|
||||
} from './conversationHandlers';
|
||||
|
||||
const API_STREAMING = import.meta.env.VITE_API_STREAMING === 'true';
|
||||
interface SharedConversationsType {
|
||||
queries: Query[];
|
||||
apiKey?: string;
|
||||
@@ -18,6 +24,85 @@ const initialState: SharedConversationsType = {
|
||||
status: 'idle',
|
||||
};
|
||||
|
||||
export const fetchSharedAnswer = createAsyncThunk<Answer, { question: string }>(
|
||||
'shared/fetchAnswer',
|
||||
async ({ question }, { dispatch, getState, signal }) => {
|
||||
console.log('bulaya sahab ji ?');
|
||||
const state = getState() as RootState;
|
||||
if (state.preference && state.sharedConversation.apiKey) {
|
||||
if (API_STREAMING) {
|
||||
await handleFetchSharedAnswerStreaming(
|
||||
question,
|
||||
signal,
|
||||
state.sharedConversation.apiKey,
|
||||
state.sharedConversation.queries,
|
||||
|
||||
(event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
// check if the 'end' event has been received
|
||||
if (data.type === 'end') {
|
||||
// set status to 'idle'
|
||||
dispatch(sharedConversationSlice.actions.setStatus('idle'));
|
||||
} else if (data.type === 'error') {
|
||||
// set status to 'failed'
|
||||
dispatch(sharedConversationSlice.actions.setStatus('failed'));
|
||||
dispatch(
|
||||
sharedConversationSlice.actions.raiseError({
|
||||
index: state.conversation.queries.length - 1,
|
||||
message: data.error,
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const result = data.answer;
|
||||
dispatch(
|
||||
updateStreamingQuery({
|
||||
index: state.sharedConversation.queries.length - 1,
|
||||
query: { response: result },
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const answer = await handleFetchSharedAnswer(
|
||||
question,
|
||||
signal,
|
||||
state.sharedConversation.apiKey,
|
||||
);
|
||||
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.sharedConversation.queries.length - 1,
|
||||
query: { response: answer.answer, sources: sourcesPrepped },
|
||||
}),
|
||||
);
|
||||
dispatch(sharedConversationSlice.actions.setStatus('idle'));
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
conversationId: null,
|
||||
title: null,
|
||||
answer: '',
|
||||
query: question,
|
||||
result: '',
|
||||
sources: [],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const sharedConversationSlice = createSlice({
|
||||
name: 'sharedConversation',
|
||||
initialState,
|
||||
@@ -38,7 +123,11 @@ export const sharedConversationSlice = createSlice({
|
||||
}>,
|
||||
) {
|
||||
const { queries, title, identifier, date } = action.payload;
|
||||
state.queries = queries;
|
||||
const previousQueriesStr = localStorage.getItem(identifier);
|
||||
const localySavedQueries: Query[] = previousQueriesStr
|
||||
? JSON.parse(previousQueriesStr)
|
||||
: [];
|
||||
state.queries = [...queries, ...localySavedQueries];
|
||||
state.title = title;
|
||||
state.date = date;
|
||||
state.identifier = identifier;
|
||||
@@ -46,11 +135,86 @@ export const sharedConversationSlice = createSlice({
|
||||
setClientApiKey(state, action: PayloadAction<string>) {
|
||||
state.apiKey = action.payload;
|
||||
},
|
||||
addQuery(state, action: PayloadAction<Query>) {
|
||||
state.queries.push(action.payload);
|
||||
if (state.identifier) {
|
||||
const previousQueriesStr = localStorage.getItem(state.identifier);
|
||||
previousQueriesStr
|
||||
? localStorage.setItem(
|
||||
state.identifier,
|
||||
JSON.stringify([
|
||||
...JSON.parse(previousQueriesStr),
|
||||
action.payload,
|
||||
]),
|
||||
)
|
||||
: localStorage.setItem(
|
||||
state.identifier,
|
||||
JSON.stringify([action.payload]),
|
||||
);
|
||||
if (action.payload.prompt) {
|
||||
fetchSharedAnswer({ question: action.payload.prompt });
|
||||
}
|
||||
}
|
||||
},
|
||||
updateStreamingQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
if (query.response != undefined) {
|
||||
state.queries[index].response =
|
||||
(state.queries[index].response || '') + query.response;
|
||||
} else {
|
||||
state.queries[index] = {
|
||||
...state.queries[index],
|
||||
...query,
|
||||
};
|
||||
}
|
||||
},
|
||||
updateQuery(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; query: Partial<Query> }>,
|
||||
) {
|
||||
const { index, query } = action.payload;
|
||||
state.queries[index] = {
|
||||
...state.queries[index],
|
||||
...query,
|
||||
};
|
||||
},
|
||||
raiseError(
|
||||
state,
|
||||
action: PayloadAction<{ index: number; message: string }>,
|
||||
) {
|
||||
const { index, message } = action.payload;
|
||||
state.queries[index].error = message;
|
||||
},
|
||||
},
|
||||
extraReducers(builder) {
|
||||
builder
|
||||
.addCase(fetchSharedAnswer.pending, (state) => {
|
||||
state.status = 'loading';
|
||||
})
|
||||
.addCase(fetchSharedAnswer.rejected, (state, action) => {
|
||||
if (action.meta.aborted) {
|
||||
state.status = 'idle';
|
||||
return state;
|
||||
}
|
||||
state.status = 'failed';
|
||||
state.queries[state.queries.length - 1].error =
|
||||
'Something went wrong. Please check your internet connection.';
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { setStatus, setIdentifier, setFetchedData, setClientApiKey } =
|
||||
sharedConversationSlice.actions;
|
||||
export const {
|
||||
setStatus,
|
||||
setIdentifier,
|
||||
setFetchedData,
|
||||
setClientApiKey,
|
||||
updateQuery,
|
||||
updateStreamingQuery,
|
||||
addQuery,
|
||||
} = sharedConversationSlice.actions;
|
||||
|
||||
export const selectStatus = (state: RootState) => state.conversation.status;
|
||||
export const selectClientAPIKey = (state: RootState) =>
|
||||
|
||||
Reference in New Issue
Block a user