From 7623bde159e47e43916de2097fc20191321a0e15 Mon Sep 17 00:00:00 2001 From: Siddhant Rai Date: Mon, 10 Feb 2025 09:36:18 +0530 Subject: [PATCH] feat: add update chunk API endpoint and service method --- application/api/user/routes.py | 73 ++++++++- frontend/src/api/endpoints.ts | 1 + frontend/src/api/services/userService.ts | 2 + frontend/src/modals/AddChunkModal.tsx | 86 ---------- frontend/src/modals/ChunkModal.tsx | 193 +++++++++++++++++++++++ frontend/src/settings/Documents.tsx | 94 +++++++---- 6 files changed, 330 insertions(+), 119 deletions(-) delete mode 100644 frontend/src/modals/AddChunkModal.tsx create mode 100644 frontend/src/modals/ChunkModal.tsx diff --git a/application/api/user/routes.py b/application/api/user/routes.py index 1ecf7e6e..b25e587f 100644 --- a/application/api/user/routes.py +++ b/application/api/user/routes.py @@ -1506,7 +1506,7 @@ class GetFeedbackAnalytics(Resource): except Exception as err: current_app.logger.error(f"Error getting API key: {err}") return make_response(jsonify({"success": False}), 400) - + end_date = datetime.datetime.now(datetime.timezone.utc) if filter_option == "last_hour": @@ -2193,3 +2193,74 @@ class DeleteChunk(Resource): ) except Exception as e: return make_response(jsonify({"error": str(e)}), 500) + + +@user_ns.route("/api/update_chunk") +class UpdateChunk(Resource): + @api.expect( + api.model( + "UpdateChunkModel", + { + "id": fields.String(required=True, description="Document ID"), + "chunk_id": fields.String( + required=True, description="Chunk ID to update" + ), + "text": fields.String( + required=False, description="New text of the chunk" + ), + "metadata": fields.Raw( + required=False, + description="Updated metadata associated with the chunk", + ), + }, + ) + ) + @api.doc( + description="Updates an existing chunk in the document.", + ) + def put(self): + data = request.get_json() + required_fields = ["id", "chunk_id"] + missing_fields = check_required_fields(data, required_fields) + if missing_fields: + return missing_fields + + doc_id = data.get("id") + chunk_id = data.get("chunk_id") + text = data.get("text") + metadata = data.get("metadata") + + if not ObjectId.is_valid(doc_id): + return make_response(jsonify({"error": "Invalid doc_id"}), 400) + + try: + store = get_vector_store(doc_id) + chunks = store.get_chunks() + existing_chunk = next((c for c in chunks if c["doc_id"] == chunk_id), None) + if not existing_chunk: + return make_response(jsonify({"error": "Chunk not found"}), 404) + + deleted = store.delete_chunk(chunk_id) + if not deleted: + return make_response( + jsonify({"error": "Failed to delete existing chunk"}), 500 + ) + + new_text = text if text is not None else existing_chunk["text"] + new_metadata = ( + metadata if metadata is not None else existing_chunk["metadata"] + ) + + new_chunk_id = store.add_chunk(new_text, new_metadata) + + return make_response( + jsonify( + { + "message": "Chunk updated successfully", + "new_chunk_id": new_chunk_id, + } + ), + 200, + ) + except Exception as e: + return make_response(jsonify({"error": str(e)}), 500) diff --git a/frontend/src/api/endpoints.ts b/frontend/src/api/endpoints.ts index b117e18b..9bf659de 100644 --- a/frontend/src/api/endpoints.ts +++ b/frontend/src/api/endpoints.ts @@ -29,6 +29,7 @@ const endpoints = { ADD_CHUNK: '/api/add_chunk', DELETE_CHUNK: (docId: string, chunkId: string) => `/api/delete_chunk?id=${docId}&chunk_id=${chunkId}`, + UPDATE_CHUNK: '/api/update_chunk', }, CONVERSATION: { ANSWER: '/api/answer', diff --git a/frontend/src/api/services/userService.ts b/frontend/src/api/services/userService.ts index 76c704d9..e7f367f1 100644 --- a/frontend/src/api/services/userService.ts +++ b/frontend/src/api/services/userService.ts @@ -57,6 +57,8 @@ const userService = { apiClient.post(endpoints.USER.ADD_CHUNK, data), deleteChunk: (docId: string, chunkId: string): Promise => apiClient.delete(endpoints.USER.DELETE_CHUNK(docId, chunkId)), + updateChunk: (data: any): Promise => + apiClient.put(endpoints.USER.UPDATE_CHUNK, data), }; export default userService; diff --git a/frontend/src/modals/AddChunkModal.tsx b/frontend/src/modals/AddChunkModal.tsx deleted file mode 100644 index 6ab41ccd..00000000 --- a/frontend/src/modals/AddChunkModal.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; - -import Exit from '../assets/exit.svg'; -import Input from '../components/Input'; -import { ActiveState } from '../models/misc'; - -export default function AddChunkModal({ - modalState, - setModalState, - handleSubmit, -}: { - modalState: ActiveState; - setModalState: (state: ActiveState) => void; - handleSubmit: (title: string, text: string) => void; -}) { - const [title, setTitle] = React.useState(''); - const [chunkText, setChunkText] = React.useState(''); - return ( -
-
-
- -
-

- Add Chunk -

-
- - Title - - setTitle(e.target.value)} - borderVariant="thin" - placeholder={'Enter title'} - > -
-
- - Body text - - -
-
- - -
-
-
-
-
- ); -} diff --git a/frontend/src/modals/ChunkModal.tsx b/frontend/src/modals/ChunkModal.tsx new file mode 100644 index 00000000..cfeb1c76 --- /dev/null +++ b/frontend/src/modals/ChunkModal.tsx @@ -0,0 +1,193 @@ +import React from 'react'; + +import Exit from '../assets/exit.svg'; +import Input from '../components/Input'; +import { ActiveState } from '../models/misc'; +import ConfirmationModal from './ConfirmationModal'; + +export default function ChunkModal({ + type, + modalState, + setModalState, + handleSubmit, + originalTitle, + originalText, + handleDelete, +}: { + type: 'ADD' | 'EDIT'; + modalState: ActiveState; + setModalState: (state: ActiveState) => void; + handleSubmit: (title: string, text: string) => void; + originalTitle?: string; + originalText?: string; + handleDelete?: () => void; +}) { + const [title, setTitle] = React.useState(''); + const [chunkText, setChunkText] = React.useState(''); + const [deleteModal, setDeleteModal] = React.useState('INACTIVE'); + + React.useEffect(() => { + setTitle(originalTitle || ''); + setChunkText(originalText || ''); + }, [originalTitle, originalText]); + if (type === 'ADD') { + return ( +
+
+
+ +
+

+ Add Chunk +

+
+ + Title + + setTitle(e.target.value)} + borderVariant="thin" + placeholder={'Enter title'} + > +
+
+
+ + Body text + + +
+
+
+ + +
+
+
+
+
+ ); + } else { + return ( +
+
+
+ +
+

+ Edit Chunk +

+
+ + Title + + setTitle(e.target.value)} + borderVariant="thin" + placeholder={'Enter title'} + > +
+
+
+ + Body text + + +
+
+
+ +
+ + +
+
+
+
+
+ {}} + submitLabel="Delete" + /> +
+ ); + } +} diff --git a/frontend/src/settings/Documents.tsx b/frontend/src/settings/Documents.tsx index 62cf21e3..5eedba21 100644 --- a/frontend/src/settings/Documents.tsx +++ b/frontend/src/settings/Documents.tsx @@ -5,6 +5,7 @@ import { useDispatch } from 'react-redux'; import userService from '../api/services/userService'; import ArrowLeft from '../assets/arrow-left.svg'; import caretSort from '../assets/caret-sort.svg'; +import Edit from '../assets/edit.svg'; import NoFilesDarkIcon from '../assets/no-files-dark.svg'; import NoFilesIcon from '../assets/no-files.svg'; import SyncIcon from '../assets/sync.svg'; @@ -15,7 +16,7 @@ import Input from '../components/Input'; import SkeletonLoader from '../components/SkeletonLoader'; import Spinner from '../components/Spinner'; import { useDarkTheme } from '../hooks'; -import AddChunkModal from '../modals/AddChunkModal'; +import ChunkModal from '../modals/ChunkModal'; import ConfirmationModal from '../modals/ConfirmationModal'; import { ActiveState, Doc, DocumentsProps } from '../models/misc'; import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi'; @@ -396,11 +397,11 @@ function DocumentChunks({ const [totalChunks, setTotalChunks] = useState(0); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); - const [deleteModalState, setDeleteModalState] = useState<{ + const [addModal, setAddModal] = useState('INACTIVE'); + const [editModal, setEditModal] = useState<{ state: ActiveState; - chunkId: string | null; - }>({ state: 'INACTIVE', chunkId: null }); - const [addModalState, setAddModalState] = useState('INACTIVE'); + chunk: ChunkType | null; + }>({ state: 'INACTIVE', chunk: null }); const fetchChunks = () => { setLoading(true); @@ -448,15 +449,39 @@ function DocumentChunks({ } }; - const handleDeleteChunk = (chunkId: string) => { + const handleUpdateChunk = (title: string, text: string, chunk: ChunkType) => { try { - userService.deleteChunk(document.id ?? '', chunkId).then((response) => { - if (!response.ok) { - throw new Error('Failed to delete chunk'); - } - setDeleteModalState({ state: 'INACTIVE', chunkId: null }); - fetchChunks(); - }); + userService + .updateChunk({ + id: document.id ?? '', + chunk_id: chunk.doc_id, + text: text, + metadata: { + title: title, + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to update chunk'); + } + fetchChunks(); + }); + } catch (e) { + console.log(e); + } + }; + + const handleDeleteChunk = (chunk: ChunkType) => { + try { + userService + .deleteChunk(document.id ?? '', chunk.doc_id) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to delete chunk'); + } + setEditModal({ state: 'INACTIVE', chunk: null }); + fetchChunks(); + }); } catch (e) { console.log(e); } @@ -498,7 +523,7 @@ function DocumentChunks({ @@ -539,18 +564,18 @@ function DocumentChunks({
@@ -590,20 +615,25 @@ function DocumentChunks({ />
)} - - setDeleteModalState((prev) => ({ ...prev, state })) - } - handleSubmit={() => handleDeleteChunk(deleteModalState.chunkId ?? '')} - submitLabel="Delete" - /> - + setEditModal((prev) => ({ ...prev, state }))} + handleSubmit={(title, text) => { + handleUpdateChunk(title, text, editModal.chunk as ChunkType); + }} + originalText={editModal.chunk?.text} + originalTitle={editModal.chunk?.metadata?.title} + handleDelete={() => { + handleDeleteChunk(editModal.chunk as ChunkType); + }} + />
); }