Feature/DocumentStore (#2106)

* datasource: initial commit

* datasource: datasource details and chunks

* datasource: Document Store Node

* more changes

* Document Store - Base functionality

* Document Store Loader Component

* Document Store Loader Component

* before merging the modularity PR

* after merging the modularity PR

* preview mode

* initial draft PR

* fixes

* minor updates and  fixes

* preview with loader and splitter

* preview with credential

* show stored chunks

* preview update...

* edit config

* save, preview and other changes

* save, preview and other changes

* save, process and other changes

* save, process and other changes

* alpha1 - for internal testing

* rerouting urls

* bug fix on new leader create

* pagination support for chunks

* delete document store

* Update pnpm-lock.yaml

* doc store card view

* Update store files to use updated storage functions, Document Store Table View and other changes

* ui changes

* add expanded chunk dialog, improve ui

* change throw Error to InternalError

* Bug Fixes and removal of subFolder, adding of view chunks for store

* lint fixes

* merge changes

* DocumentStoreStatus component

* ui changes for doc store

* add remove metadata key field, add custom document loader

* add chatflows used doc store chips

* add types/interfaces to DocumentStore Services

* document loader list dialog title bar color change

* update interfaces

* Whereused Chatflow Name and Added chunkNo to retain order of created chunks.

* use typeorm order chunkNo, ui changes

---------

Co-authored-by: Henry <hzj94@hotmail.com>
Co-authored-by: Henry Heng <henryheng@flowiseai.com>
This commit is contained in:
Vinod Kiran
2024-05-06 19:53:27 +05:30
committed by GitHub
parent af4e28aa91
commit 40e36d1b39
91 changed files with 38713 additions and 32791 deletions
+5 -4
View File
@@ -1,3 +1,5 @@
import * as PropTypes from 'prop-types'
import moment from 'moment/moment'
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from '@/store/actions'
@@ -28,6 +30,8 @@ import MainCard from '@/ui-component/cards/MainCard'
import { StyledButton } from '@/ui-component/button/StyledButton'
import APIKeyDialog from './APIKeyDialog'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// API
import apiKeyApi from '@/api/apikey'
@@ -42,12 +46,9 @@ import useNotifier from '@/utils/useNotifier'
// Icons
import { IconTrash, IconEdit, IconCopy, IconChevronsUp, IconChevronsDown, IconX, IconPlus, IconEye, IconEyeOff } from '@tabler/icons'
import APIEmptySVG from '@/assets/images/api_empty.svg'
import * as PropTypes from 'prop-types'
import moment from 'moment/moment'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// ==============================|| APIKey ||============================== //
const StyledTableCell = styled(TableCell)(({ theme }) => ({
borderColor: theme.palette.grey[900] + 25,
padding: '6px 16px',
+6 -8
View File
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
// material-ui
import { Box, Skeleton, Stack, ToggleButton } from '@mui/material'
import { Box, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
@@ -12,6 +12,10 @@ import { gridSpacing } from '@/store/constant'
import WorkflowEmptySVG from '@/assets/images/workflow_empty.svg'
import LoginDialog from '@/ui-component/dialog/LoginDialog'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import { FlowListTable } from '@/ui-component/table/FlowListTable'
import { StyledButton } from '@/ui-component/button/StyledButton'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// API
import chatflowsApi from '@/api/chatflows'
@@ -24,12 +28,6 @@ import { baseURL } from '@/store/constant'
// icons
import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons'
import * as React from 'react'
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
import { FlowListTable } from '@/ui-component/table/FlowListTable'
import { StyledButton } from '@/ui-component/button/StyledButton'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
// ==============================|| CHATFLOWS ||============================== //
@@ -45,7 +43,7 @@ const Chatflows = () => {
const [loginDialogProps, setLoginDialogProps] = useState({})
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
const [view, setView] = React.useState(localStorage.getItem('flowDisplayStyle') || 'card')
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
const handleChange = (event, nextView) => {
if (nextView === null) return
@@ -0,0 +1,228 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useState, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import {
HIDE_CANVAS_DIALOG,
SHOW_CANVAS_DIALOG,
enqueueSnackbar as enqueueSnackbarAction,
closeSnackbar as closeSnackbarAction
} from '@/store/actions'
// Material
import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Box, Typography, OutlinedInput } from '@mui/material'
// Project imports
import { StyledButton } from '@/ui-component/button/StyledButton'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
// Icons
import { IconX, IconFiles } from '@tabler/icons'
// API
import documentStoreApi from '@/api/documentstore'
// utils
import useNotifier from '@/utils/useNotifier'
const AddDocStoreDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [documentStoreName, setDocumentStoreName] = useState('')
const [documentStoreDesc, setDocumentStoreDesc] = useState('')
const [dialogType, setDialogType] = useState('ADD')
const [docStoreId, setDocumentStoreId] = useState()
useEffect(() => {
setDialogType(dialogProps.type)
if (dialogProps.type === 'EDIT' && dialogProps.data) {
setDocumentStoreName(dialogProps.data.name)
setDocumentStoreDesc(dialogProps.data.description)
setDocumentStoreId(dialogProps.data.id)
} else if (dialogProps.type === 'ADD') {
setDocumentStoreName('')
setDocumentStoreDesc('')
}
return () => {
setDocumentStoreName('')
setDocumentStoreDesc('')
}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const createDocumentStore = async () => {
try {
const obj = {
name: documentStoreName,
description: documentStoreDesc
}
const createResp = await documentStoreApi.createDocumentStore(obj)
if (createResp.data) {
enqueueSnackbar({
message: 'New Document Store created.',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(createResp.data.id)
}
} catch (err) {
const errorData = typeof err === 'string' ? err : err.response?.data || `${err.response.data.message}`
enqueueSnackbar({
message: `Failed to add new Document Store: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const updateDocumentStore = async () => {
try {
const saveObj = {
name: documentStoreName,
description: documentStoreDesc
}
const saveResp = await documentStoreApi.updateDocumentStore(docStoreId, saveObj)
if (saveResp.data) {
enqueueSnackbar({
message: 'Document Store Updated!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm(saveResp.data.id)
}
} catch (error) {
const errorData = error.response?.data || `${error.response?.status}: ${error.response?.statusText}`
enqueueSnackbar({
message: `Failed to update Document Store: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onCancel()
}
}
const component = show ? (
<Dialog
fullWidth
maxWidth='sm'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle style={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<IconFiles style={{ marginRight: '10px' }} />
{dialogProps.title}
</div>
</DialogTitle>
<DialogContent>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
Name<span style={{ color: 'red' }}>&nbsp;*</span>
</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
size='small'
sx={{ mt: 1 }}
type='string'
fullWidth
key='documentStoreName'
onChange={(e) => setDocumentStoreName(e.target.value)}
value={documentStoreName ?? ''}
/>
</Box>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>Description</Typography>
<div style={{ flexGrow: 1 }}></div>
</div>
<OutlinedInput
size='small'
multiline={true}
rows={7}
sx={{ mt: 1 }}
type='string'
fullWidth
key='documentStoreDesc'
onChange={(e) => setDocumentStoreDesc(e.target.value)}
value={documentStoreDesc ?? ''}
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => onCancel()}>Cancel</Button>
<StyledButton
disabled={!documentStoreName}
variant='contained'
onClick={() => (dialogType === 'ADD' ? createDocumentStore() : updateDocumentStore())}
>
{dialogProps.confirmButtonName}
</StyledButton>
</DialogActions>
<ConfirmDialog />
</Dialog>
) : null
return createPortal(component, portalElement)
}
AddDocStoreDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onConfirm: PropTypes.func
}
export default AddDocStoreDialog
@@ -0,0 +1,275 @@
import PropTypes from 'prop-types'
import { useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { Box, Typography, IconButton, Button } from '@mui/material'
import { IconArrowsMaximize, IconAlertTriangle } from '@tabler/icons'
// project import
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
import { MultiDropdown } from '@/ui-component/dropdown/MultiDropdown'
import { AsyncDropdown } from '@/ui-component/dropdown/AsyncDropdown'
import { Input } from '@/ui-component/input/Input'
import { DataGrid } from '@/ui-component/grid/DataGrid'
import { File } from '@/ui-component/file/File'
import { SwitchInput } from '@/ui-component/switch/Switch'
import { JsonEditorInput } from '@/ui-component/json/JsonEditor'
import { TooltipWithParser } from '@/ui-component/tooltip/TooltipWithParser'
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
import ExpandTextDialog from '@/ui-component/dialog/ExpandTextDialog'
import ManageScrapedLinksDialog from '@/ui-component/dialog/ManageScrapedLinksDialog'
import CredentialInputHandler from '@/views/canvas/CredentialInputHandler'
// const
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
// ===========================|| DocStoreInputHandler ||=========================== //
const DocStoreInputHandler = ({ inputParam, data, disabled = false }) => {
const customization = useSelector((state) => state.customization)
const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({})
const [showManageScrapedLinksDialog, setShowManageScrapedLinksDialog] = useState(false)
const [manageScrapedLinksDialogProps, setManageScrapedLinksDialogProps] = useState({})
const onExpandDialogClicked = (value, inputParam) => {
const dialogProps = {
value,
inputParam,
disabled,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}
setExpandDialogProps(dialogProps)
setShowExpandDialog(true)
}
const onManageLinksDialogClicked = (url, selectedLinks, relativeLinksMethod, limit) => {
const dialogProps = {
url,
relativeLinksMethod,
limit,
selectedLinks,
confirmButtonName: 'Save',
cancelButtonName: 'Cancel'
}
setManageScrapedLinksDialogProps(dialogProps)
setShowManageScrapedLinksDialog(true)
}
const onManageLinksDialogSave = (url, links) => {
setShowManageScrapedLinksDialog(false)
data.inputs.url = url
data.inputs.selectedLinks = links
}
const onExpandDialogSave = (newValue, inputParamName) => {
setShowExpandDialog(false)
data.inputs[inputParamName] = newValue
}
return (
<div>
{inputParam && (
<>
<Box sx={{ p: 2 }}>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Typography>
{inputParam.label}
{!inputParam.optional && <span style={{ color: 'red' }}>&nbsp;*</span>}
{inputParam.description && <TooltipWithParser style={{ marginLeft: 10 }} title={inputParam.description} />}
</Typography>
<div style={{ flexGrow: 1 }}></div>
{((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && (
<IconButton
size='small'
sx={{
height: 25,
width: 25
}}
title='Expand'
color='primary'
onClick={() =>
onExpandDialogClicked(data.inputs[inputParam.name] ?? inputParam.default ?? '', inputParam)
}
>
<IconArrowsMaximize />
</IconButton>
)}
</div>
{inputParam.warning && (
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
background: 'rgb(254,252,191)',
padding: 10,
marginTop: 10,
marginBottom: 10
}}
>
<IconAlertTriangle size={30} color='orange' />
<span style={{ color: 'rgb(116,66,16)', marginLeft: 10 }}>{inputParam.warning}</span>
</div>
)}
{inputParam.type === 'credential' && (
<CredentialInputHandler
disabled={disabled}
data={data}
inputParam={inputParam}
onSelect={(newValue) => {
data.credential = newValue
data.inputs[FLOWISE_CREDENTIAL_ID] = newValue // in case data.credential is not updated
}}
/>
)}
{inputParam.type === 'file' && (
<File
disabled={disabled}
fileType={inputParam.fileType || '*'}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'Choose a file to upload'}
/>
)}
{inputParam.type === 'boolean' && (
<SwitchInput
disabled={disabled}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? false}
/>
)}
{inputParam.type === 'datagrid' && (
<DataGrid
disabled={disabled}
columns={inputParam.datagrid}
hideFooter={true}
rows={data.inputs[inputParam.name] ?? JSON.stringify(inputParam.default) ?? []}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
/>
)}
{inputParam.type === 'code' && (
<>
<div style={{ height: '5px' }}></div>
<div style={{ height: inputParam.rows ? '100px' : '200px' }}>
<CodeEditor
disabled={disabled}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
height={inputParam.rows ? '100px' : '200px'}
theme={customization.isDarkMode ? 'dark' : 'light'}
lang={'js'}
placeholder={inputParam.placeholder}
onValueChange={(code) => (data.inputs[inputParam.name] = code)}
basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }}
/>
</div>
</>
)}
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
<Input
key={data.inputs[inputParam.name]}
disabled={disabled}
inputParam={inputParam}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
nodeId={data.id}
/>
)}
{inputParam.type === 'json' && (
<JsonEditorInput
disabled={disabled}
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
isDarkMode={customization.isDarkMode}
/>
)}
{inputParam.type === 'options' && (
<Dropdown
disabled={disabled}
name={inputParam.name}
options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/>
)}
{inputParam.type === 'multiOptions' && (
<MultiDropdown
disabled={disabled}
name={inputParam.name}
options={inputParam.options}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
/>
)}
{inputParam.type === 'asyncOptions' && (
<>
{data.inputParams.length === 1 && <div style={{ marginTop: 10 }} />}
<div style={{ display: 'flex', flexDirection: 'row' }}>
<AsyncDropdown
disabled={disabled}
name={inputParam.name}
nodeData={data}
value={data.inputs[inputParam.name] ?? inputParam.default ?? 'choose an option'}
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
onCreateNew={() => addAsyncOption(inputParam.name)}
/>
</div>
</>
)}
{(data.name === 'cheerioWebScraper' ||
data.name === 'puppeteerWebScraper' ||
data.name === 'playwrightWebScraper') &&
inputParam.name === 'url' && (
<>
<Button
style={{
display: 'flex',
flexDirection: 'row',
width: '100%'
}}
disabled={disabled}
sx={{ borderRadius: '12px', width: '100%', mt: 1 }}
variant='outlined'
onClick={() =>
onManageLinksDialogClicked(
data.inputs[inputParam.name] ?? inputParam.default ?? '',
data.inputs.selectedLinks,
data.inputs['relativeLinksMethod'] ?? 'webCrawl',
parseInt(data.inputs['limit']) ?? 0
)
}
>
Manage Links
</Button>
<ManageScrapedLinksDialog
show={showManageScrapedLinksDialog}
dialogProps={manageScrapedLinksDialogProps}
onCancel={() => setShowManageScrapedLinksDialog(false)}
onSave={onManageLinksDialogSave}
/>
</>
)}
</Box>
</>
)}
<ExpandTextDialog
show={showExpandDialog}
dialogProps={expandDialogProps}
onCancel={() => setShowExpandDialog(false)}
onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)}
></ExpandTextDialog>
</div>
)
}
DocStoreInputHandler.propTypes = {
inputParam: PropTypes.object,
data: PropTypes.object,
disabled: PropTypes.bool
}
export default DocStoreInputHandler
@@ -0,0 +1,189 @@
import { useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { useDispatch } from 'react-redux'
import PropTypes from 'prop-types'
import { List, ListItemButton, Dialog, DialogContent, DialogTitle, Box, OutlinedInput, InputAdornment, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { IconSearch, IconX } from '@tabler/icons'
// API
import documentStoreApi from '@/api/documentstore'
// const
import { baseURL } from '@/store/constant'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
import useApi from '@/hooks/useApi'
const DocumentLoaderListDialog = ({ show, dialogProps, onCancel, onDocLoaderSelected }) => {
const portalElement = document.getElementById('portal')
const dispatch = useDispatch()
const theme = useTheme()
const [searchValue, setSearchValue] = useState('')
const [documentLoaders, setDocumentLoaders] = useState([])
const getDocumentLoadersApi = useApi(documentStoreApi.getDocumentLoaders)
const onSearchChange = (val) => {
setSearchValue(val)
}
function filterFlows(data) {
return data.name.toLowerCase().indexOf(searchValue.toLowerCase()) > -1
}
useEffect(() => {
if (dialogProps.documentLoaders) {
setDocumentLoaders(dialogProps.documentLoaders)
}
}, [dialogProps])
useEffect(() => {
getDocumentLoadersApi.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getDocumentLoadersApi.data) {
setDocumentLoaders(getDocumentLoadersApi.data)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getDocumentLoadersApi.data])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const component = show ? (
<Dialog
fullWidth
maxWidth='md'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle sx={{ fontSize: '1rem', p: 3, pb: 0 }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}>
<Box
sx={{
backgroundColor: theme.palette.background.paper,
pt: 2,
position: 'sticky',
top: 0,
zIndex: 10
}}
>
<OutlinedInput
sx={{ width: '100%', pr: 2, pl: 2, position: 'sticky' }}
id='input-search-credential'
value={searchValue}
onChange={(e) => onSearchChange(e.target.value)}
placeholder='Search'
startAdornment={
<InputAdornment position='start'>
<IconSearch stroke={1.5} size='1rem' color={theme.palette.grey[500]} />
</InputAdornment>
}
endAdornment={
<InputAdornment
position='end'
sx={{
cursor: 'pointer',
color: theme.palette.grey[500],
'&:hover': {
color: theme.palette.grey[900]
}
}}
title='Clear Search'
>
<IconX
stroke={1.5}
size='1rem'
onClick={() => onSearchChange('')}
style={{
cursor: 'pointer'
}}
/>
</InputAdornment>
}
aria-describedby='search-helper-text'
inputProps={{
'aria-label': 'weight'
}}
/>
</Box>
<List
sx={{
width: '100%',
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: 2,
py: 0,
zIndex: 9,
borderRadius: '10px',
[theme.breakpoints.down('md')]: {
maxWidth: 370
}
}}
>
{[...documentLoaders].filter(filterFlows).map((documentLoader) => (
<ListItemButton
alignItems='center'
key={documentLoader.name}
onClick={() => onDocLoaderSelected(documentLoader.name)}
sx={{
border: 1,
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
textAlign: 'left',
gap: 1,
p: 2
}}
>
<div
style={{
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={documentLoader.name}
src={`${baseURL}/api/v1/node-icon/${documentLoader.name}`}
/>
</div>
<Typography>{documentLoader.label}</Typography>
</ListItemButton>
))}
</List>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
DocumentLoaderListDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onDocLoaderSelected: PropTypes.func
}
export default DocumentLoaderListDialog
@@ -0,0 +1,626 @@
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import * as PropTypes from 'prop-types'
import { useNavigate } from 'react-router-dom'
// material-ui
import {
Box,
Stack,
Typography,
TableContainer,
Paper,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Chip,
Menu,
MenuItem,
Divider,
Button,
Skeleton,
IconButton
} 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 ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import DocumentLoaderListDialog from '@/views/docstore/DocumentLoaderListDialog'
import ErrorBoundary from '@/ErrorBoundary'
// API
import documentsApi from '@/api/documentstore'
// Hooks
import useApi from '@/hooks/useApi'
import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier'
// icons
import { IconPlus, IconRefresh, IconScissors, IconTrash, IconX, IconVectorBezier2 } from '@tabler/icons'
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 doc_store_details_emptySVG from '@/assets/images/doc_store_details_empty.svg'
// store
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
import { StyledButton } from '@/ui-component/button/StyledButton'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
// ==============================|| 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) => (
<Menu
elevation={0}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
{...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()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const { confirm } = useConfirm()
const getSpecificDocumentStore = useApi(documentsApi.getSpecificDocumentStore)
const [error, setError] = useState(null)
const [isLoading, setLoading] = useState(true)
const [showDialog, setShowDialog] = useState(false)
const [documentStore, setDocumentStore] = useState({})
const [dialogProps, setDialogProps] = useState({})
const [showDocumentLoaderListDialog, setShowDocumentLoaderListDialog] = useState(false)
const [documentLoaderListDialogProps, setDocumentLoaderListDialogProps] = useState({})
const URLpath = document.location.pathname.toString().split('/')
const storeId = URLpath[URLpath.length - 1] === 'document-stores' ? '' : URLpath[URLpath.length - 1]
const openPreviewSettings = (id) => {
navigate('/document-stores/' + storeId + '/' + id)
}
const showStoredChunks = (id) => {
navigate('/document-stores/chunks/' + storeId + '/' + id)
}
const onDocLoaderSelected = (docLoaderComponentName) => {
setShowDocumentLoaderListDialog(false)
navigate('/document-stores/' + storeId + '/' + docLoaderComponentName)
}
const listLoaders = () => {
const dialogProp = {
title: 'Select Document Loader'
}
setDocumentLoaderListDialogProps(dialogProp)
setShowDocumentLoaderListDialog(true)
}
const onLoaderDelete = async (file) => {
const confirmPayload = {
title: `Delete`,
description: `Delete Loader ${file.loaderName} ? This will delete all the associated document chunks.`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
const deleteResp = await documentsApi.deleteLoaderFromStore(storeId, file.id)
if (deleteResp.data) {
enqueueSnackbar({
message: 'Loader and associated document chunks deleted',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
onConfirm()
}
} catch (error) {
setError(error)
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to delete loader: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const onStoreDelete = async () => {
const confirmPayload = {
title: `Delete`,
description: `Delete Store ${getSpecificDocumentStore.data?.name} ? This will delete all the associated loaders and document chunks.`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
try {
const deleteResp = await documentsApi.deleteDocumentStore(storeId)
if (deleteResp.data) {
enqueueSnackbar({
message: 'Store, Loader and associated document chunks deleted',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
navigate('/document-stores/')
}
} catch (error) {
setError(error)
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: `Failed to delete loader: ${errorData}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
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)
}
useEffect(() => {
getSpecificDocumentStore.request(storeId)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getSpecificDocumentStore.data) {
setDocumentStore(getSpecificDocumentStore.data)
// total the chunks and chars
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificDocumentStore.data])
useEffect(() => {
if (getSpecificDocumentStore.error) {
setError(getSpecificDocumentStore.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificDocumentStore.error])
useEffect(() => {
setLoading(getSpecificDocumentStore.loading)
}, [getSpecificDocumentStore.loading])
return (
<>
<MainCard>
{error ? (
<ErrorBoundary error={error} />
) : (
<Stack flexDirection='column' sx={{ gap: 3 }}>
<ViewHeader
isBackButton={true}
isEditButton={true}
search={false}
title={documentStore?.name}
description={documentStore?.description}
onBack={() => navigate('/document-stores')}
onEdit={() => onEditClicked()}
>
<IconButton onClick={onStoreDelete} size='small' color='error' title='Delete Document Store' sx={{ mr: 2 }}>
<IconTrash />
</IconButton>
{documentStore?.status === 'STALE' && (
<Button variant='outlined' sx={{ mr: 2 }} startIcon={<IconRefresh />} onClick={onConfirm}>
Refresh
</Button>
)}
{documentStore?.totalChunks > 0 && (
<Button
variant='outlined'
sx={{ borderRadius: 2, height: '100%' }}
startIcon={<IconScissors />}
onClick={() => showStoredChunks('all')}
>
View Chunks
</Button>
)}
<StyledButton
variant='contained'
sx={{ borderRadius: 2, height: '100%', color: 'white' }}
startIcon={<IconPlus />}
onClick={listLoaders}
>
Add Document Loader
</StyledButton>
</ViewHeader>
{getSpecificDocumentStore.data?.whereUsed?.length > 0 && (
<Stack flexDirection='row' sx={{ gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
<div
style={{
paddingLeft: '15px',
paddingRight: '15px',
paddingTop: '10px',
paddingBottom: '10px',
fontSize: '0.9rem',
width: 'max-content',
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<IconVectorBezier2 style={{ marginRight: 5 }} size={17} />
Chatflows Used:
</div>
{getSpecificDocumentStore.data.whereUsed.map((chatflowUsed, index) => (
<Chip
key={index}
clickable
style={{
width: 'max-content',
borderRadius: '25px',
boxShadow: customization.isDarkMode
? '0 2px 14px 0 rgb(255 255 255 / 10%)'
: '0 2px 14px 0 rgb(32 40 45 / 10%)'
}}
label={chatflowUsed.name}
onClick={() => navigate('/canvas/' + chatflowUsed.id)}
></Chip>
))}
</Stack>
)}
{!isLoading && documentStore && !documentStore?.loaders?.length ? (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={doc_store_details_emptySVG}
alt='doc_store_details_emptySVG'
/>
</Box>
<div>No Document Added Yet</div>
<StyledButton
variant='contained'
sx={{ borderRadius: 2, height: '100%', mt: 2, color: 'white' }}
startIcon={<IconPlus />}
onClick={listLoaders}
>
Add Document Loader
</StyledButton>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>&nbsp;</StyledTableCell>
<StyledTableCell>Loader</StyledTableCell>
<StyledTableCell>Splitter</StyledTableCell>
<StyledTableCell>Source(s)</StyledTableCell>
<StyledTableCell>Chunks</StyledTableCell>
<StyledTableCell>Chars</StyledTableCell>
<StyledTableCell>Actions</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{documentStore?.loaders &&
documentStore?.loaders.length > 0 &&
documentStore?.loaders.map((loader, index) => (
<LoaderRow
key={index}
index={index}
loader={loader}
theme={theme}
onEditClick={() => openPreviewSettings(loader.id)}
onViewChunksClick={() => showStoredChunks(loader.id)}
onDeleteClick={() => onLoaderDelete(loader)}
/>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
)}
{getSpecificDocumentStore.data?.status === 'STALE' && (
<div style={{ width: '100%', textAlign: 'center', marginTop: '20px' }}>
<Typography
color='warning'
style={{ color: 'darkred', fontWeight: 500, fontStyle: 'italic', fontSize: 12 }}
>
Some files are pending processing. Please Refresh to get the latest status.
</Typography>
</div>
)}
</Stack>
)}
</MainCard>
{showDialog && (
<AddDocStoreDialog
dialogProps={dialogProps}
show={showDialog}
onCancel={() => setShowDialog(false)}
onConfirm={onConfirm}
/>
)}
{showDocumentLoaderListDialog && (
<DocumentLoaderListDialog
show={showDocumentLoaderListDialog}
dialogProps={documentLoaderListDialogProps}
onCancel={() => setShowDocumentLoaderListDialog(false)}
onDocLoaderSelected={onDocLoaderSelected}
/>
)}
<ConfirmDialog />
</>
)
}
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 = (source) => {
if (source && typeof source === 'string' && source.startsWith('[') && source.endsWith(']')) {
return JSON.parse(source).join(', ')
}
return source
}
return (
<>
<TableRow hover key={props.index} sx={{ '&:last-child td, &:last-child th': { border: 0 }, cursor: 'pointer' }}>
<StyledTableCell onClick={props.onViewChunksClick} scope='row' style={{ width: '5%' }}>
<div
style={{
display: 'flex',
width: '20px',
height: '20px',
backgroundColor: props.loader?.status === 'SYNC' ? '#00e676' : '#ffe57f',
borderRadius: '50%'
}}
></div>
</StyledTableCell>
<StyledTableCell onClick={props.onViewChunksClick} scope='row'>
{props.loader.loaderName}
</StyledTableCell>
<StyledTableCell onClick={props.onViewChunksClick}>{props.loader.splitterName ?? 'None'}</StyledTableCell>
<StyledTableCell onClick={props.onViewChunksClick}>{formatSources(props.loader.source)}</StyledTableCell>
<StyledTableCell onClick={props.onViewChunksClick}>
{props.loader.totalChunks && <Chip variant='outlined' size='small' label={props.loader.totalChunks.toLocaleString()} />}
</StyledTableCell>
<StyledTableCell onClick={props.onViewChunksClick}>
{props.loader.totalChars && <Chip variant='outlined' size='small' label={props.loader.totalChars.toLocaleString()} />}
</StyledTableCell>
<StyledTableCell>
<div>
<Button
id='document-store-action-button'
aria-controls={open ? 'document-store-action-customized-menu' : undefined}
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
disableElevation
onClick={(e) => handleClick(e)}
endIcon={<KeyboardArrowDownIcon />}
>
Options
</Button>
<StyledMenu
id='document-store-actions-customized-menu'
MenuListProps={{
'aria-labelledby': 'document-store-actions-customized-button'
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
<MenuItem onClick={props.onEditClick} disableRipple>
<FileEditIcon />
Preview & Process
</MenuItem>
<MenuItem onClick={props.onViewChunksClick} disableRipple>
<FileChunksIcon />
View & Edit Chunks
</MenuItem>
<Divider sx={{ my: 0.5 }} />
<MenuItem onClick={props.onDeleteClick} disableRipple>
<FileDeleteIcon />
Delete
</MenuItem>
</StyledMenu>
</div>
</StyledTableCell>
</TableRow>
</>
)
}
LoaderRow.propTypes = {
loader: PropTypes.any,
index: PropTypes.number,
open: PropTypes.bool,
theme: PropTypes.any,
onViewChunksClick: PropTypes.func,
onEditClick: PropTypes.func,
onDeleteClick: PropTypes.func
}
export default DocumentStoreDetails
@@ -0,0 +1,85 @@
import { useTheme } from '@mui/material'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
const DocumentStoreStatus = ({ status, isTableView }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const getColor = (status) => {
switch (status) {
case 'STALE':
return customization.isDarkMode
? [theme.palette.grey[400], theme.palette.grey[600], theme.palette.grey[700]]
: [theme.palette.grey[300], theme.palette.grey[500], theme.palette.grey[700]]
case 'EMPTY':
return ['#673ab7', '#673ab7', '#673ab7']
case 'SYNCING':
return ['#fff8e1', '#ffe57f', '#ffc107']
case 'SYNC':
return ['#cdf5d8', '#00e676', '#00c853']
case 'NEW':
return ['#e3f2fd', '#2196f3', '#1e88e5']
default:
return customization.isDarkMode
? [theme.palette.grey[300], theme.palette.grey[500], theme.palette.grey[700]]
: [theme.palette.grey[300], theme.palette.grey[500], theme.palette.grey[700]]
}
}
return (
<>
{!isTableView && (
<div
style={{
display: 'flex',
flexDirection: 'row',
alignContent: 'center',
alignItems: 'center',
background: status === 'EMPTY' ? 'transparent' : getColor(status)[0],
border: status === 'EMPTY' ? '1px solid' : 'none',
borderColor: status === 'EMPTY' ? getColor(status)[0] : 'transparent',
borderRadius: '25px',
paddingTop: '3px',
paddingBottom: '3px',
paddingLeft: '10px',
paddingRight: '10px'
}}
>
<div
style={{
width: '10px',
height: '10px',
borderRadius: '50%',
backgroundColor: status === 'EMPTY' ? 'transparent' : getColor(status)[1],
border: status === 'EMPTY' ? '3px solid' : 'none',
borderColor: status === 'EMPTY' ? getColor(status)[1] : 'transparent'
}}
/>
<span style={{ fontSize: '0.7rem', color: getColor(status)[2], marginLeft: 5 }}>{status}</span>
</div>
)}
{isTableView && (
<div
style={{
display: 'flex',
width: '20px',
height: '20px',
borderRadius: '50%',
backgroundColor: status === 'EMPTY' ? 'transparent' : getColor(status)[1],
border: status === 'EMPTY' ? '3px solid' : 'none',
borderColor: status === 'EMPTY' ? getColor(status)[1] : 'transparent'
}}
title={status}
></div>
)}
</>
)
}
DocumentStoreStatus.propTypes = {
status: PropTypes.string,
isTableView: PropTypes.bool
}
export default DocumentStoreStatus
@@ -0,0 +1,236 @@
import { useEffect, useState } from 'react'
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useDispatch, useSelector } from 'react-redux'
import ReactJson from 'flowise-react-json-view'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
// Material
import { Button, Dialog, IconButton, DialogContent, DialogTitle, Typography } from '@mui/material'
import { IconEdit, IconTrash, IconX, IconLanguage } from '@tabler/icons'
// Project imports
import { CodeEditor } from '@/ui-component/editor/CodeEditor'
const ExpandedChunkDialog = ({ show, dialogProps, onCancel, onChunkEdit, onDeleteChunk, isReadOnly }) => {
const portalElement = document.getElementById('portal')
const customization = useSelector((state) => state.customization)
const dispatch = useDispatch()
const [selectedChunk, setSelectedChunk] = useState()
const [selectedChunkNumber, setSelectedChunkNumber] = useState()
const [isEdit, setIsEdit] = useState(false)
const [contentValue, setContentValue] = useState('')
const [metadata, setMetadata] = useState({})
const onClipboardCopy = (e) => {
const src = e.src
if (Array.isArray(src) || typeof src === 'object') {
navigator.clipboard.writeText(JSON.stringify(src, null, ' '))
} else {
navigator.clipboard.writeText(src)
}
}
const onEditCancel = () => {
setContentValue(selectedChunk?.pageContent)
setMetadata(selectedChunk?.metadata ? JSON.parse(selectedChunk?.metadata) : {})
setIsEdit(false)
}
const onEditSaved = () => {
onChunkEdit(contentValue, metadata, selectedChunk)
}
useEffect(() => {
if (dialogProps.data) {
setSelectedChunk(dialogProps.data?.selectedChunk)
setContentValue(dialogProps.data?.selectedChunk?.pageContent)
setSelectedChunkNumber(dialogProps?.data.selectedChunkNumber)
if (dialogProps.data?.selectedChunk?.metadata) {
if (typeof dialogProps.data?.selectedChunk?.metadata === 'string') {
setMetadata(JSON.parse(dialogProps.data?.selectedChunk?.metadata))
} else if (typeof dialogProps.data?.selectedChunk?.metadata === 'object') {
setMetadata(dialogProps.data?.selectedChunk?.metadata)
}
}
}
return () => {
setSelectedChunk()
setSelectedChunkNumber()
setContentValue('')
setMetadata({})
setIsEdit(false)
}
}, [dialogProps])
useEffect(() => {
if (show) dispatch({ type: SHOW_CANVAS_DIALOG })
else dispatch({ type: HIDE_CANVAS_DIALOG })
return () => dispatch({ type: HIDE_CANVAS_DIALOG })
}, [show, dispatch])
const component = show ? (
<Dialog
fullWidth
maxWidth='md'
open={show}
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle style={{ fontSize: '1rem' }} id='alert-dialog-title'>
{selectedChunk && selectedChunkNumber && (
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<Typography sx={{ flex: 1 }} variant='h4'>
#{selectedChunkNumber}. {selectedChunk.id}
</Typography>
{!isEdit && !isReadOnly && (
<IconButton onClick={() => setIsEdit(true)} size='small' color='primary' title='Edit Chunk' sx={{ ml: 2 }}>
<IconEdit />
</IconButton>
)}
{isEdit && !isReadOnly && (
<Button onClick={() => onEditCancel()} color='primary' title='Cancel' sx={{ ml: 2 }}>
Cancel
</Button>
)}
{isEdit && !isReadOnly && (
<Button
onClick={() => onEditSaved(true)}
color='primary'
title='Save'
variant='contained'
sx={{ ml: 2, mr: 1 }}
>
Save
</Button>
)}
{!isEdit && !isReadOnly && (
<IconButton
onClick={() => onDeleteChunk(selectedChunk)}
size='small'
color='error'
title='Delete Chunk'
sx={{ ml: 1 }}
>
<IconTrash />
</IconButton>
)}
<IconButton onClick={onCancel} size='small' color='inherit' title='Close' sx={{ ml: 1 }}>
<IconX />
</IconButton>
</div>
)}
</DialogTitle>
<DialogContent>
{selectedChunk && selectedChunkNumber && (
<div>
<div
style={{
paddingLeft: '10px',
paddingRight: '10px',
paddingTop: '5px',
paddingBottom: '5px',
fontSize: '15px',
width: 'max-content',
borderRadius: '25px',
boxShadow: customization.isDarkMode
? '0 2px 14px 0 rgb(255 255 255 / 20%)'
: '0 2px 14px 0 rgb(32 40 45 / 20%)',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginTop: '5px',
marginBottom: '10px'
}}
>
<IconLanguage style={{ marginRight: 5 }} size={15} />
{selectedChunk?.pageContent?.length} characters
</div>
<div style={{ marginTop: '5px' }}></div>
{!isEdit && (
<CodeEditor
disabled={true}
height='max-content'
value={contentValue}
theme={customization.isDarkMode ? 'dark' : 'light'}
basicSetup={{
lineNumbers: false,
foldGutter: false,
autocompletion: false,
highlightActiveLine: false
}}
/>
)}
{isEdit && (
<CodeEditor
disabled={false}
// eslint-disable-next-line
autoFocus={true}
height='max-content'
value={contentValue}
theme={customization.isDarkMode ? 'dark' : 'light'}
basicSetup={{
lineNumbers: false,
foldGutter: false,
autocompletion: false,
highlightActiveLine: false
}}
onValueChange={(text) => setContentValue(text)}
/>
)}
<div style={{ marginTop: '20px', marginBottom: '15px' }}>
{!isEdit && (
<ReactJson
theme={customization.isDarkMode ? 'ocean' : 'rjv-default'}
src={metadata}
style={{ padding: '10px' }}
name={null}
quotesOnKeys={false}
enableClipboard={false}
displayDataTypes={false}
collapsed={1}
/>
)}
{isEdit && (
<ReactJson
theme={customization.isDarkMode ? 'ocean' : 'rjv-default'}
src={metadata}
style={{ padding: '10px' }}
name={null}
quotesOnKeys={false}
displayDataTypes={false}
enableClipboard={(e) => onClipboardCopy(e)}
onEdit={(edit) => {
setMetadata(edit.updated_src)
}}
onAdd={() => {
//console.log(add)
}}
onDelete={(deleteobj) => {
setMetadata(deleteobj.updated_src)
}}
/>
)}
</div>
</div>
)}
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
ExpandedChunkDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func,
onChunkEdit: PropTypes.func,
onDeleteChunk: PropTypes.func,
isReadOnly: PropTypes.bool
}
export default ExpandedChunkDialog
@@ -0,0 +1,666 @@
import { cloneDeep } from 'lodash'
import { useEffect, useState } from 'react'
import { validate as uuidValidate, v4 as uuidv4 } from 'uuid'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import ReactJson from 'flowise-react-json-view'
// Hooks
import useApi from '@/hooks/useApi'
// Material-UI
import { Skeleton, Toolbar, Box, Button, Card, CardContent, Grid, OutlinedInput, Stack, Typography } from '@mui/material'
import { useTheme, styled } from '@mui/material/styles'
import { IconScissors, IconArrowLeft, IconDatabaseImport, IconBook, IconX, IconEye } from '@tabler/icons'
// Project import
import MainCard from '@/ui-component/cards/MainCard'
import { StyledButton } from '@/ui-component/button/StyledButton'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
import DocStoreInputHandler from '@/views/docstore/DocStoreInputHandler'
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
import { StyledFab } from '@/ui-component/button/StyledFab'
import ErrorBoundary from '@/ErrorBoundary'
import ExpandedChunkDialog from './ExpandedChunkDialog'
// API
import nodesApi from '@/api/nodes'
import documentStoreApi from '@/api/documentstore'
import documentsApi from '@/api/documentstore'
// Const
import { baseURL, gridSpacing } from '@/store/constant'
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
// Utils
import { initNode } from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
overflow: 'auto',
position: 'relative',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
cursor: 'pointer',
'&:hover': {
background: theme.palette.card.hover,
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 20%)'
},
maxHeight: '250px',
minHeight: '250px',
maxWidth: '100%',
overflowWrap: 'break-word',
whiteSpace: 'pre-line',
padding: 1
}))
// ===========================|| DOCUMENT LOADER CHUNKS ||=========================== //
const LoaderConfigPreviewChunks = () => {
const customization = useSelector((state) => state.customization)
const navigate = useNavigate()
const theme = useTheme()
const getNodeDetailsApi = useApi(nodesApi.getSpecificNode)
const getNodesByCategoryApi = useApi(nodesApi.getNodesByCategory)
const getSpecificDocumentStoreApi = useApi(documentsApi.getSpecificDocumentStore)
const URLpath = document.location.pathname.toString().split('/')
const docLoaderNodeName = URLpath[URLpath.length - 1] === 'document-stores' ? '' : URLpath[URLpath.length - 1]
const storeId = URLpath[URLpath.length - 2] === 'document-stores' ? '' : URLpath[URLpath.length - 2]
const [selectedDocumentLoader, setSelectedDocumentLoader] = useState({})
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const [textSplitterNodes, setTextSplitterNodes] = useState([])
const [splitterOptions, setTextSplitterOptions] = useState([])
const [selectedTextSplitter, setSelectedTextSplitter] = useState({})
const [documentChunks, setDocumentChunks] = useState([])
const [totalChunks, setTotalChunks] = useState(0)
const [currentPreviewCount, setCurrentPreviewCount] = useState(0)
const [previewChunkCount, setPreviewChunkCount] = useState(20)
const [existingLoaderFromDocStoreTable, setExistingLoaderFromDocStoreTable] = useState()
const [showExpandedChunkDialog, setShowExpandedChunkDialog] = useState(false)
const [expandedChunkDialogProps, setExpandedChunkDialogProps] = useState({})
const dispatch = useDispatch()
// ==============================|| Snackbar ||============================== //
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const onSplitterChange = (name) => {
const textSplitter = (textSplitterNodes ?? []).find((splitter) => splitter.name === name)
if (textSplitter) {
setSelectedTextSplitter(textSplitter)
} else {
setSelectedTextSplitter({})
}
}
const onChunkClick = (selectedChunk, selectedChunkNumber) => {
const dialogProps = {
data: {
selectedChunk,
selectedChunkNumber
}
}
setExpandedChunkDialogProps(dialogProps)
setShowExpandedChunkDialog(true)
}
const checkMandatoryFields = () => {
let canSubmit = true
const inputParams = (selectedDocumentLoader.inputParams ?? []).filter((inputParam) => !inputParam.hidden)
for (const inputParam of inputParams) {
if (!inputParam.optional && !selectedDocumentLoader.inputs[inputParam.name]) {
canSubmit = false
break
}
}
if (!canSubmit) {
enqueueSnackbar({
message: 'Please fill in all mandatory fields.',
options: {
key: new Date().getTime() + Math.random(),
variant: 'warning',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
return canSubmit
}
const onPreviewChunks = async () => {
if (checkMandatoryFields()) {
setLoading(true)
const config = prepareConfig()
config.previewChunkCount = previewChunkCount
try {
const previewResp = await documentStoreApi.previewChunks(config)
if (previewResp.data) {
setTotalChunks(previewResp.data.totalChunks)
setDocumentChunks(previewResp.data.chunks)
setCurrentPreviewCount(previewResp.data.previewChunkCount)
}
setLoading(false)
} catch (error) {
setLoading(false)
enqueueSnackbar({
message: `Failed to preview chunks: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const onSaveAndProcess = async () => {
if (checkMandatoryFields()) {
setLoading(true)
const config = prepareConfig()
try {
const processResp = await documentStoreApi.processChunks(config)
setLoading(false)
if (processResp.data) {
enqueueSnackbar({
message: 'File submitted for processing. Redirecting to Document Store..',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
navigate('/document-stores/' + storeId)
}
} catch (error) {
setLoading(false)
enqueueSnackbar({
message: `Failed to process chunking: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
const prepareConfig = () => {
const config = {}
// Set loader id & name
if (existingLoaderFromDocStoreTable) {
config.loaderId = existingLoaderFromDocStoreTable.loaderId
config.id = existingLoaderFromDocStoreTable.id
} else {
config.loaderId = docLoaderNodeName
}
// Set store id & loader name
config.storeId = storeId
config.loaderName = selectedDocumentLoader?.label
// Set loader config
if (selectedDocumentLoader.inputs) {
config.loaderConfig = {}
Object.keys(selectedDocumentLoader.inputs).map((key) => {
config.loaderConfig[key] = selectedDocumentLoader.inputs[key]
})
}
// If Text splitter is set
if (selectedTextSplitter.inputs && selectedTextSplitter.name && Object.keys(selectedTextSplitter).length > 0) {
config.splitterId = selectedTextSplitter.name
config.splitterConfig = {}
Object.keys(selectedTextSplitter.inputs).map((key) => {
config.splitterConfig[key] = selectedTextSplitter.inputs[key]
})
const textSplitter = textSplitterNodes.find((splitter) => splitter.name === selectedTextSplitter.name)
if (textSplitter) config.splitterName = textSplitter.label
}
if (selectedDocumentLoader.credential) {
config.credential = selectedDocumentLoader.credential
}
return config
}
useEffect(() => {
if (uuidValidate(docLoaderNodeName)) {
// this is a document store edit config
getSpecificDocumentStoreApi.request(storeId)
} else {
getNodeDetailsApi.request(docLoaderNodeName)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getNodeDetailsApi.data) {
const nodeData = cloneDeep(initNode(getNodeDetailsApi.data, uuidv4()))
// If this is a document store edit config, set the existing input values
if (existingLoaderFromDocStoreTable && existingLoaderFromDocStoreTable.loaderConfig) {
nodeData.inputs = existingLoaderFromDocStoreTable.loaderConfig
}
setSelectedDocumentLoader(nodeData)
// Check if the loader has a text splitter, if yes, get the text splitter nodes
const textSplitter = nodeData.inputAnchors.find((inputAnchor) => inputAnchor.name === 'textSplitter')
if (textSplitter) {
getNodesByCategoryApi.request('Text Splitters')
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getNodeDetailsApi.data])
useEffect(() => {
if (getNodesByCategoryApi.data) {
// Set available text splitter nodes
const nodes = []
for (const node of getNodesByCategoryApi.data) {
nodes.push(cloneDeep(initNode(node, uuidv4())))
}
setTextSplitterNodes(nodes)
// Set options
const options = getNodesByCategoryApi.data.map((splitter) => ({
label: splitter.label,
name: splitter.name
}))
options.unshift({ label: 'None', name: 'none' })
setTextSplitterOptions(options)
// If this is a document store edit config, set the existing input values
if (
existingLoaderFromDocStoreTable &&
existingLoaderFromDocStoreTable.splitterConfig &&
existingLoaderFromDocStoreTable.splitterId
) {
const textSplitter = nodes.find((splitter) => splitter.name === existingLoaderFromDocStoreTable.splitterId)
if (textSplitter) {
textSplitter.inputs = cloneDeep(existingLoaderFromDocStoreTable.splitterConfig)
setSelectedTextSplitter(textSplitter)
} else {
setSelectedTextSplitter({})
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getNodesByCategoryApi.data])
useEffect(() => {
if (getSpecificDocumentStoreApi.data) {
if (getSpecificDocumentStoreApi.data?.loaders.length > 0) {
const loader = getSpecificDocumentStoreApi.data.loaders.find((loader) => loader.id === docLoaderNodeName)
if (loader) {
setExistingLoaderFromDocStoreTable(loader)
getNodeDetailsApi.request(loader.loaderId)
}
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificDocumentStoreApi.data])
useEffect(() => {
if (getSpecificDocumentStoreApi.error) {
setError(getSpecificDocumentStoreApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificDocumentStoreApi.error])
useEffect(() => {
if (getNodeDetailsApi.error) {
setError(getNodeDetailsApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getNodeDetailsApi.error])
useEffect(() => {
if (getNodesByCategoryApi.error) {
setError(getNodesByCategoryApi.error)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getNodesByCategoryApi.error])
return (
<>
<MainCard>
{error ? (
<ErrorBoundary error={error} />
) : (
<Stack flexDirection='column'>
<Box sx={{ flexGrow: 1, py: 1.25, width: '100%' }}>
<Toolbar
disableGutters={true}
sx={{
p: 0,
display: 'flex',
justifyContent: 'space-between',
width: '100%'
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'row' }}>
<StyledFab size='small' color='secondary' aria-label='back' title='Back' onClick={() => navigate(-1)}>
<IconArrowLeft />
</StyledFab>
<Typography sx={{ ml: 2, mr: 2 }} variant='h3'>
{selectedDocumentLoader?.label}
</Typography>
<div
style={{
width: 40,
height: 40,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'white',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 25%)'
}}
>
{selectedDocumentLoader?.name ? (
<img
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={selectedDocumentLoader?.name ?? 'docloader'}
src={`${baseURL}/api/v1/node-icon/${selectedDocumentLoader?.name}`}
/>
) : (
<IconBook color='black' />
)}
</div>
</Box>
<Box>
<StyledButton
variant='contained'
onClick={onSaveAndProcess}
sx={{ borderRadius: 2, height: '100%' }}
startIcon={<IconDatabaseImport />}
>
Process
</StyledButton>
</Box>
</Toolbar>
</Box>
<Box>
<Grid container spacing='2'>
<Grid item xs={4} md={6} lg={6} sm={4}>
<div
style={{
display: 'flex',
flexDirection: 'column',
paddingRight: 15
}}
>
{selectedDocumentLoader &&
Object.keys(selectedDocumentLoader).length > 0 &&
(selectedDocumentLoader.inputParams ?? [])
.filter((inputParam) => !inputParam.hidden)
.map((inputParam, index) => (
<DocStoreInputHandler
key={index}
inputParam={inputParam}
data={selectedDocumentLoader}
/>
))}
{textSplitterNodes && textSplitterNodes.length > 0 && (
<>
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'row', p: 2, mt: 5 }}>
<Typography sx={{ mr: 2 }} variant='h3'>
{(splitterOptions ?? []).find(
(splitter) => splitter.name === selectedTextSplitter?.name
)?.label ?? 'Select Text Splitter'}
</Typography>
<div
style={{
width: 40,
height: 40,
borderRadius: '50%',
backgroundColor: 'white',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 25%)'
}}
>
{selectedTextSplitter?.name ? (
<img
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={selectedTextSplitter?.name ?? 'textsplitter'}
src={`${baseURL}/api/v1/node-icon/${selectedTextSplitter?.name}`}
/>
) : (
<IconScissors color='black' />
)}
</div>
</Box>
<Box sx={{ p: 2 }}>
<Typography>Splitter</Typography>
<Dropdown
key={JSON.stringify(selectedTextSplitter)}
name='textSplitter'
options={splitterOptions}
onSelect={(newValue) => onSplitterChange(newValue)}
value={selectedTextSplitter?.name ?? 'none'}
/>
</Box>
</>
)}
{Object.keys(selectedTextSplitter).length > 0 &&
(selectedTextSplitter.inputParams ?? [])
.filter((inputParam) => !inputParam.hidden)
.map((inputParam, index) => (
<DocStoreInputHandler key={index} data={selectedTextSplitter} inputParam={inputParam} />
))}
</div>
</Grid>
<Grid item xs={8} md={6} lg={6} sm={8}>
{!documentChunks ||
(documentChunks.length === 0 && (
<div style={{ position: 'relative' }}>
<Box display='grid' gridTemplateColumns='repeat(2, 1fr)' gap={gridSpacing}>
<Skeleton
animation={false}
sx={{ bgcolor: customization.isDarkMode ? '#23262c' : '#fafafa' }}
variant='rounded'
height={160}
/>
<Skeleton
animation={false}
sx={{ bgcolor: customization.isDarkMode ? '#23262c' : '#fafafa' }}
variant='rounded'
height={160}
/>
<Skeleton
animation={false}
sx={{ bgcolor: customization.isDarkMode ? '#23262c' : '#fafafa' }}
variant='rounded'
height={160}
/>
<Skeleton
animation={false}
sx={{ bgcolor: customization.isDarkMode ? '#23262c' : '#fafafa' }}
variant='rounded'
height={160}
/>
<Skeleton
animation={false}
sx={{ bgcolor: customization.isDarkMode ? '#23262c' : '#fafafa' }}
variant='rounded'
height={160}
/>
<Skeleton
animation={false}
sx={{ bgcolor: customization.isDarkMode ? '#23262c' : '#fafafa' }}
variant='rounded'
height={160}
/>
</Box>
<div
style={{
position: 'absolute',
top: 0,
right: 0,
width: '100%',
height: '100%',
backdropFilter: `blur(1px)`,
background: `transparent`,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<StyledFab
color='secondary'
aria-label='preview'
title='Preview'
variant='extended'
onClick={onPreviewChunks}
>
<IconEye style={{ marginRight: '5px' }} />
Preview Chunks
</StyledFab>
</div>
</div>
))}
{documentChunks && documentChunks.length > 0 && (
<>
<Typography sx={{ wordWrap: 'break-word', textAlign: 'left', mb: 2 }} variant='h3'>
{currentPreviewCount} of {totalChunks} Chunks
</Typography>
<Box sx={{ mb: 3 }}>
<Typography>Show Chunks in Preview</Typography>
<div style={{ display: 'flex', flexDirection: 'row' }}>
<OutlinedInput
size='small'
multiline={false}
sx={{ mt: 1, flex: 1, mr: 2 }}
type='number'
key='previewChunkCount'
onChange={(e) => setPreviewChunkCount(e.target.value)}
value={previewChunkCount ?? 25}
/>
<StyledFab
color='secondary'
aria-label='preview'
title='Preview'
variant='extended'
onClick={onPreviewChunks}
>
<IconEye style={{ marginRight: '5px' }} />
Preview
</StyledFab>
</div>
</Box>
<div style={{ height: '800px', overflow: 'scroll', padding: '5px' }}>
<Grid container spacing={2}>
{documentChunks?.map((row, index) => (
<Grid item lg={6} md={6} sm={6} xs={6} key={index}>
<CardWrapper
content={false}
onClick={() => onChunkClick(row, index + 1)}
sx={{
border: 1,
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2
}}
>
<Card>
<CardContent sx={{ p: 1 }}>
<Typography sx={{ wordWrap: 'break-word', mb: 1 }} variant='h5'>
{`#${index + 1}. Characters: ${row.pageContent.length}`}
</Typography>
<Typography sx={{ wordWrap: 'break-word' }} variant='body2'>
{row.pageContent}
</Typography>
<ReactJson
theme={customization.isDarkMode ? 'ocean' : 'rjv-default'}
style={{ paddingTop: 10 }}
src={row.metadata}
name={null}
quotesOnKeys={false}
enableClipboard={false}
displayDataTypes={false}
collapsed={1}
/>
</CardContent>
</Card>
</CardWrapper>
</Grid>
))}
</Grid>
</div>
</>
)}
</Grid>
</Grid>
</Box>
</Stack>
)}
</MainCard>
<ExpandedChunkDialog
show={showExpandedChunkDialog}
isReadOnly={true}
dialogProps={expandedChunkDialogProps}
onCancel={() => setShowExpandedChunkDialog(false)}
></ExpandedChunkDialog>
{loading && <BackdropLoader open={loading} />}
</>
)
}
export default LoaderConfigPreviewChunks
@@ -0,0 +1,386 @@
import { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useNavigate } from 'react-router-dom'
import ReactJson from 'flowise-react-json-view'
// material-ui
import { Box, Card, Button, Grid, IconButton, Stack, Typography } from '@mui/material'
import { useTheme, styled } from '@mui/material/styles'
import CardContent from '@mui/material/CardContent'
import { IconLanguage, IconX, IconChevronLeft, IconChevronRight } from '@tabler/icons'
import chunks_emptySVG from '@/assets/images/chunks_empty.svg'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
import { BackdropLoader } from '@/ui-component/loading/BackdropLoader'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import ExpandedChunkDialog from './ExpandedChunkDialog'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
// API
import documentsApi from '@/api/documentstore'
// Hooks
import useApi from '@/hooks/useApi'
import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier'
// store
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
const CardWrapper = styled(MainCard)(({ theme }) => ({
background: theme.palette.card.main,
color: theme.darkTextPrimary,
overflow: 'auto',
position: 'relative',
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
cursor: 'pointer',
'&:hover': {
background: theme.palette.card.hover,
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 20%)'
},
maxHeight: '250px',
minHeight: '250px',
maxWidth: '100%',
overflowWrap: 'break-word',
whiteSpace: 'pre-line',
padding: 1
}))
const ShowStoredChunks = () => {
const customization = useSelector((state) => state.customization)
const navigate = useNavigate()
const dispatch = useDispatch()
const theme = useTheme()
const { confirm } = useConfirm()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const getChunksApi = useApi(documentsApi.getFileChunks)
const URLpath = document.location.pathname.toString().split('/')
const fileId = URLpath[URLpath.length - 1] === 'document-stores' ? '' : URLpath[URLpath.length - 1]
const storeId = URLpath[URLpath.length - 2] === 'document-stores' ? '' : URLpath[URLpath.length - 2]
const [documentChunks, setDocumentChunks] = useState([])
const [totalChunks, setTotalChunks] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [start, setStart] = useState(1)
const [end, setEnd] = useState(50)
const [loading, setLoading] = useState(false)
const [showExpandedChunkDialog, setShowExpandedChunkDialog] = useState(false)
const [expandedChunkDialogProps, setExpandedChunkDialogProps] = useState({})
const [fileNames, setFileNames] = useState([])
const chunkSelected = (chunkId) => {
const selectedChunk = documentChunks.find((chunk) => chunk.id === chunkId)
const selectedChunkNumber = documentChunks.findIndex((chunk) => chunk.id === chunkId) + start
const dialogProps = {
data: {
selectedChunk,
selectedChunkNumber
}
}
setExpandedChunkDialogProps(dialogProps)
setShowExpandedChunkDialog(true)
}
const onChunkEdit = async (newPageContent, newMetadata, chunk) => {
setLoading(true)
setShowExpandedChunkDialog(false)
try {
const editResp = await documentsApi.editChunkFromStore(
chunk.storeId,
chunk.docId,
chunk.id,
{ pageContent: newPageContent, metadata: newMetadata },
true
)
if (editResp.data) {
enqueueSnackbar({
message: 'Document chunk successfully edited!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
getChunksApi.request(storeId, fileId, currentPage)
}
setLoading(false)
} catch (error) {
setLoading(false)
enqueueSnackbar({
message: `Failed to edit chunk: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
const onDeleteChunk = async (chunk) => {
const confirmPayload = {
title: `Delete`,
description: `Delete chunk ${chunk.id} ? This action cannot be undone.`,
confirmButtonName: 'Delete',
cancelButtonName: 'Cancel'
}
const isConfirmed = await confirm(confirmPayload)
if (isConfirmed) {
setLoading(true)
setShowExpandedChunkDialog(false)
try {
const delResp = await documentsApi.deleteChunkFromStore(chunk.storeId, chunk.docId, chunk.id)
if (delResp.data) {
enqueueSnackbar({
message: 'Document chunk successfully deleted!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
getChunksApi.request(storeId, fileId, currentPage)
}
setLoading(false)
} catch (error) {
setLoading(false)
enqueueSnackbar({
message: `Failed to delete chunk: ${
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
}`,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
}
useEffect(() => {
setLoading(true)
getChunksApi.request(storeId, fileId, currentPage)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const changePage = (newPage) => {
setLoading(true)
setCurrentPage(newPage)
getChunksApi.request(storeId, fileId, newPage)
}
useEffect(() => {
if (getChunksApi.data) {
const data = getChunksApi.data
setTotalChunks(data.count)
setDocumentChunks(data.chunks)
setLoading(false)
setCurrentPage(data.currentPage)
setStart(data.currentPage * 50 - 49)
setEnd(data.currentPage * 50 > data.count ? data.count : data.currentPage * 50)
if (data.file?.files && data.file.files.length > 0) {
const fileNames = []
for (const attachedFile of data.file.files) {
fileNames.push(attachedFile.name)
}
setFileNames(fileNames)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getChunksApi.data])
return (
<>
<MainCard style={{ position: 'relative' }}>
<Stack flexDirection='column' sx={{ gap: 1 }}>
<ViewHeader
isBackButton={true}
search={false}
title={getChunksApi.data?.file?.loaderName || getChunksApi.data?.storeName}
description={getChunksApi.data?.file?.splitterName || getChunksApi.data?.description}
onBack={() => navigate(-1)}
></ViewHeader>
<div style={{ width: '100%' }}>
{fileNames.length > 0 && (
<Grid sx={{ mt: 1 }} container>
{fileNames.map((fileName, index) => (
<div
key={index}
style={{
paddingLeft: '15px',
paddingRight: '15px',
paddingTop: '10px',
paddingBottom: '10px',
fontSize: '0.9rem',
width: 'max-content',
borderRadius: '25px',
boxShadow: customization.isDarkMode
? '0 2px 14px 0 rgb(255 255 255 / 20%)'
: '0 2px 14px 0 rgb(32 40 45 / 20%)',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
marginRight: '10px'
}}
>
{fileName}
</div>
))}
</Grid>
)}
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
alignContent: 'center',
overflow: 'hidden',
marginTop: 15,
marginBottom: 10
}}
>
<div style={{ marginRight: 20, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<IconButton
size='small'
onClick={() => changePage(currentPage - 1)}
style={{ marginRight: 10 }}
variant='outlined'
disabled={currentPage === 1}
>
<IconChevronLeft
color={
customization.isDarkMode
? currentPage === 1
? '#616161'
: 'white'
: currentPage === 1
? '#e0e0e0'
: 'black'
}
/>
</IconButton>
Showing {Math.min(start, totalChunks)}-{end} of {totalChunks} chunks
<IconButton
size='small'
onClick={() => changePage(currentPage + 1)}
style={{ marginLeft: 10 }}
variant='outlined'
disabled={end >= totalChunks}
>
<IconChevronRight
color={
customization.isDarkMode
? end >= totalChunks
? '#616161'
: 'white'
: end >= totalChunks
? '#e0e0e0'
: 'black'
}
/>
</IconButton>
</div>
<div style={{ marginRight: 20, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<IconLanguage style={{ marginRight: 10 }} size={20} />
{getChunksApi.data?.file?.totalChars?.toLocaleString()} characters
</div>
</div>
</div>
<div>
<Grid container spacing={2}>
{!documentChunks.length && (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%'
}}
>
<Box sx={{ mt: 5, p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={chunks_emptySVG}
alt='chunks_emptySVG'
/>
</Box>
<div>No Chunks</div>
</div>
)}
{documentChunks.length > 0 &&
documentChunks.map((row, index) => (
<Grid item lg={4} md={4} sm={6} xs={6} key={index}>
<CardWrapper
content={false}
onClick={() => chunkSelected(row.id)}
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
>
<Card>
<CardContent sx={{ p: 2 }}>
<Typography sx={{ wordWrap: 'break-word', mb: 1 }} variant='h5'>
{`#${row.chunkNo}. Characters: ${row.pageContent.length}`}
</Typography>
<Typography sx={{ wordWrap: 'break-word' }} variant='body2'>
{row.pageContent}
</Typography>
<ReactJson
theme={customization.isDarkMode ? 'ocean' : 'rjv-default'}
style={{ paddingTop: 10 }}
src={row.metadata ? JSON.parse(row.metadata) : {}}
name={null}
quotesOnKeys={false}
enableClipboard={false}
displayDataTypes={false}
collapsed={1}
/>
</CardContent>
</Card>
</CardWrapper>
</Grid>
))}
</Grid>
</div>
</Stack>
</MainCard>
<ConfirmDialog />
<ExpandedChunkDialog
show={showExpandedChunkDialog}
dialogProps={expandedChunkDialogProps}
onCancel={() => setShowExpandedChunkDialog(false)}
onChunkEdit={(newPageContent, newMetadata, selectedChunk) => onChunkEdit(newPageContent, newMetadata, selectedChunk)}
onDeleteChunk={(selectedChunk) => onDeleteChunk(selectedChunk)}
></ExpandedChunkDialog>
{loading && <BackdropLoader open={loading} />}
</>
)
}
export default ShowStoredChunks
+352
View File
@@ -0,0 +1,352 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui
import {
Box,
Paper,
Skeleton,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
ToggleButton,
ToggleButtonGroup,
Typography
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
import DocumentStoreCard from '@/ui-component/cards/DocumentStoreCard'
import { StyledButton } from '@/ui-component/button/StyledButton'
import AddDocStoreDialog from '@/views/docstore/AddDocStoreDialog'
import ErrorBoundary from '@/ErrorBoundary'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus'
// API
import useApi from '@/hooks/useApi'
import documentsApi from '@/api/documentstore'
// icons
import { IconPlus, IconLayoutGrid, IconList } from '@tabler/icons'
import doc_store_empty from '@/assets/images/doc_store_empty.svg'
// const
import { baseURL, gridSpacing } from '@/store/constant'
// ==============================|| DOCUMENTS ||============================== //
const Documents = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const navigate = useNavigate()
const getAllDocumentStores = useApi(documentsApi.getAllDocumentStores)
const [error, setError] = useState(null)
const [isLoading, setLoading] = useState(true)
const [images, setImages] = useState({})
const [search, setSearch] = useState('')
const [showDialog, setShowDialog] = useState(false)
const [dialogProps, setDialogProps] = useState({})
const [docStores, setDocStores] = useState([])
const [view, setView] = useState(localStorage.getItem('docStoreDisplayStyle') || 'card')
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('docStoreDisplayStyle', nextView)
setView(nextView)
}
function filterDocStores(data) {
return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
const goToDocumentStore = (id) => {
navigate('/document-stores/' + id)
}
const addNew = () => {
const dialogProp = {
title: 'Add New Document Store',
type: 'ADD',
cancelButtonName: 'Cancel',
confirmButtonName: 'Add'
}
setDialogProps(dialogProp)
setShowDialog(true)
}
const onConfirm = () => {
setShowDialog(false)
getAllDocumentStores.request()
}
useEffect(() => {
getAllDocumentStores.request()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllDocumentStores.data) {
try {
const docStores = getAllDocumentStores.data
if (!Array.isArray(docStores)) return
const loaderImages = {}
for (let i = 0; i < docStores.length; i += 1) {
const loaders = docStores[i].loaders ?? []
let totalChunks = 0
let totalChars = 0
loaderImages[docStores[i].id] = []
for (let j = 0; j < loaders.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${loaders[j].loaderId}`
if (!loaderImages[docStores[i].id].includes(imageSrc)) {
loaderImages[docStores[i].id].push(imageSrc)
}
totalChunks += loaders[j]?.totalChunks ?? 0
totalChars += loaders[j]?.totalChars ?? 0
}
docStores[i].totalDocs = loaders?.length ?? 0
docStores[i].totalChunks = totalChunks
docStores[i].totalChars = totalChars
}
setDocStores(docStores)
setImages(loaderImages)
} catch (e) {
console.error(e)
}
}
}, [getAllDocumentStores.data])
useEffect(() => {
setLoading(getAllDocumentStores.loading)
}, [getAllDocumentStores.loading])
useEffect(() => {
setError(getAllDocumentStores.error)
}, [getAllDocumentStores.error])
return (
<MainCard>
{error ? (
<ErrorBoundary error={error} />
) : (
<Stack flexDirection='column' sx={{ gap: 3 }}>
<ViewHeader onSearchChange={onSearchChange} search={true} searchPlaceholder='Search Name' title='Document Store'>
<ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
exclusive
onChange={handleChange}
>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='card'
title='Card View'
>
<IconLayoutGrid />
</ToggleButton>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='list'
title='List View'
>
<IconList />
</ToggleButton>
</ToggleButtonGroup>
<StyledButton
variant='contained'
sx={{ borderRadius: 2, height: '100%' }}
onClick={addNew}
startIcon={<IconPlus />}
id='btn_createVariable'
>
Add New
</StyledButton>
</ViewHeader>
{!view || view === 'card' ? (
<>
{isLoading && !docStores ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{docStores?.filter(filterDocStores).map((data, index) => (
<DocumentStoreCard
key={index}
images={images[data.id]}
data={data}
onClick={() => goToDocumentStore(data.id)}
/>
))}
</Box>
)}
</>
) : (
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table aria-label='documents table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>&nbsp;</TableCell>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Connected flows</TableCell>
<TableCell>Total characters</TableCell>
<TableCell>Total chunks</TableCell>
<TableCell>Loader types</TableCell>
</TableRow>
</TableHead>
<TableBody>
{docStores?.filter(filterDocStores).map((data, index) => (
<TableRow
onClick={() => goToDocumentStore(data.id)}
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell align='center'>
<DocumentStoreStatus isTableView={true} status={data.status} />
</TableCell>
<TableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data.name}
</Typography>
</TableCell>
<TableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data?.description}
</Typography>
</TableCell>
<TableCell>{data.whereUsed?.length ?? 0}</TableCell>
<TableCell>{data.totalChars}</TableCell>
<TableCell>{data.totalChunks}</TableCell>
<TableCell>
{images[data.id] && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images[data.id].slice(0, images.length > 3 ? 3 : images.length).map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt=''
src={img}
/>
</Box>
))}
{images.length > 3 && (
<Typography
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
>
+ {images.length - 3} More
</Typography>
)}
</Box>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{!isLoading && (!docStores || docStores.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
src={doc_store_empty}
alt='doc_store_empty'
/>
</Box>
<div>No Document Stores Created Yet</div>
</Stack>
)}
</Stack>
)}
{showDialog && (
<AddDocStoreDialog
dialogProps={dialogProps}
show={showDialog}
onCancel={() => setShowDialog(false)}
onConfirm={onConfirm}
/>
)}
</MainCard>
)
}
export default Documents
@@ -187,25 +187,7 @@ const AddEditVariableDialog = ({ show, dialogProps, onCancel, onConfirm, setErro
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div
style={{
width: 50,
height: 50,
marginRight: 10,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
<IconVariable style={{ marginRight: '10px' }} />
{dialogProps.type === 'ADD' ? 'Add Variable' : 'Edit Variable'}
</div>
</DialogTitle>