(refactor) separation in chunks/files view

This commit is contained in:
ManishMadan2882
2025-07-22 19:36:52 +05:30
parent 50b1755a63
commit 6eb2c884a2
4 changed files with 274 additions and 232 deletions

View File

@@ -0,0 +1,3 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.4798 10.739C9.27414 11.6748 7.7572 12.116 6.23773 11.9728C4.71826 11.8296 3.31047 11.1127 2.30094 9.96806C1.2914 8.82345 0.756002 7.33714 0.803717 5.81168C0.851432 4.28622 1.47868 2.83628 2.55777 1.75699C3.63706 0.677895 5.087 0.0506505 6.61246 0.00293578C8.13792 -0.044779 9.62423 0.490623 10.7688 1.50016C11.9135 2.50969 12.6303 3.91747 12.7736 5.43694C12.9168 6.95641 12.4756 8.47336 11.5398 9.67899L14.5798 12.719C14.6785 12.8107 14.7507 12.9273 14.7887 13.0565C14.8267 13.1858 14.8291 13.3229 14.7958 13.4534C14.7624 13.5839 14.6944 13.703 14.5991 13.7982C14.5037 13.8933 14.3844 13.961 14.2538 13.994C14.1234 14.0274 13.9864 14.0251 13.8573 13.9872C13.7281 13.9494 13.6115 13.8775 13.5198 13.779L10.4798 10.739ZM11.2998 5.99899C11.3087 5.4026 11.1989 4.81039 10.9768 4.25681C10.7547 3.70323 10.4248 3.19934 10.0062 2.77445C9.58757 2.34955 9.08865 2.01214 8.53844 1.78183C7.98824 1.55152 7.39773 1.43292 6.80127 1.43292C6.20481 1.43292 5.6143 1.55152 5.0641 1.78183C4.5139 2.01214 4.01498 2.34955 3.59637 2.77445C3.17777 3.19934 2.84783 3.70323 2.62575 4.25681C2.40367 4.81039 2.29388 5.4026 2.30277 5.99899C2.32039 7.18045 2.80208 8.30756 3.6438 9.13682C4.48552 9.96608 5.61968 10.4309 6.80127 10.4309C7.98286 10.4309 9.11703 9.96608 9.95874 9.13682C10.8005 8.30756 11.2822 7.18045 11.2998 5.99899Z" fill="#59636E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useSelector } from 'react-redux';
import { useTranslation } from 'react-i18next';
import { selectToken } from '../preferences/preferenceSlice';
@@ -19,16 +19,16 @@ interface DocumentChunksProps {
documentId: string;
documentName?: string;
handleGoBack: () => void;
showHeader?: boolean;
path?: string;
renderFileSearch?: () => React.ReactNode;
}
const DocumentChunks: React.FC<DocumentChunksProps> = ({
documentId,
documentName,
handleGoBack,
showHeader = true,
path,
renderFileSearch
}) => {
const { t } = useTranslation();
const token = useSelector(selectToken);
@@ -45,6 +45,8 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
chunk: ChunkType | null;
}>({ state: 'INACTIVE', chunk: null });
const pathParts = path ? path.split('/') : [];
const fetchChunks = () => {
@@ -149,159 +151,173 @@ const DocumentChunks: React.FC<DocumentChunksProps> = ({
.includes(searchTerm.toLowerCase());
});
return (
<div className={`${showHeader ? 'mt-8' : 'mt-0'} flex flex-col`}>
{showHeader && (
<div className="mb-4 flex items-center overflow-hidden">
<button
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34] flex-shrink-0"
onClick={handleGoBack}
>
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
<div className="flex items-center overflow-hidden">
<img src={OutlineSource} alt="source" className="mr-2 h-5 w-5 flex-shrink-0" />
<span className="text-[#7D54D1] font-semibold text-base leading-6 whitespace-nowrap">
{documentName}
</span>
{pathParts.length > 0 && (
<>
<span className="mx-1 text-gray-500 flex-shrink-0">/</span>
{pathParts.map((part, index) => (
<React.Fragment key={index}>
<span className="font-normal text-base leading-6 text-gray-700 dark:text-gray-300 whitespace-nowrap">
{part}
</span>
{index < pathParts.length - 1 && (
<span className="mx-1 text-gray-500 flex-shrink-0">/</span>
)}
</React.Fragment>
))}
</>
)}
</div>
</div>
)}
<div className="mb-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div className="flex-1 w-full flex items-center border border-[#D1D9E0] dark:border-[#6A6A6A] rounded-md overflow-hidden h-[38px]">
<div className="px-4 flex items-center text-gray-700 dark:text-[#E0E0E0] font-medium whitespace-nowrap h-full">
{totalChunks > 999999
? `${(totalChunks / 1000000).toFixed(2)}M`
: totalChunks > 999
? `${(totalChunks / 1000).toFixed(2)}K`
: totalChunks} {t('settings.documents.chunks')}
</div>
<div className="h-full w-[1px] bg-[#D1D9E0] dark:bg-[#6A6A6A]"></div>
<div className="flex-1 h-full">
<input
type="text"
placeholder={t('settings.documents.searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full h-full px-3 py-2 bg-transparent border-none outline-none font-normal text-[13.56px] leading-[100%] dark:text-[#E0E0E0]"
/>
</div>
</div>
const renderPathNavigation = () => {
return (
<div className="flex items-center">
<button
className="bg-purple-30 hover:bg-violets-are-blue flex h-[38px] w-full sm:w-auto min-w-[108px] items-center justify-center rounded-full px-4 text-sm whitespace-normal text-white shrink-0"
title={t('settings.documents.addNew')}
onClick={() => setAddModal('ACTIVE')}
className="mr-3 flex h-[29px] w-[29px] items-center justify-center rounded-full border p-2 text-sm text-gray-400 dark:border-0 dark:bg-[#28292D] dark:text-gray-500 dark:hover:bg-[#2E2F34] flex-shrink-0"
onClick={handleGoBack}
>
{t('settings.documents.addNew')}
<img src={ArrowLeft} alt="left-arrow" className="h-3 w-3" />
</button>
</div>
{loading ? (
<div className="w-full mt-24 flex justify-center">
<Spinner />
</div>
) : (
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredChunks.length === 0 ? (
<div className="col-span-full flex flex-col items-center justify-center mt-24 text-center text-gray-500 dark:text-gray-400">
<img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt={t('settings.documents.noChunksAlt')}
className="mx-auto mb-2 h-24 w-24"
/>
{t('settings.documents.noChunks')}
</div>
) : (
filteredChunks.map((chunk, index) => (
<div
key={index}
className="relative flex h-[208px] flex-col justify-between rounded-[5.86px] border border-[#D1D9E0] dark:border-[#6A6A6A] overflow-hidden w-full"
>
<div className="w-full">
<div className="flex w-full items-center justify-between border-b border-[#D1D9E0] bg-[#F6F8FA] dark:bg-[#27282D] dark:border-[#6A6A6A] px-4 py-3">
<div className="text-[#59636E] text-sm dark:text-[#E0E0E0]">
{chunk.metadata.token_count ? chunk.metadata.token_count.toLocaleString() : '-'} tokens
</div>
<button
aria-label={'edit'}
onClick={() => {
setEditModal({
state: 'ACTIVE',
chunk: chunk,
});
}}
className="text-left"
>
<img src={EditIcon} alt="edit" className="h-4 w-4" />
</button>
</div>
<div className="p-4">
<p className="font-['Inter'] text-[13.68px] leading-[19.93px] text-[#18181B] dark:text-[#E0E0E0] line-clamp-7 font-normal">
{chunk.text}
</p>
</div>
</div>
</div>
))
<div className="flex items-center overflow-hidden">
<img src={OutlineSource} alt="source" className="mr-2 h-5 w-5 flex-shrink-0" />
<span className="text-[#7D54D1] font-semibold text-base leading-6 whitespace-nowrap">
{documentName}
</span>
{pathParts.length > 0 && (
<>
<span className="mx-1 text-gray-500 flex-shrink-0">/</span>
{pathParts.map((part, index) => (
<React.Fragment key={index}>
<span className="font-normal text-base leading-6 text-gray-700 dark:text-gray-300 whitespace-nowrap">
{part}
</span>
{index < pathParts.length - 1 && (
<span className="mx-1 text-gray-500 flex-shrink-0">/</span>
)}
</React.Fragment>
))}
</>
)}
</div>
)}
</div>
);
};
{!loading && filteredChunks.length > 0 && (
<Pagination
currentPage={page}
totalPages={Math.ceil(totalChunks / perPage)}
rowsPerPage={perPage}
onPageChange={setPage}
onRowsPerPageChange={(rows) => {
setPerPage(rows);
setPage(1);
}}
/>
)}
return (
<div className="flex flex-col">
<div className="mb-4">
{renderPathNavigation()}
</div>
<div className="flex gap-4">
{renderFileSearch && (
renderFileSearch()
)}
<ChunkModal
type="ADD"
modalState={addModal}
setModalState={setAddModal}
handleSubmit={handleAddChunk}
/>
{editModal.chunk && (
<ChunkModal
type="EDIT"
modalState={editModal.state}
setModalState={(state) =>
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);
}}
/>
)}
</div>
);
{/* Right side: Chunks content */}
<div className="flex-1">
<div className="mb-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
<div className="flex-1 w-full flex items-center border border-[#D1D9E0] dark:border-[#6A6A6A] rounded-md overflow-hidden h-[38px]">
<div className="px-4 flex items-center text-gray-700 dark:text-[#E0E0E0] font-medium whitespace-nowrap h-full">
{totalChunks > 999999
? `${(totalChunks / 1000000).toFixed(2)}M`
: totalChunks > 999
? `${(totalChunks / 1000).toFixed(2)}K`
: totalChunks} {t('settings.documents.chunks')}
</div>
<div className="h-full w-[1px] bg-[#D1D9E0] dark:bg-[#6A6A6A]"></div>
<div className="flex-1 h-full">
<input
type="text"
placeholder={t('settings.documents.searchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full h-full px-3 py-2 bg-transparent border-none outline-none font-normal text-[13.56px] leading-[100%] dark:text-[#E0E0E0]"
/>
</div>
</div>
<button
className="bg-purple-30 hover:bg-violets-are-blue flex h-[38px] w-full sm:w-auto min-w-[108px] items-center justify-center rounded-full px-4 text-sm whitespace-normal text-white shrink-0"
title={t('settings.documents.addNew')}
onClick={() => setAddModal('ACTIVE')}
>
{t('settings.documents.addNew')}
</button>
</div>
{loading ? (
<div className="w-full mt-24 flex justify-center">
<Spinner />
</div>
) : (
<div className="w-full grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredChunks.length === 0 ? (
<div className="col-span-full flex flex-col items-center justify-center mt-24 text-center text-gray-500 dark:text-gray-400">
<img
src={isDarkTheme ? NoFilesDarkIcon : NoFilesIcon}
alt={t('settings.documents.noChunksAlt')}
className="mx-auto mb-2 h-24 w-24"
/>
{t('settings.documents.noChunks')}
</div>
) : (
filteredChunks.map((chunk, index) => (
<div
key={index}
className="relative flex h-[208px] flex-col justify-between rounded-[5.86px] border border-[#D1D9E0] dark:border-[#6A6A6A] overflow-hidden w-full"
>
<div className="w-full">
<div className="flex w-full items-center justify-between border-b border-[#D1D9E0] bg-[#F6F8FA] dark:bg-[#27282D] dark:border-[#6A6A6A] px-4 py-3">
<div className="text-[#59636E] text-sm dark:text-[#E0E0E0]">
{chunk.metadata.token_count ? chunk.metadata.token_count.toLocaleString() : '-'} tokens
</div>
<button
aria-label={'edit'}
onClick={() => {
setEditModal({
state: 'ACTIVE',
chunk: chunk,
});
}}
className="text-left"
>
<img src={EditIcon} alt="edit" className="h-4 w-4" />
</button>
</div>
<div className="p-4">
<p className="font-['Inter'] text-[13.68px] leading-[19.93px] text-[#18181B] dark:text-[#E0E0E0] line-clamp-7 font-normal">
{chunk.text}
</p>
</div>
</div>
</div>
))
)}
</div>
)}
{!loading && filteredChunks.length > 0 && (
<Pagination
currentPage={page}
totalPages={Math.ceil(totalChunks / perPage)}
rowsPerPage={perPage}
onPageChange={setPage}
onRowsPerPageChange={(rows) => {
setPerPage(rows);
setPage(1);
}}
/>
)}
<ChunkModal
type="ADD"
modalState={addModal}
setModalState={setAddModal}
handleSubmit={handleAddChunk}
/>
{editModal.chunk && (
<ChunkModal
type="EDIT"
modalState={editModal.state}
setModalState={(state) =>
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);
}}
/>
)}
</div>
</div>
</div>
);
};
export default DocumentChunks;
export default DocumentChunks;

