import { useEffect, useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import * as PropTypes from 'prop-types' import { useNavigate, useParams } from 'react-router-dom' // material-ui import { Box, Stack, Typography, TableContainer, Paper, Table, TableHead, TableRow, TableCell, TableBody, Chip, Menu, MenuItem, Divider, Button, Skeleton } from '@mui/material' import { alpha, styled, useTheme } from '@mui/material/styles' import { tableCellClasses } from '@mui/material/TableCell' // project imports import MainCard from '@/ui-component/cards/MainCard' import AddDocStoreDialog from '@/views/docstore/AddDocStoreDialog' import { BackdropLoader } from '@/ui-component/loading/BackdropLoader' import DocumentLoaderListDialog from '@/views/docstore/DocumentLoaderListDialog' import ErrorBoundary from '@/ErrorBoundary' import { StyledButton } from '@/ui-component/button/StyledButton' import ViewHeader from '@/layout/MainLayout/ViewHeader' import DeleteDocStoreDialog from './DeleteDocStoreDialog' import { Available } from '@/ui-component/rbac/available' import { PermissionIconButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons' import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus' import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog' import DocStoreAPIDialog from './DocStoreAPIDialog' // API import documentsApi from '@/api/documentstore' // Hooks import useApi from '@/hooks/useApi' import useNotifier from '@/utils/useNotifier' import { useAuth } from '@/hooks/useAuth' import { getFileName } from '@/utils/genericHelper' import useConfirm from '@/hooks/useConfirm' // icons import { IconPlus, IconRefresh, IconX, IconVectorBezier2 } from '@tabler/icons-react' import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' import FileDeleteIcon from '@mui/icons-material/Delete' import FileEditIcon from '@mui/icons-material/Edit' import FileChunksIcon from '@mui/icons-material/AppRegistration' import NoteAddIcon from '@mui/icons-material/NoteAdd' import SearchIcon from '@mui/icons-material/Search' import RefreshIcon from '@mui/icons-material/Refresh' import CodeIcon from '@mui/icons-material/Code' import doc_store_details_emptySVG from '@/assets/images/doc_store_details_empty.svg' // store import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions' import { useError } from '@/store/context/ErrorContext' // ==============================|| DOCUMENTS ||============================== // const StyledTableCell = styled(TableCell)(({ theme }) => ({ borderColor: theme.palette.grey[900] + 25, padding: '6px 16px', [`&.${tableCellClasses.head}`]: { color: theme.palette.grey[900] }, [`&.${tableCellClasses.body}`]: { fontSize: 14, height: 64 } })) const StyledTableRow = styled(TableRow)(() => ({ // hide last border '&:last-child td, &:last-child th': { border: 0 } })) const StyledMenu = styled((props) => ( ))(({ theme }) => ({ '& .MuiPaper-root': { borderRadius: 6, marginTop: theme.spacing(1), minWidth: 180, boxShadow: 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px', '& .MuiMenu-list': { padding: '4px 0' }, '& .MuiMenuItem-root': { '& .MuiSvgIcon-root': { fontSize: 18, color: theme.palette.text.secondary, marginRight: theme.spacing(1.5) }, '&:active': { backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity) } } } })) const DocumentStoreDetails = () => { const theme = useTheme() const customization = useSelector((state) => state.customization) const navigate = useNavigate() const dispatch = useDispatch() const { hasAssignedWorkspace } = useAuth() useNotifier() const { confirm } = useConfirm() const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args)) const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args)) const { error, setError } = useError() const { hasPermission } = useAuth() const getSpecificDocumentStore = useApi(documentsApi.getSpecificDocumentStore) const [isLoading, setLoading] = useState(true) const [isBackdropLoading, setBackdropLoading] = useState(false) const [showDialog, setShowDialog] = useState(false) const [documentStore, setDocumentStore] = useState({}) const [dialogProps, setDialogProps] = useState({}) const [showDocumentLoaderListDialog, setShowDocumentLoaderListDialog] = useState(false) const [documentLoaderListDialogProps, setDocumentLoaderListDialogProps] = useState({}) const [showDeleteDocStoreDialog, setShowDeleteDocStoreDialog] = useState(false) const [deleteDocStoreDialogProps, setDeleteDocStoreDialogProps] = useState({}) const [showDocStoreAPIDialog, setShowDocStoreAPIDialog] = useState(false) const [docStoreAPIDialogProps, setDocStoreAPIDialogProps] = useState({}) const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) const { storeId } = useParams() const openPreviewSettings = (id) => { navigate('/document-stores/' + storeId + '/' + id) } const showStoredChunks = (id) => { navigate('/document-stores/chunks/' + storeId + '/' + id) } const showVectorStoreQuery = (id) => { navigate('/document-stores/query/' + id) } const onDocLoaderSelected = (docLoaderComponentName) => { setShowDocumentLoaderListDialog(false) navigate('/document-stores/' + storeId + '/' + docLoaderComponentName) } const showVectorStore = (id) => { navigate('/document-stores/vector/' + id) } const listLoaders = () => { const dialogProp = { title: 'Select Document Loader' } setDocumentLoaderListDialogProps(dialogProp) setShowDocumentLoaderListDialog(true) } const deleteVectorStoreDataFromStore = async (storeId) => { try { await documentsApi.deleteVectorStoreDataFromStore(storeId) } catch (error) { console.error(error) } } const onDocStoreDelete = async (type, file, removeFromVectorStore) => { setBackdropLoading(true) setShowDeleteDocStoreDialog(false) if (type === 'STORE') { if (removeFromVectorStore) { await deleteVectorStoreDataFromStore(storeId) } try { const deleteResp = await documentsApi.deleteDocumentStore(storeId) setBackdropLoading(false) if (deleteResp.data) { enqueueSnackbar({ message: 'Store, Loader and associated document chunks deleted', options: { key: new Date().getTime() + Math.random(), variant: 'success', action: (key) => ( ) } }) navigate('/document-stores/') } } catch (error) { setBackdropLoading(false) setError(error) enqueueSnackbar({ message: `Failed to delete Document Store: ${ typeof error.response.data === 'object' ? error.response.data.message : error.response.data }`, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( ) } }) } } else if (type === 'LOADER') { try { const deleteResp = await documentsApi.deleteLoaderFromStore(storeId, file.id) setBackdropLoading(false) if (deleteResp.data) { enqueueSnackbar({ message: 'Loader and associated document chunks deleted', options: { key: new Date().getTime() + Math.random(), variant: 'success', action: (key) => ( ) } }) onConfirm() } } catch (error) { setError(error) setBackdropLoading(false) enqueueSnackbar({ message: `Failed to delete Document Loader: ${ typeof error.response.data === 'object' ? error.response.data.message : error.response.data }`, options: { key: new Date().getTime() + Math.random(), variant: 'error', persist: true, action: (key) => ( ) } }) } } } const onLoaderDelete = (file, vectorStoreConfig, recordManagerConfig) => { const props = { title: `Delete`, description: `Delete Loader ${file.loaderName} ? This will delete all the associated document chunks.`, vectorStoreConfig, recordManagerConfig, type: 'LOADER', file } setDeleteDocStoreDialogProps(props) setShowDeleteDocStoreDialog(true) } const onStoreDelete = (vectorStoreConfig, recordManagerConfig) => { const props = { title: `Delete`, description: `Delete Store ${getSpecificDocumentStore.data?.name} ? This will delete all the associated loaders and document chunks.`, vectorStoreConfig, recordManagerConfig, type: 'STORE' } setDeleteDocStoreDialogProps(props) setShowDeleteDocStoreDialog(true) } const onStoreRefresh = async (storeId) => { const confirmPayload = { title: `Refresh all loaders and upsert all chunks?`, description: `This will re-process all loaders and upsert all chunks. This action might take some time.`, confirmButtonName: 'Refresh', cancelButtonName: 'Cancel' } const isConfirmed = await confirm(confirmPayload) if (isConfirmed) { setAnchorEl(null) setBackdropLoading(true) try { const resp = await documentsApi.refreshLoader(storeId) if (resp.data) { enqueueSnackbar({ message: 'Document store refresh successfully!', options: { key: new Date().getTime() + Math.random(), variant: 'success', action: (key) => ( ) } }) } setBackdropLoading(false) } catch (error) { setBackdropLoading(false) enqueueSnackbar({ message: `Failed to refresh document store: ${ typeof error.response.data === 'object' ? error.response.data.message : error.response.data }`, options: { key: new Date().getTime() + Math.random(), variant: 'error', action: (key) => ( ) } }) } } } const onEditClicked = () => { const data = { name: documentStore.name, description: documentStore.description, id: documentStore.id } const dialogProp = { title: 'Edit Document Store', type: 'EDIT', cancelButtonName: 'Cancel', confirmButtonName: 'Update', data: data } setDialogProps(dialogProp) setShowDialog(true) } const onConfirm = () => { setShowDialog(false) getSpecificDocumentStore.request(storeId) } const handleClick = (event) => { event.preventDefault() event.stopPropagation() setAnchorEl(event.currentTarget) } const onViewUpsertAPI = (storeId, loaderId) => { const props = { title: `Upsert API`, storeId, loaderId } setDocStoreAPIDialogProps(props) setShowDocStoreAPIDialog(true) } const handleClose = () => { setAnchorEl(null) } useEffect(() => { getSpecificDocumentStore.request(storeId) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { if (getSpecificDocumentStore.data) { const workspaceId = getSpecificDocumentStore.data.workspaceId if (!hasAssignedWorkspace(workspaceId)) { navigate('/unauthorized') return } setDocumentStore(getSpecificDocumentStore.data) } // eslint-disable-next-line react-hooks/exhaustive-deps }, [getSpecificDocumentStore.data]) useEffect(() => { setLoading(getSpecificDocumentStore.loading) }, [getSpecificDocumentStore.loading]) return ( <> {error ? ( ) : ( navigate('/document-stores')} onEdit={() => onEditClicked()} > {(documentStore?.status === 'STALE' || documentStore?.status === 'UPSERTING') && ( )} } onClick={listLoaders} > Add Document Loader showStoredChunks('all')} disableRipple > View & Edit Chunks showVectorStore(documentStore.id)} disableRipple > Upsert All Chunks showVectorStoreQuery(documentStore.id)} disableRipple > Retrieval Query onStoreRefresh(documentStore.id)} disableRipple title='Re-process all loaders and upsert all chunks' > Refresh onStoreDelete(documentStore.vectorStoreConfig, documentStore.recordManagerConfig)} disableRipple > Delete {getSpecificDocumentStore.data?.whereUsed?.length > 0 && (
Chatflows Used:
{getSpecificDocumentStore.data.whereUsed.map((chatflowUsed, index) => ( navigate('/canvas/' + chatflowUsed.id)} > ))}
)} {!isLoading && documentStore && !documentStore?.loaders?.length ? ( doc_store_details_emptySVG
No Document Added Yet
} onClick={listLoaders} > Add Document Loader
) : (   Loader Splitter Source(s) Chunks Chars Actions {isLoading ? ( <> ) : ( <> {documentStore?.loaders && documentStore?.loaders.length > 0 && documentStore?.loaders.map((loader, index) => ( openPreviewSettings(loader.id)} onViewChunksClick={() => showStoredChunks(loader.id)} onDeleteClick={() => onLoaderDelete( loader, documentStore?.vectorStoreConfig, documentStore?.recordManagerConfig ) } onChunkUpsert={() => navigate(`/document-stores/vector/${documentStore.id}/${loader.id}`) } onViewUpsertAPI={() => onViewUpsertAPI(documentStore.id, loader.id)} /> ))} )}
)} {getSpecificDocumentStore.data?.status === 'STALE' && (
Some files are pending processing. Please Refresh to get the latest status.
)}
)}
{showDialog && ( setShowDialog(false)} onConfirm={onConfirm} /> )} {showDocumentLoaderListDialog && ( setShowDocumentLoaderListDialog(false)} onDocLoaderSelected={onDocLoaderSelected} /> )} {showDeleteDocStoreDialog && ( setShowDeleteDocStoreDialog(false)} onDelete={onDocStoreDelete} /> )} {showDocStoreAPIDialog && ( setShowDocStoreAPIDialog(false)} /> )} {isBackdropLoading && } ) } function LoaderRow(props) { const [anchorEl, setAnchorEl] = useState(null) const open = Boolean(anchorEl) const handleClick = (event) => { event.preventDefault() event.stopPropagation() setAnchorEl(event.currentTarget) } const handleClose = () => { setAnchorEl(null) } const formatSources = (files, source) => { // Prefer files.name when files array exists and has items if (files && Array.isArray(files) && files.length > 0) { return files.map((file) => file.name).join(', ') } // Fallback to original source logic if (source && typeof source === 'string' && source.includes('base64')) { return getFileName(source) } if (source && typeof source === 'string' && source.startsWith('[') && source.endsWith(']')) { return JSON.parse(source).join(', ') } return source || 'No source' } return ( <>
{props.loader.loaderName} {props.loader.splitterName ?? 'None'} {formatSources(props.loader.files, props.loader.source)} {props.loader.totalChunks && } {props.loader.totalChars && }
Preview & Process View & Edit Chunks Upsert Chunks View API Delete
) } LoaderRow.propTypes = { loader: PropTypes.any, index: PropTypes.number, open: PropTypes.bool, theme: PropTypes.any, onViewChunksClick: PropTypes.func, onEditClick: PropTypes.func, onDeleteClick: PropTypes.func, onChunkUpsert: PropTypes.func, onViewUpsertAPI: PropTypes.func } export default DocumentStoreDetails