integrated stream for shared(prompt) conv

This commit is contained in:
ManishMadan2882
2024-07-27 02:42:33 +05:30
parent db7c001076
commit a0dd8f8e0f
4 changed files with 310 additions and 25 deletions

View File

@@ -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();

View File

@@ -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();
identifier && dispatch(setIdentifier(identifier));
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>

View File

@@ -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,
};
});
}

View File

@@ -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) =>