diff --git a/application/api/answer/routes.py b/application/api/answer/routes.py
index f9ab19be..a413194b 100644
--- a/application/api/answer/routes.py
+++ b/application/api/answer/routes.py
@@ -118,8 +118,19 @@ def is_azure_configured():
)
-def save_conversation(conversation_id, question, response, source_log_docs, llm):
- if conversation_id is not None and conversation_id != "None":
+def save_conversation(conversation_id, question, response, source_log_docs, llm,index=None):
+ if conversation_id is not None and index is not None:
+ conversations_collection.update_one(
+ {"_id": ObjectId(conversation_id), f"queries.{index}": {"$exists": True}},
+ {
+ "$set": {
+ f"queries.{index}.prompt": question,
+ f"queries.{index}.response": response,
+ f"queries.{index}.sources": source_log_docs,
+ }
+ }
+ )
+ elif conversation_id is not None and conversation_id != "None":
conversations_collection.update_one(
{"_id": ObjectId(conversation_id)},
{
@@ -186,7 +197,7 @@ def get_prompt(prompt_id):
def complete_stream(
- question, retriever, conversation_id, user_api_key, isNoneDoc=False
+ question, retriever, conversation_id, user_api_key, isNoneDoc=False,index=None
):
try:
@@ -217,7 +228,7 @@ def complete_stream(
)
if user_api_key is None:
conversation_id = save_conversation(
- conversation_id, question, response_full, source_log_docs, llm
+ conversation_id, question, response_full, source_log_docs, llm,index
)
# send data.type = "end" to indicate that the stream has ended as json
data = json.dumps({"type": "id", "id": str(conversation_id)})
@@ -282,6 +293,9 @@ class Stream(Resource):
"isNoneDoc": fields.Boolean(
required=False, description="Flag indicating if no document is used"
),
+ "index":fields.Integer(
+ required=False, description="The position where query is to be updated"
+ ),
},
)
@@ -290,23 +304,24 @@ class Stream(Resource):
def post(self):
data = request.get_json()
required_fields = ["question"]
-
+ if "index" in data:
+ required_fields = ["question","conversation_id"]
missing_fields = check_required_fields(data, required_fields)
if missing_fields:
return missing_fields
try:
question = data["question"]
- history = data.get("history", [])
- history = json.loads(history)
+ history = str(data.get("history", []))
+ history = str(json.loads(history))
conversation_id = data.get("conversation_id")
prompt_id = data.get("prompt_id", "default")
-
+ index=data.get("index",None)
chunks = int(data.get("chunks", 2))
token_limit = data.get("token_limit", settings.DEFAULT_MAX_HISTORY)
retriever_name = data.get("retriever", "classic")
-
+
if "api_key" in data:
data_key = get_data_from_api_key(data["api_key"])
chunks = int(data_key.get("chunks", 2))
@@ -343,7 +358,7 @@ class Stream(Resource):
gpt_model=gpt_model,
user_api_key=user_api_key,
)
-
+
return Response(
complete_stream(
question=question,
@@ -351,6 +366,7 @@ class Stream(Resource):
conversation_id=conversation_id,
user_api_key=user_api_key,
isNoneDoc=data.get("isNoneDoc"),
+ index=index,
),
mimetype="text/event-stream",
)
diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts
index 4e7112d0..05c8f786 100644
--- a/frontend/src/api/endpoints.ts
+++ b/frontend/src/api/endpoints.ts
@@ -17,7 +17,7 @@ const endpoints = {
TOKEN_ANALYTICS: '/api/get_token_analytics',
FEEDBACK_ANALYTICS: '/api/get_feedback_analytics',
LOGS: `/api/get_user_logs`,
- MANAGE_SYNC: '/api/manage_sync',
+ MANAGE_SYNC: '/api/manage_sync'
},
CONVERSATION: {
ANSWER: '/api/answer',
diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx
index b2f24b5a..ed69064a 100644
--- a/frontend/src/conversation/Conversation.tsx
+++ b/frontend/src/conversation/Conversation.tsx
@@ -15,12 +15,14 @@ import { useDarkTheme, useMediaQuery } from '../hooks';
import { ShareConversationModal } from '../modals/ShareConversationModal';
import { selectConversationId } from '../preferences/preferenceSlice';
import { AppDispatch } from '../store';
+import conversationService from '../api/services/conversationService';
import ConversationBubble from './ConversationBubble';
import { handleSendFeedback } from './conversationHandlers';
import { FEEDBACK, Query } from './conversationModels';
import {
addQuery,
fetchAnswer,
+ resendQuery,
selectQueries,
selectStatus,
setConversation,
@@ -85,15 +87,25 @@ export default function Conversation() {
const handleQuestion = ({
question,
isRetry = false,
+ updated = null,
+ indx = undefined,
}: {
question: string;
isRetry?: boolean;
+ updated?: boolean | null;
+ indx?: number;
}) => {
- question = question.trim();
- if (question === '') return;
- setEventInterrupt(false);
- !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
- fetchStream.current = dispatch(fetchAnswer({ question }));
+ if (updated === true) {
+ !isRetry &&
+ dispatch(resendQuery({ index: indx as number, prompt: question })); //dispatch only new queries
+ fetchStream.current = dispatch(fetchAnswer({ question, indx }));
+ } else {
+ question = question.trim();
+ if (question === '') return;
+ setEventInterrupt(false);
+ !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries
+ fetchStream.current = dispatch(fetchAnswer({ question }));
+ }
};
const handleFeedback = (query: Query, feedback: FEEDBACK, index: number) => {
@@ -104,8 +116,14 @@ export default function Conversation() {
);
};
- const handleQuestionSubmission = () => {
- if (inputRef.current?.value && status !== 'loading') {
+ const handleQuestionSubmission = (
+ updatedQuestion?: string,
+ updated?: boolean,
+ indx?: number,
+ ) => {
+ if (updated === true) {
+ handleQuestion({ question: updatedQuestion as string, updated, indx });
+ } else if (inputRef.current?.value && status !== 'loading') {
if (lastQueryReturnedErr) {
// update last failed query with new prompt
dispatch(
@@ -290,6 +308,8 @@ export default function Conversation() {
key={`${index}QUESTION`}
message={query.prompt}
type="QUESTION"
+ handleUpdatedQuestionSubmission={handleQuestionSubmission}
+ questionNumber={index}
sources={query.sources}
>
@@ -328,7 +348,7 @@ export default function Conversation() {
![]()
handleQuestionSubmission()}
src={isDarkTheme ? SendDark : Send}
>
diff --git a/frontend/src/conversation/ConversationBubble.tsx b/frontend/src/conversation/ConversationBubble.tsx
index 8e5df666..567b09e9 100644
--- a/frontend/src/conversation/ConversationBubble.tsx
+++ b/frontend/src/conversation/ConversationBubble.tsx
@@ -15,6 +15,7 @@ import Document from '../assets/document.svg';
import Like from '../assets/like.svg?react';
import Link from '../assets/link.svg';
import Sources from '../assets/sources.svg';
+import Edit from '../assets/edit.svg';
import Avatar from '../components/Avatar';
import CopyButton from '../components/CopyButton';
import Sidebar from '../components/Sidebar';
@@ -38,37 +39,104 @@ const ConversationBubble = forwardRef<
handleFeedback?: (feedback: FEEDBACK) => void;
sources?: { title: string; text: string; source: string }[];
retryBtn?: React.ReactElement;
+ questionNumber?: number;
+ handleUpdatedQuestionSubmission?: (
+ updatedquestion?: string,
+ updated?: boolean,
+ index?: number,
+ ) => void;
}
>(function ConversationBubble(
- { message, type, className, feedback, handleFeedback, sources, retryBtn },
+ {
+ message,
+ type,
+ className,
+ feedback,
+ handleFeedback,
+ sources,
+ retryBtn,
+ questionNumber,
+ handleUpdatedQuestionSubmission,
+ },
ref,
) {
// const bubbleRef = useRef(null);
const chunks = useSelector(selectChunks);
const selectedDocs = useSelector(selectSelectedDocs);
const [isLikeHovered, setIsLikeHovered] = useState(false);
+ const [isEditClicked, setIsEditClicked] = useState(false);
const [isDislikeHovered, setIsDislikeHovered] = useState(false);
+ const [isQuestionHovered, setIsQuestionHovered] = useState(false);
+ const [editInputBox, setEditInputBox] = useState('');
+
const [isLikeClicked, setIsLikeClicked] = useState(false);
const [isDislikeClicked, setIsDislikeClicked] = useState(false);
const [activeTooltip, setActiveTooltip] = useState(null);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
+ const handleEditClick = () => {
+ setIsEditClicked(false);
+ handleUpdatedQuestionSubmission?.(editInputBox, true, questionNumber);
+ };
let bubble;
if (type === 'QUESTION') {
bubble = (
setIsQuestionHovered(true)}
+ onMouseLeave={() => setIsQuestionHovered(false)}
>
-
- {message}
+
+ {!isEditClicked && (
+
+ {message}
+
+ )}
+ {isEditClicked && (
+
setEditInputBox(e.target.value)}
+ value={editInputBox}
+ className="w-[85%] ml-2 mr-2 rounded-[28px] py-[12px] dark:border-[0.5px] dark:border-white dark:bg-raisin-black dark:text-white px-[18px] border-[1.5px] border-black"
+ />
+ )}
+
+

{
+ setIsEditClicked(true);
+ setEditInputBox(message);
+ }}
+ />
+
+ {isEditClicked && (
+
+
+
+
+ )}
);
} else {
diff --git a/frontend/src/conversation/conversationHandlers.ts b/frontend/src/conversation/conversationHandlers.ts
index e28e4b22..be046bca 100644
--- a/frontend/src/conversation/conversationHandlers.ts
+++ b/frontend/src/conversation/conversationHandlers.ts
@@ -75,6 +75,7 @@ export function handleFetchAnswerSteaming(
chunks: string,
token_limit: number,
onEvent: (event: MessageEvent) => void,
+ indx?: number,
): Promise {
history = history.map((item) => {
return { prompt: item.prompt, response: item.response };
@@ -87,6 +88,7 @@ export function handleFetchAnswerSteaming(
chunks: chunks,
token_limit: token_limit,
isNoneDoc: selectedDocs === null,
+ index: indx,
};
if (selectedDocs && 'id' in selectedDocs) {
payload.active_docs = selectedDocs.id as string;
diff --git a/frontend/src/conversation/conversationModels.ts b/frontend/src/conversation/conversationModels.ts
index 99bb69e6..cd286852 100644
--- a/frontend/src/conversation/conversationModels.ts
+++ b/frontend/src/conversation/conversationModels.ts
@@ -41,4 +41,5 @@ export interface RetrievalPayload {
chunks: string;
token_limit: number;
isNoneDoc: boolean;
+ index?: number;
}
diff --git a/frontend/src/conversation/conversationSlice.ts b/frontend/src/conversation/conversationSlice.ts
index 5a78e014..5e1f9b27 100644
--- a/frontend/src/conversation/conversationSlice.ts
+++ b/frontend/src/conversation/conversationSlice.ts
@@ -25,138 +25,139 @@ export function handleAbort() {
}
}
-export const fetchAnswer = createAsyncThunk(
- 'fetchAnswer',
- async ({ question }, { dispatch, getState }) => {
- if (abortController) {
- abortController.abort();
- }
- abortController = new AbortController();
- const { signal } = abortController;
+export const fetchAnswer = createAsyncThunk<
+ Answer,
+ { question: string; indx?: number }
+>('fetchAnswer', async ({ question, indx }, { dispatch, getState }) => {
+ if (abortController) {
+ abortController.abort();
+ }
+ abortController = new AbortController();
+ const { signal } = abortController;
- let isSourceUpdated = false;
- const state = getState() as RootState;
- if (state.preference) {
- if (API_STREAMING) {
- await handleFetchAnswerSteaming(
- question,
- signal,
- state.preference.selectedDocs!,
- state.conversation.queries,
- state.conversation.conversationId,
- state.preference.prompt.id,
- state.preference.chunks,
- state.preference.token_limit,
+ let isSourceUpdated = false;
+ const state = getState() as RootState;
+ if (state.preference) {
+ if (API_STREAMING) {
+ await handleFetchAnswerSteaming(
+ question,
+ signal,
+ state.preference.selectedDocs!,
+ state.conversation.queries,
+ state.conversation.conversationId,
+ state.preference.prompt.id,
+ state.preference.chunks,
+ state.preference.token_limit,
+ (event) => {
+ const data = JSON.parse(event.data);
- (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);
- });
- if (!isSourceUpdated) {
- dispatch(
- updateStreamingSource({
- index: state.conversation.queries.length - 1,
- query: { sources: [] },
- }),
- );
- }
- } else if (data.type === 'id') {
- dispatch(
- updateConversationId({
- query: { conversationId: data.id },
- }),
- );
- } else if (data.type === 'source') {
- isSourceUpdated = true;
+ if (data.type === 'end') {
+ dispatch(conversationSlice.actions.setStatus('idle'));
+ getConversations()
+ .then((fetchedConversations) => {
+ dispatch(setConversations(fetchedConversations));
+ })
+ .catch((error) => {
+ console.error('Failed to fetch conversations: ', error);
+ });
+ if (!isSourceUpdated) {
dispatch(
updateStreamingSource({
- index: state.conversation.queries.length - 1,
- query: { sources: data.source ?? [] },
- }),
- );
- } else if (data.type === 'error') {
- // set status to 'failed'
- dispatch(conversationSlice.actions.setStatus('failed'));
- dispatch(
- conversationSlice.actions.raiseError({
- index: state.conversation.queries.length - 1,
- message: data.error,
- }),
- );
- } else {
- const result = data.answer;
- dispatch(
- updateStreamingQuery({
- index: state.conversation.queries.length - 1,
- query: { response: result },
+ index: indx ?? state.conversation.queries.length - 1,
+ query: { sources: [] },
}),
);
}
- },
- );
- } else {
- const answer = await handleFetchAnswer(
- question,
- signal,
- state.preference.selectedDocs!,
- state.conversation.queries,
- state.conversation.conversationId,
- state.preference.prompt.id,
- state.preference.chunks,
- state.preference.token_limit,
- );
- 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;
- });
+ } else if (data.type === 'id') {
+ dispatch(
+ updateConversationId({
+ query: { conversationId: data.id },
+ }),
+ );
+ } else if (data.type === 'source') {
+ isSourceUpdated = true;
+ dispatch(
+ updateStreamingSource({
+ index: indx ?? state.conversation.queries.length - 1,
+ query: { sources: data.source ?? [] },
+ }),
+ );
+ } else if (data.type === 'error') {
+ // set status to 'failed'
+ dispatch(conversationSlice.actions.setStatus('failed'));
+ dispatch(
+ conversationSlice.actions.raiseError({
+ index: indx ?? state.conversation.queries.length - 1,
+ message: data.error,
+ }),
+ );
+ } else {
+ const result = data.answer;
+ dispatch(
+ updateStreamingQuery({
+ index: indx ?? state.conversation.queries.length - 1,
+ query: { response: result },
+ }),
+ );
+ }
+ },
+ indx,
+ );
+ } else {
+ const answer = await handleFetchAnswer(
+ question,
+ signal,
+ state.preference.selectedDocs!,
+ state.conversation.queries,
+ state.conversation.conversationId,
+ state.preference.prompt.id,
+ state.preference.chunks,
+ state.preference.token_limit,
+ );
+ 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);
- });
- }
+ dispatch(
+ updateQuery({
+ index: indx ?? 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: [],
- };
- },
+ }
+ return {
+ conversationId: null,
+ title: null,
+ answer: '',
+ query: question,
+ result: '',
+ sources: [],
+ };
+},
);
export const conversationSlice = createSlice({
@@ -169,6 +170,12 @@ export const conversationSlice = createSlice({
setConversation(state, action: PayloadAction) {
state.queries = action.payload;
},
+ resendQuery(
+ state,
+ action: PayloadAction<{ index: number; prompt: string; query?: Query }>,
+ ) {
+ state.queries[action.payload.index] = action.payload;
+ },
updateStreamingQuery(
state,
action: PayloadAction<{ index: number; query: Partial }>,
@@ -250,6 +257,7 @@ export const selectStatus = (state: RootState) => state.conversation.status;
export const {
addQuery,
updateQuery,
+ resendQuery,
updateStreamingQuery,
updateConversationId,
updateStreamingSource,