feat: loading state and spinner + delete chunk option

This commit is contained in:
Siddhant Rai
2025-02-08 14:52:32 +05:30
parent 0379b81d43
commit 0fc9718c35
5 changed files with 172 additions and 73 deletions

View File

@@ -20,19 +20,19 @@ class FaissStore(BaseVectorStore):
super().__init__()
self.source_id = source_id
self.path = get_vectorstore(source_id)
embeddings = self._get_embeddings(settings.EMBEDDINGS_NAME, embeddings_key)
self.embeddings = self._get_embeddings(settings.EMBEDDINGS_NAME, embeddings_key)
try:
if docs_init:
self.docsearch = FAISS.from_documents(docs_init, embeddings)
self.docsearch = FAISS.from_documents(docs_init, self.embeddings)
else:
self.docsearch = FAISS.load_local(
self.path, embeddings, allow_dangerous_deserialization=True
self.path, self.embeddings, allow_dangerous_deserialization=True
)
except Exception:
raise
self.assert_embedding_dimensions(embeddings)
self.assert_embedding_dimensions(self.embeddings)
def search(self, *args, **kwargs):
return self.docsearch.similarity_search(*args, **kwargs)
@@ -84,17 +84,6 @@ class FaissStore(BaseVectorStore):
return doc_id
def delete_chunk(self, chunk_id):
docstore = self.docsearch.docstore._dict
if chunk_id not in docstore:
return False
del docstore[chunk_id]
documents = list(docstore.values())
if documents:
self.docsearch = FAISS.from_documents(documents, self.embeddings)
else:
self.docsearch = FAISS.from_texts([" "], self.embeddings)
self.save_local()
self.delete_index([chunk_id])
self.save_local(self.path)
return True

View File

