feat: view and add document chunks for mongodb and faiss

This commit is contained in:
Siddhant Rai
2025-02-07 19:39:07 +05:30
parent f97b56a87b
commit 0379b81d43
11 changed files with 558 additions and 56 deletions

View File

@@ -1,24 +1,31 @@
import React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import userService from '../api/services/userService';
import SyncIcon from '../assets/sync.svg';
import Trash from '../assets/trash.svg';
import caretSort from '../assets/caret-sort.svg';
import DropdownMenu from '../components/DropdownMenu';
import SkeletonLoader from '../components/SkeletonLoader';
import Input from '../components/Input';
import Upload from '../upload/Upload'; // Import the Upload component
import Pagination from '../components/DocumentPagination';
import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { Doc, DocumentsProps, ActiveState } from '../models/misc'; // Ensure ActiveState type is imported
import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi';
import { setSourceDocs } from '../preferences/preferenceSlice';
import { setPaginatedDocuments } from '../preferences/preferenceSlice';
import { formatDate } from '../utils/dateTimeUtils';
import ConfirmationModal from '../modals/ConfirmationModal';
// Utility function to format numbers
import userService from '../api/services/userService';
import ArrowLeft from '../assets/arrow-left.svg';
import caretSort from '../assets/caret-sort.svg';
import NoFilesDarkIcon from '../assets/no-files-dark.svg';
import NoFilesIcon from '../assets/no-files.svg';
import SyncIcon from '../assets/sync.svg';
import Trash from '../assets/trash.svg';
import Pagination from '../components/DocumentPagination';
import DropdownMenu from '../components/DropdownMenu';
import Input from '../components/Input';
import SkeletonLoader from '../components/SkeletonLoader';
import { useDarkTheme } from '../hooks';
import AddChunkModal from '../modals/AddChunkModal';
import ConfirmationModal from '../modals/ConfirmationModal';
import { ActiveState, Doc, DocumentsProps } from '../models/misc';
import { getDocs, getDocsWithPagination } from '../preferences/preferenceApi';
import {
setPaginatedDocuments,
setSourceDocs,
} from '../preferences/preferenceSlice';
import Upload from '../upload/Upload';
import { formatDate } from '../utils/dateTimeUtils';
import { ChunkType } from './types';
const formatTokens = (tokens: number): string => {
const roundToTwoDecimals = (num: number): string => {
return (Math.round((num + Number.EPSILON) * 100) / 100).toString();
@@ -35,17 +42,16 @@ const formatTokens = (tokens: number): string => {
}
};
const Documents: React.FC<DocumentsProps> = ({
export default function Documents({
paginatedDocuments,
handleDeleteDocument,
}) => {
}: DocumentsProps) {
const { t } = useTranslation();
const dispatch = useDispatch();
// State for search input
const [searchTerm, setSearchTerm] = useState<string>('');
// State for modal: active/inactive
const [modalState, setModalState] = useState<ActiveState>('INACTIVE'); // Initialize with inactive state
const [isOnboarding, setIsOnboarding] = useState<boolean>(false); // State for onboarding flag
const [modalState, setModalState] = useState<ActiveState>('INACTIVE');
const [isOnboarding, setIsOnboarding] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(false);
const [sortField, setSortField] = useState<'date' | 'tokens'>('date');
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc');
@@ -60,6 +66,7 @@ const Documents: React.FC<DocumentsProps> = ({
{ label: t('settings.documents.syncFrequency.weekly'), value: 'weekly' },
{ label: t('settings.documents.syncFrequency.monthly'), value: 'monthly' },
];
const [showDocumentChunks, setShowDocumentChunks] = useState<Doc>();
const refreshDocs = useCallback(
(
@@ -159,7 +166,14 @@ const Documents: React.FC<DocumentsProps> = ({
refreshDocs(undefined, 1, rowsPerPage);
}, [searchTerm]);
return (
return showDocumentChunks ? (
<DocumentChunks
document={showDocumentChunks}
handleGoBack={() => {
setShowDocumentChunks(undefined);
}}
/>
) : (
<div className="flex flex-col mt-8">
<div className="flex flex-col relative flex-grow">
<div className="mb-6">
@@ -183,6 +197,7 @@ const Documents: React.FC<DocumentsProps> = ({
setSearchTerm(e.target.value);
setCurrentPage(1);
}}
borderVariant="thin"
/>
</div>
<button
@@ -256,7 +271,11 @@ const Documents: React.FC<DocumentsProps> = ({
</tr>
) : (
currentDocuments.map((document, index) => (
<tr key={index} className="group transition-colors">
<tr
key={index}
className="group transition-colors"
onClick={() => setShowDocumentChunks(document)}
>
<td
className="py-4 px-4 text-sm text-gray-700 dark:text-[#E0E0E0] w-[45%] truncate group-hover:bg-gray-50 dark:group-hover:bg-gray-800/50"
title={document.name}
@@ -359,11 +378,169 @@ const Documents: React.FC<DocumentsProps> = ({
)}
</div>
);
};
}
Documents.propTypes = {
//documents: PropTypes.array.isRequired,
handleDeleteDocument: PropTypes.func.isRequired,
};
function DocumentChunks({
document,
handleGoBack,
}: {
document: Doc;
handleGoBack: () => void;
}) {
const { t } = useTranslation();
const [isDarkTheme] = useDarkTheme();
const [paginatedChunks, setPaginatedChunks] = useState<ChunkType[]>([]);
const [page, setPage] = useState(1);
const [perPage, setPerPage] = useState(5);
const [totalChunks, setTotalChunks] = useState(0);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState<string>('');
const [modalState, setModalState] = useState<ActiveState>('INACTIVE');
export default Documents;
const fetchChunks = () => {
setLoading(true);
try {
userService
.getDocumentChunks(document.id ?? '', page, perPage)
.then((response) => {
if (!response.ok) {
throw new Error('Failed to fetch chunks data');
}
return response.json();
})
.then((data) => {
setPage(data.page);
setPerPage(data.per_page);
setTotalChunks(data.total);
setPaginatedChunks(data.chunks);
});
} catch (e) {
console.log(e);
} finally {
setLoading(false);
}
};
const handleAddChunk = (title: string, text: string) => {
try {
userService
.addChunk({
id: document.id ?? '',
text: text,
metadata: {
title: title,
},
})
.then((response) => {
if (!response.ok) {
throw new Error('Failed to add chunk');
}
fetchChunks();
});
} catch (e) {
console.log(e);
}
};
React.useEffect(() => {
fetchChunks();
}, [page, perPage]);
return (
<div className="flex flex-col mt-8">
<div className="mb-3 flex items-center gap-3 text-eerie-black dark:text-bright-gray text-sm">
<button
className="text-sm text-gray-400 dark:text-gray-500 border dark:border-0 dark:bg-[#28292D] dark:hover:bg-[#2E2F34] p-3 rounded-full"
onClick={handleGoBack}
>
<img src={ArrowLeft} alt="left-arrow" className="w-3 h-3" />
</button>
<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">
<label htmlFor="chunk-search-input" className="sr-only">
{t('settings.documents.searchPlaceholder')}
</label>
<Input
maxLength={256}
placeholder={t('settings.documents.searchPlaceholder')}
name="chunk-search-input"
type="text"
id="chunk-search-input"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
}}
borderVariant="thin"
/>
</div>
<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')}
>
{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) =>
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="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>
<AddChunkModal
modalState={modalState}
setModalState={setModalState}
handleSubmit={handleAddChunk}
/>
</div>
);
}