diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 37a3f7a1..57a55406 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -168,6 +168,48 @@ class UpdateConversationName(Resource): return make_response(jsonify({"success": True}), 200) +@user_ns.route("/api/update_conversation_queries") +class UpdateConversationQueries(Resource): + @api.expect( + api.model( + "UpdateConversationQueriesModel", + { + "id": fields.String(required=True, description="Conversation ID"), + "limit": fields.Integer( + required=True, description="Number by which queries should be sliced." + ), + }, + ) + ) + @api.doc( + description="Updates the queries in a conversation", + ) + def post(self): + data = request.get_json() + required_fields = ["id", "limit"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + try: + conversations_collection.update_one( + {"_id": ObjectId(data["id"])},[{ + "$set": { + "queries": { + "$slice": ["$queries", data["limit"]] + } + } + }]) + conversation = conversations_collection.find_one( + {"_id": ObjectId(data["id"])} + ) + if not conversation: + return make_response(jsonify({"status": "not found"}), 404) + except Exception as err: + return make_response(jsonify({"success": False, "error": str(err)}), 400) + + return make_response(jsonify(conversation["queries"]), 200) + @user_ns.route("/api/feedback") class SubmitFeedback(Resource): diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index 4e7112d0..d584544c 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -18,6 +18,7 @@ const endpoints = { FEEDBACK_ANALYTICS: '/api/get_feedback_analytics', LOGS: `/api/get_user_logs`, MANAGE_SYNC: '/api/manage_sync', + UPDATE_CONVERSATION_QUERIES: '/api/update_conversation_queries', }, CONVERSATION: { ANSWER: '/api/answer', diff --git a/frontend/src/api/services/conversationService.ts b/frontend/src/api/services/conversationService.ts index aaf703de..5c8dc58f 100644 --- a/frontend/src/api/services/conversationService.ts +++ b/frontend/src/api/services/conversationService.ts @@ -27,6 +27,8 @@ const conversationService = { apiClient.get(endpoints.CONVERSATION.DELETE_ALL), update: (data: any): Promise => apiClient.post(endpoints.CONVERSATION.UPDATE, data), + update_conversation_queries: (data: any): Promise => + apiClient.post(endpoints.USER.UPDATE_CONVERSATION_QUERIES, data), }; export default conversationService; diff --git a/frontend/src/conversation/Conversation.tsx b/frontend/src/conversation/Conversation.tsx index bd9b8496..abb0eb87 100644 --- a/frontend/src/conversation/Conversation.tsx +++ b/frontend/src/conversation/Conversation.tsx @@ -15,6 +15,7 @@ import { ShareConversationModal } from '../modals/ShareConversationModal'; import { setConversation, updateConversationId } from './conversationSlice'; 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'; @@ -84,15 +85,33 @@ export default function Conversation() { const handleQuestion = ({ question, isRetry = false, + updated = null, + indx = null, }: { question: string; isRetry?: boolean; + updated?: boolean | null; + indx?: number | null; }) => { - 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) { + conversationService + .update_conversation_queries({ + id: conversationId, + limit: indx, + }) + .then((response) => response.json()) + .then((data) => { + dispatch(setConversation(data)); + !isRetry && dispatch(addQuery({ prompt: question })); //dispatch only new queries + fetchStream.current = dispatch(fetchAnswer({ question })); + }); + } 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) => { @@ -103,8 +122,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( @@ -289,6 +314,8 @@ export default function Conversation() { key={`${index}QUESTION`} message={query.prompt} type="QUESTION" + handleUpdatedQuestionSubmission={handleQuestionSubmission} + questionNumber={index} sources={query.sources} > @@ -327,7 +354,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 284c2b56..04c95dd8 100644 --- a/frontend/src/conversation/ConversationBubble.tsx +++ b/frontend/src/conversation/ConversationBubble.tsx @@ -13,6 +13,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'; @@ -36,36 +37,103 @@ 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 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="ml-2 mr-2 rounded-[28px] py-[14px] px-[19px] border-[1.5px] border-black" + /> + )} +
+ Edit { + setIsEditClicked(true); + setEditInputBox(message); + }} + /> +
+ {isEditClicked && ( +
+ + +
+ )}
); } else {