@@ -27,6 +27,8 @@ const endpoints = {
GET_CHUNKS: (docId: string, page: number, per_page: number) =>
`/api/get_chunks?id=${docId}&page=${page}&per_page=${per_page}`,
ADD_CHUNK: '/api/add_chunk',
DELETE_CHUNK: (docId: string, chunkId: string) =>
`/api/delete_chunk?id=${docId}&chunk_id=${chunkId}`,
},
CONVERSATION: {
ANSWER: '/api/answer',

View File

@@ -55,6 +55,8 @@ const userService = {
apiClient.get(endpoints.USER.GET_CHUNKS(docId, page, perPage)),
addChunk: (data: any): Promise<any> =>
apiClient.post(endpoints.USER.ADD_CHUNK, data),
deleteChunk: (docId: string, chunkId: string): Promise<any> =>
apiClient.delete(endpoints.USER.DELETE_CHUNK(docId, chunkId)),
};
export default userService;

View File

@@ -0,0 +1,43 @@
import React from 'react';
type SpinnerProps = {
size?: 'small' | 'medium' | 'large';
color?: string;
};
export default function Spinner({
size = 'medium',
color = 'grey',
}: SpinnerProps) {
const sizeMap = {
small: '20px',
medium: '30px',
large: '40px',
};
const spinnerSize = sizeMap[size];
const spinnerStyle = {
width: spinnerSize,
height: spinnerSize,
aspectRatio: '1',
borderRadius: '50%',
background: `
radial-gradient(farthest-side, ${color} 94%, #0000) top/8px 8px no-repeat,
conic-gradient(#0000 30%, ${color})
`,
WebkitMask:
'radial-gradient(farthest-side, #0000 calc(100% - 8px), #000 0)',
animation: 'l13 1s infinite linear',
} as React.CSSProperties;
const keyframesStyle = `@keyframes l13 {
100% { transform: rotate(1turn) }
}`;
return (
<>
<style>{keyframesStyle}</style>
<div className="loader" style={spinnerStyle} />
</>
);
}

View File

@@ -13,6 +13,7 @@ import Pagination from '../components/DocumentPagination';
import DropdownMenu from '../components/DropdownMenu';
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 ConfirmationModal from '../modals/ConfirmationModal';
@@ -393,9 +394,13 @@ function DocumentChunks({
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(5);
const [totalChunks, setTotalChunks] = useState(0);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState<string>('');
const [modalState, setModalState] = useState<ActiveState>('INACTIVE');
const [deleteModalState, setDeleteModalState] = useState<{
state: ActiveState;
chunkId: string | null;
}>({ state: 'INACTIVE', chunkId: null });
const [addModalState, setAddModalState] = useState<ActiveState>('INACTIVE');
const fetchChunks = () => {
setLoading(true);
@@ -404,6 +409,8 @@ function DocumentChunks({
.getDocumentChunks(document.id ?? '', page, perPage)
.then((response) => {
if (!response.ok) {
setLoading(false);
setPaginatedChunks([]);
throw new Error('Failed to fetch chunks data');
}
return response.json();
@@ -413,11 +420,10 @@ function DocumentChunks({
setPerPage(data.per_page);
setTotalChunks(data.total);
setPaginatedChunks(data.chunks);
setLoading(false);
});
} catch (e) {
console.log(e);
} finally {
setLoading(false);
}
};
@@ -442,6 +448,20 @@ function DocumentChunks({
}
};
const handleDeleteChunk = (chunkId: string) => {
try {
userService.deleteChunk(document.id ?? '', chunkId).then((response) => {
if (!response.ok) {
throw new Error('Failed to delete chunk');
}
setDeleteModalState({ state: 'INACTIVE', chunkId: null });
fetchChunks();
});
} catch (e) {
console.log(e);
}
};
React.useEffect(() => {
fetchChunks();
}, [page, perPage]);
@@ -457,7 +477,8 @@ function DocumentChunks({
<p className="mt-px">Back to all documents</p>
</div>
<div className="my-3 flex justify-between items-center gap-1">
<div className="w-full sm:w-auto">
<div className="w-full sm:w-auto flex items-center gap-2 text-eerie-black dark:text-bright-gray">
<p className="font-semibold text-2xl hidden sm:flex">{`${totalChunks} Chunks`}</p>
<label htmlFor="chunk-search-input" className="sr-only">
{t('settings.documents.searchPlaceholder')}
</label>
@@ -477,68 +498,110 @@ function DocumentChunks({
<button
className="rounded-full w-full sm:w-40 bg-purple-30 px-4 py-3 text-white hover:bg-[#6F3FD1]"
title={t('settings.documents.addNew')}
onClick={() => setModalState('ACTIVE')}
onClick={() => setAddModalState('ACTIVE')}
>
{t('settings.documents.addNew')}
</button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{paginatedChunks.filter((chunk) =>
{loading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="h-32 flex items-center justify-center mt-24 col-span-2 lg:col-span-3">
<Spinner />
</div>
</div>
) : (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{paginatedChunks.filter((chunk) =>
chunk.metadata?.title
.toLowerCase()
.includes(searchTerm.toLowerCase()),
).length === 0 ? (
<div className="mt-24 col-span-2 lg:col-span-3 text-center text-gray-500 dark:text-gray-400">
<img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt="No tools found"
className="h-24 w-24 mx-auto mb-2"
/>
No chunks found
</div>
) : (
paginatedChunks
.filter((chunk) =>
chunk.metadata?.title
.toLowerCase()
.includes(searchTerm.toLowerCase()),
)
.map((chunk, index) => (
<div
key={index}
className="relative h-56 w-full p-6 border rounded-2xl border-silver dark:border-silver/40 flex flex-col justify-between"
>
<div className="w-full">
<div className="w-full flex items-center justify-between">
<button
className="absolute top-3 right-3 h-[19px] w-[19px] cursor-pointer"
onClick={() => {
setDeleteModalState({
state: 'ACTIVE',
chunkId: chunk.doc_id,
});
}}
aria-label={'delete'}
>
<img
src={Trash}
alt={'delete'}
className="opacity-60 hover:opacity-100"
/>
</button>
</div>
<div className="mt-[9px]">
<p className="h-12 text-sm font-semibold text-eerie-black dark:text-[#EEEEEE] leading-relaxed break-words ellipsis-text">
{chunk.metadata?.title}
</p>
<p className="mt-1 pr-1 h-[110px] overflow-y-auto text-[13px] text-gray-600 dark:text-gray-400 leading-relaxed break-words">
{chunk.text}
</p>
</div>
</div>
</div>
))
)}
</div>
)}
{!loading &&
paginatedChunks.filter((chunk) =>
chunk.metadata?.title
.toLowerCase()
.includes(searchTerm.toLowerCase()),
).length === 0 ? (
<div className="mt-24 col-span-2 lg:col-span-3 text-center text-gray-500 dark:text-gray-400">
<img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt="No tools found"
className="h-24 w-24 mx-auto mb-2"
).length !== 0 && (
<div className="mt-10 w-full flex items-center justify-center">
<Pagination
currentPage={page}
totalPages={Math.ceil(totalChunks / perPage)}
rowsPerPage={perPage}
onPageChange={(page) => {
setPage(page);
}}
onRowsPerPageChange={(rows) => {
setPerPage(rows);
setPage(1);
}}
/>
No chunks found
</div>
) : (
paginatedChunks
.filter((chunk) =>
chunk.metadata?.title
.toLowerCase()
.includes(searchTerm.toLowerCase()),
)
.map((chunk, index) => (
<div
key={index}
className="relative h-56 w-full p-6 border rounded-2xl border-silver dark:border-silver/40 flex flex-col justify-between"
>
<div className="w-full">
<div className="mt-[9px]">
<p className="h-12 text-sm font-semibold text-eerie-black dark:text-[#EEEEEE] leading-relaxed break-words ellipsis-text">
{chunk.metadata?.title}
</p>
<p className="mt-1 pr-1 h-[110px] overflow-y-auto text-[13px] text-gray-600 dark:text-gray-400 leading-relaxed break-words">
{chunk.text}
</p>
</div>
</div>
</div>
))
)}
</div>
<div className="mt-10 w-full flex items-center justify-center">
<Pagination
currentPage={page}
totalPages={Math.ceil(totalChunks / perPage)}
rowsPerPage={perPage}
onPageChange={(page) => {
setPage(page);
}}
onRowsPerPageChange={(rows) => {
setPerPage(rows);
setPage(1);
}}
/>
</div>
<ConfirmationModal
message="Are you sure you want to delete this?"
modalState={deleteModalState.state}
setModalState={(state) =>
setDeleteModalState((prev) => ({ ...prev, state }))
}
handleSubmit={() => handleDeleteChunk(deleteModalState.chunkId ?? '')}
submitLabel="Delete"
/>
<AddChunkModal
modalState={modalState}
setModalState={setModalState}
modalState={addModalState}
setModalState={setAddModalState}
handleSubmit={handleAddChunk}
/>
</div>