View File

@@ -12,6 +12,7 @@ import EyeView from '../assets/eye-view.svg';
import OutlineSource from '../assets/outline-source.svg';
import Trash from '../assets/red-trash.svg';
import SearchIcon from '../assets/search.svg';
import { useOutsideAlerter } from '../hooks';
interface FileNode {
type?: string;
@@ -58,6 +59,17 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
} | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
const searchDropdownRef = useRef<HTMLDivElement>(null);
useOutsideAlerter(
searchDropdownRef,
() => {
setSearchQuery('');
setSearchResults([]);
},
[],
false
);
const handleFileClick = (fileName: string) => {
const fullPath = [...currentPath, fileName].join('/');
@@ -435,103 +447,115 @@ const FileTreeComponent: React.FC<FileTreeComponentProps> = ({
setSearchQuery('');
setSearchResults([]);
};
const renderFileSearch = () => {
return (
<div className="w-[283px]" ref={searchDropdownRef}>
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
if (directoryStructure) {
setSearchResults(searchFiles(e.target.value, directoryStructure));
}
}}
placeholder={t('settings.documents.searchFiles')}
className={`w-full px-4 py-2 pl-10 border border-[#D1D9E0] dark:border-[#6A6A6A] ${
searchQuery ? 'rounded-t-md rounded-b-none border-b-0' : 'rounded-md'
} bg-transparent dark:text-[#E0E0E0] focus:outline-none`}
/>
<img
src={SearchIcon}
alt="Search"
className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 opacity-60"
/>
{searchQuery && (
<div className="absolute z-10 w-full border border-[#D1D9E0] dark:border-[#6A6A6A] rounded-b-md bg-white dark:bg-[#1F2023] shadow-lg max-h-[calc(100vh-200px)] overflow-y-auto">
{searchResults.length === 0 ? (
<div className="text-sm text-gray-500 dark:text-gray-400 text-center py-2">
{t('settings.documents.noResults')}
</div>
) : (
searchResults.map((result, index) => (
<div
key={index}
onClick={() => handleSearchSelect(result)}
title={result.path}
className={`flex items-center px-3 py-2 cursor-pointer hover:bg-[#ECEEEF] dark:hover:bg-[#27282D] ${
index !== searchResults.length - 1 ? "border-b border-[#D1D9E0] dark:border-[#6A6A6A]" : ""
}`}
>
<img
src={result.isFile ? FileIcon : FolderIcon}
alt={result.isFile ? "File" : "Folder"}
className="flex-shrink-0 w-4 h-4 mr-2"
/>
<span className="text-sm dark:text-[#E0E0E0]">
{result.path.split('/').pop() || result.path}
</span>
</div>
))
)}
</div>
)}
</div>
</div>
);
};
return (
<>
<div className="mb-4">{renderPathNavigation()}</div>
{selectedFile ? (
<div className="flex">
{/* Search Panel */}
<div className="w-[283px] min-w-[283px] pr-4">
<div className="relative">
<input
type="text"
value={searchQuery}
onChange={(e) => {
setSearchQuery(e.target.value);
if (directoryStructure) {
setSearchResults(searchFiles(e.target.value, directoryStructure));
}
}}
placeholder={t('settings.documents.searchFiles')}
className={`w-full px-4 py-2 pl-10 border border-[#D1D9E0] dark:border-[#6A6A6A] ${searchQuery ? 'rounded-t-md rounded-b-none border-b-0' : 'rounded-md'
} bg-transparent dark:text-[#E0E0E0] focus:outline-none`}
/>
<img
src={SearchIcon}
alt="Search"
className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 opacity-60"
/>
{searchQuery && (
<div className="absolute z-10 w-full border border-[#D1D9E0] dark:border-[#6A6A6A] rounded-b-md bg-white dark:bg-[#1F2023] shadow-lg max-h-[calc(100vh-200px)] overflow-y-auto">
{searchResults.map((result, index) => {
const name = result.path.split('/').pop() || result.path;
return (
<div
key={index}
onClick={() => handleSearchSelect(result)}
title={result.path}
className={`flex items-center px-3 py-2 cursor-pointer hover:bg-[#ECEEEF] dark:hover:bg-[#27282D] ${index !== searchResults.length - 1 ? "border-b border-[#D1D9E0] dark:border-[#6A6A6A]" : ""
}`}
>
<img
src={result.isFile ? FileIcon : FolderIcon}
alt={result.isFile ? "File" : "Folder"}
className="flex-shrink-0 w-4 h-4 mr-2"
/>
<span className="text-sm dark:text-[#E0E0E0]">
{name}
</span>
</div>
);
})}
{searchResults.length === 0 && (
<div className="text-sm text-gray-500 dark:text-gray-400 text-center py-2">
{t('settings.documents.noResults')}
</div>
)}
</div>
)}
</div>
</div>
<div className="flex-1 pl-4 pt-0">
<DocumentChunks
documentId={docId}
documentName={sourceName}
handleGoBack={() => setSelectedFile(null)}
path={selectedFile.id}
showHeader={false}
renderFileSearch={renderFileSearch}
/>
</div>
</div>
) : (
<div className="mt-8 flex flex-col">
<div className="overflow-x-auto rounded-[6px] border border-[#D1D9E0] dark:border-[#6A6A6A]">
<table className="min-w-full table-fixed bg-transparent">
<thead className="bg-gray-100 dark:bg-[#27282D]">
<tr className="border-b border-[#D1D9E0] dark:border-[#6A6A6A]">
<th className="w-3/5 px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-[#59636E]">
Name
</th>
<th className="w-1/5 px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-[#59636E]">
Tokens
</th>
<th className="w-1/5 px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-[#59636E]">
Size
</th>
<th className="w-[60px] px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-[#59636E]">
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody className="[&>tr:last-child]:border-b-0">
{renderFileTree(currentDirectory)}
</tbody>
</table>
<div className="flex flex-col">
<div className="mb-4">
{renderPathNavigation()}
</div>
<div className="flex gap-4">
{/* Left side: Search dropdown */}
{renderFileSearch()}
{/* Right side: File table */}
<div className="flex-1">
<div className="overflow-x-auto rounded-[6px] border border-[#D1D9E0] dark:border-[#6A6A6A]">
<table className="min-w-full table-fixed bg-transparent">
<thead className="bg-gray-100 dark:bg-[#27282D]">
<tr className="border-b border-[#D1D9E0] dark:border-[#6A6A6A]">
<th className="w-3/5 px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-[#59636E]">
Name
</th>
<th className="w-1/5 px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-[#59636E]">
Tokens
</th>
<th className="w-1/5 px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-[#59636E]">
Size
</th>
<th className="w-[60px] px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-[#59636E]">
<span className="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody className="[&>tr:last-child]:border-b-0">
{renderFileTree(currentDirectory)}
</tbody>
</table>
</div>
</div>
</div>
</div>
)}

View File

@@ -272,7 +272,6 @@ export default function Documents({
documentId={documentToView.id || ''}
documentName={documentToView.name}
handleGoBack={() => setDocumentToView(undefined)}
showHeader={true}
/>
)}
</div>