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
+32
View File
@@ -0,0 +1,32 @@
import client from './client'
const getAllDocumentStores = () => client.get('/document-store/stores')
const getDocumentLoaders = () => client.get('/document-store/loaders')
const getSpecificDocumentStore = (id) => client.get(`/document-store/store/${id}`)
const createDocumentStore = (body) => client.post(`/document-store/store`, body)
const updateDocumentStore = (id, body) => client.put(`/document-store/store/${id}`, body)
const deleteDocumentStore = (id) => client.delete(`/document-store/store/${id}`)
const deleteLoaderFromStore = (id, fileId) => client.delete(`/document-store/loader/${id}/${fileId}`)
const deleteChunkFromStore = (storeId, loaderId, chunkId) => client.delete(`/document-store/chunks/${storeId}/${loaderId}/${chunkId}`)
const editChunkFromStore = (storeId, loaderId, chunkId, body) =>
client.put(`/document-store/chunks/${storeId}/${loaderId}/${chunkId}`, body)
const getFileChunks = (storeId, fileId, pageNo) => client.get(`/document-store/chunks/${storeId}/${fileId}/${pageNo}`)
const previewChunks = (body) => client.post('/document-store/loader/preview', body)
const processChunks = (body) => client.post(`/document-store/loader/process`, body)
export default {
getAllDocumentStores,
getSpecificDocumentStore,
createDocumentStore,
deleteLoaderFromStore,
getFileChunks,
updateDocumentStore,
previewChunks,
processChunks,
getDocumentLoaders,
deleteChunkFromStore,
editChunkFromStore,
deleteDocumentStore
}
+3 -1
View File
@@ -3,11 +3,13 @@ import client from './client'
const getAllNodes = () => client.get('/nodes')
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
const getNodesByCategory = (name) => client.get(`/nodes/category/${name}`)
const executeCustomFunctionNode = (body) => client.post(`/node-custom-function`, body)
export default {
getAllNodes,
getSpecificNode,
executeCustomFunctionNode
executeCustomFunctionNode,
getNodesByCategory
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" width="647.63626" height="632.17383" viewBox="0 0 647.63626 632.17383" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M687.3279,276.08691H512.81813a15.01828,15.01828,0,0,0-15,15v387.85l-2,.61005-42.81006,13.11a8.00676,8.00676,0,0,1-9.98974-5.31L315.678,271.39691a8.00313,8.00313,0,0,1,5.31006-9.99l65.97022-20.2,191.25-58.54,65.96972-20.2a7.98927,7.98927,0,0,1,9.99024,5.3l32.5498,106.32Z" transform="translate(-276.18187 -133.91309)" fill="#f2f2f2"/><path d="M725.408,274.08691l-39.23-128.14a16.99368,16.99368,0,0,0-21.23-11.28l-92.75,28.39L380.95827,221.60693l-92.75,28.4a17.0152,17.0152,0,0,0-11.28028,21.23l134.08008,437.93a17.02661,17.02661,0,0,0,16.26026,12.03,16.78926,16.78926,0,0,0,4.96972-.75l63.58008-19.46,2-.62v-2.09l-2,.61-64.16992,19.65a15.01489,15.01489,0,0,1-18.73-9.95l-134.06983-437.94a14.97935,14.97935,0,0,1,9.94971-18.73l92.75-28.4,191.24024-58.54,92.75-28.4a15.15551,15.15551,0,0,1,4.40966-.66,15.01461,15.01461,0,0,1,14.32032,10.61l39.0498,127.56.62012,2h2.08008Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M398.86279,261.73389a9.0157,9.0157,0,0,1-8.61133-6.3667l-12.88037-42.07178a8.99884,8.99884,0,0,1,5.9712-11.24023l175.939-53.86377a9.00867,9.00867,0,0,1,11.24072,5.9707l12.88037,42.07227a9.01029,9.01029,0,0,1-5.9707,11.24072L401.49219,261.33887A8.976,8.976,0,0,1,398.86279,261.73389Z" transform="translate(-276.18187 -133.91309)" fill="#673ab7"/><circle cx="190.15351" cy="24.95465" r="20" fill="#673ab7"/><circle cx="190.15351" cy="24.95465" r="12.66462" fill="#fff"/><path d="M878.81836,716.08691h-338a8.50981,8.50981,0,0,1-8.5-8.5v-405a8.50951,8.50951,0,0,1,8.5-8.5h338a8.50982,8.50982,0,0,1,8.5,8.5v405A8.51013,8.51013,0,0,1,878.81836,716.08691Z" transform="translate(-276.18187 -133.91309)" fill="#e6e6e6"/><path d="M723.31813,274.08691h-210.5a17.02411,17.02411,0,0,0-17,17v407.8l2-.61v-407.19a15.01828,15.01828,0,0,1,15-15H723.93825Zm183.5,0h-394a17.02411,17.02411,0,0,0-17,17v458a17.0241,17.0241,0,0,0,17,17h394a17.0241,17.0241,0,0,0,17-17v-458A17.02411,17.02411,0,0,0,906.81813,274.08691Zm15,475a15.01828,15.01828,0,0,1-15,15h-394a15.01828,15.01828,0,0,1-15-15v-458a15.01828,15.01828,0,0,1,15-15h394a15.01828,15.01828,0,0,1,15,15Z" transform="translate(-276.18187 -133.91309)" fill="#3f3d56"/><path d="M801.81836,318.08691h-184a9.01015,9.01015,0,0,1-9-9v-44a9.01016,9.01016,0,0,1,9-9h184a9.01016,9.01016,0,0,1,9,9v44A9.01015,9.01015,0,0,1,801.81836,318.08691Z" transform="translate(-276.18187 -133.91309)" fill="#673ab7"/><circle cx="433.63626" cy="105.17383" r="20" fill="#673ab7"/><circle cx="433.63626" cy="105.17383" r="12.18187" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

@@ -1,13 +1,26 @@
import PropTypes from 'prop-types'
// material-ui
import { Box, OutlinedInput, Toolbar, Typography } from '@mui/material'
import { IconButton, Box, OutlinedInput, Toolbar, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles'
import { StyledFab } from '@/ui-component/button/StyledFab'
// icons
import { IconSearch } from '@tabler/icons'
import { IconSearch, IconArrowLeft, IconEdit } from '@tabler/icons'
const ViewHeader = ({ children, filters = null, onSearchChange, search, searchPlaceholder = 'Search', title }) => {
const ViewHeader = ({
children,
filters = null,
onSearchChange,
search,
searchPlaceholder = 'Search',
title,
description,
isBackButton,
onBack,
isEditButton,
onEdit
}) => {
const theme = useTheme()
return (
@@ -21,15 +34,54 @@ const ViewHeader = ({ children, filters = null, onSearchChange, search, searchPl
width: '100%'
}}
>
<Typography
sx={{
fontSize: '2rem',
fontWeight: 600
}}
variant='h1'
>
{title}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: 'row' }}>
{isBackButton && (
<StyledFab sx={{ mr: 3 }} size='small' color='secondary' aria-label='back' title='Back' onClick={onBack}>
<IconArrowLeft />
</StyledFab>
)}
<Box sx={{ display: 'flex', alignItems: 'start', flexDirection: 'column' }}>
<Typography
sx={{
fontSize: '2rem',
fontWeight: 600,
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden',
flex: 1,
maxWidth: 'calc(100vh - 100px)'
}}
variant='h1'
>
{title}
</Typography>
{description && (
<Typography
sx={{
fontSize: '1rem',
fontWeight: 500,
mt: 2,
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden',
flex: 1,
maxWidth: 'calc(100vh - 100px)'
}}
>
{description}
</Typography>
)}
</Box>
{isEditButton && (
<IconButton sx={{ ml: 3 }} color='secondary' title='Edit' onClick={onEdit}>
<IconEdit />
</IconButton>
)}
</Box>
<Box sx={{ height: 40, display: 'flex', alignItems: 'center', gap: 1 }}>
{search && (
<OutlinedInput
@@ -77,7 +129,12 @@ ViewHeader.propTypes = {
onSearchChange: PropTypes.func,
search: PropTypes.bool,
searchPlaceholder: PropTypes.string,
title: PropTypes.string
title: PropTypes.string,
description: PropTypes.string,
isBackButton: PropTypes.bool,
onBack: PropTypes.func,
isEditButton: PropTypes.bool,
onEdit: PropTypes.func
}
export default ViewHeader
+10 -2
View File
@@ -1,8 +1,8 @@
// assets
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable } from '@tabler/icons'
import { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable, IconFiles } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable }
const icons = { IconHierarchy, IconBuildingStore, IconKey, IconTool, IconLock, IconRobot, IconVariable, IconFiles }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -66,6 +66,14 @@ const dashboard = {
url: '/apikey',
icon: icons.IconKey,
breadcrumbs: true
},
{
id: 'documents',
title: 'Document Stores',
type: 'item',
url: '/document-stores',
icon: icons.IconFiles,
breadcrumbs: true
}
]
}
+22
View File
@@ -25,6 +25,12 @@ const Credentials = Loadable(lazy(() => import('@/views/credentials')))
// variables routing
const Variables = Loadable(lazy(() => import('@/views/variables')))
// documents routing
const Documents = Loadable(lazy(() => import('@/views/docstore')))
const DocumentStoreDetail = Loadable(lazy(() => import('@/views/docstore/DocumentStoreDetail')))
const ShowStoredChunks = Loadable(lazy(() => import('@/views/docstore/ShowStoredChunks')))
const LoaderConfigPreviewChunks = Loadable(lazy(() => import('@/views/docstore/LoaderConfigPreviewChunks')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
@@ -62,6 +68,22 @@ const MainRoutes = {
{
path: '/variables',
element: <Variables />
},
{
path: '/document-stores',
element: <Documents />
},
{
path: '/document-stores/:id',
element: <DocumentStoreDetail />
},
{
path: '/document-stores/chunks/:id/:id',
element: <ShowStoredChunks />
},
{
path: '/document-stores/:id/:name',
element: <LoaderConfigPreviewChunks />
}
]
}
@@ -0,0 +1,191 @@
import PropTypes from 'prop-types'
import { useSelector } from 'react-redux'
// material-ui
import { styled } from '@mui/material/styles'
import { Box, Grid, Typography, useTheme } from '@mui/material'
import { IconVectorBezier2, IconLanguage, IconScissors } from '@tabler/icons'
// project imports
import MainCard from '@/ui-component/cards/MainCard'
import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus'
import { kFormatter } from '@/utils/genericHelper'
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%)'
},
height: '100%',
minHeight: '160px',
maxHeight: '300px',
width: '100%',
overflowWrap: 'break-word',
whiteSpace: 'pre-line'
}))
// ===========================|| DOC STORE CARD ||=========================== //
const DocumentStoreCard = ({ data, images, onClick }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
return (
<CardWrapper content={false} onClick={onClick} sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}>
<Box sx={{ height: '100%', p: 2.25 }}>
<Grid container justifyContent='space-between' direction='column' sx={{ height: '100%' }} gap={2}>
<Box display='flex' flexDirection='column' sx={{ flex: 1, width: '100%' }}>
<div
style={{
width: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
overflow: 'hidden'
}}
>
<Typography
sx={{
display: '-webkit-box',
fontSize: '1.25rem',
fontWeight: 500,
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden',
flex: 1
}}
>
{data.name}
</Typography>
<DocumentStoreStatus status={data.status} />
</div>
<span
style={{
display: '-webkit-box',
marginTop: 10,
overflowWrap: 'break-word',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data.description || ' '}
</span>
</Box>
<Grid container columnGap={2} rowGap={1}>
<div
style={{
paddingLeft: '7px',
paddingRight: '7px',
paddingTop: '3px',
paddingBottom: '3px',
fontSize: '11px',
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'
}}
>
<IconVectorBezier2 style={{ marginRight: 5 }} size={15} />
{data.whereUsed?.length ?? 0} {data.whereUsed?.length <= 1 ? 'flow' : 'flows'}
</div>
<div
style={{
paddingLeft: '7px',
paddingRight: '7px',
paddingTop: '3px',
paddingBottom: '3px',
fontSize: '11px',
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'
}}
>
<IconLanguage style={{ marginRight: 5 }} size={15} />
{kFormatter(data.totalChars ?? 0)} chars
</div>
<div
style={{
paddingLeft: '7px',
paddingRight: '7px',
paddingTop: '3px',
paddingBottom: '3px',
fontSize: '11px',
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'
}}
>
<IconScissors style={{ marginRight: 5 }} size={15} />
{kFormatter(data.totalChunks ?? 0)} chunks
</div>
</Grid>
{images && images.length > 0 && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images.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>
)}
</Grid>
</Box>
</CardWrapper>
)
}
DocumentStoreCard.propTypes = {
data: PropTypes.object,
images: PropTypes.array,
onClick: PropTypes.func
}
export default DocumentStoreCard
@@ -144,7 +144,6 @@ const ItemCard = ({ data, images, onClick }) => {
}
ItemCard.propTypes = {
isLoading: PropTypes.bool,
data: PropTypes.object,
images: PropTypes.array,
onClick: PropTypes.func
@@ -23,7 +23,7 @@ const StatsCard = ({ title, stat }) => {
StatsCard.propTypes = {
title: PropTypes.string,
stat: PropTypes.string
stat: PropTypes.string | PropTypes.number
}
export default StatsCard
@@ -113,7 +113,7 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => {
lang={languageType}
placeholder={inputParam.placeholder}
basicSetup={
languageType === 'json'
languageType !== 'js'
? { lineNumbers: false, foldGutter: false, autocompletion: false, highlightActiveLine: false }
: {}
}
@@ -161,7 +161,7 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => {
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 1.5 }}>
<Typography sx={{ fontWeight: 500 }}>Scraped Links</Typography>
{selectedLinks.length > 0 ? (
<StyledButton
<Button
sx={{ height: 'max-content', width: 'max-content' }}
variant='outlined'
color='error'
@@ -170,7 +170,7 @@ const ManageScrapedLinksDialog = ({ show, dialogProps, onCancel, onSave }) => {
startIcon={<IconEraser />}
>
Clear All
</StyledButton>
</Button>
) : null}
</Box>
<>
@@ -22,7 +22,7 @@ export const MultiDropdown = ({ name, value, options, onSelect, formControlSx =
const customization = useSelector((state) => state.customization)
const findMatchingOptions = (options = [], internalValue) => {
let values = []
if (internalValue && typeof internalValue === 'string') values = JSON.parse(internalValue)
if ('choose an option' !== internalValue && internalValue && typeof internalValue === 'string') values = JSON.parse(internalValue)
else values = internalValue
return options.filter((option) => values.includes(option.name))
}
@@ -5,8 +5,21 @@ import { json } from '@codemirror/lang-json'
import { vscodeDark } from '@uiw/codemirror-theme-vscode'
import { sublime } from '@uiw/codemirror-theme-sublime'
import { EditorView } from '@codemirror/view'
import { useTheme } from '@mui/material/styles'
export const CodeEditor = ({
value,
height,
theme,
lang,
placeholder,
disabled = false,
autoFocus = false,
basicSetup = {},
onValueChange
}) => {
const colorTheme = useTheme()
export const CodeEditor = ({ value, height, theme, lang, placeholder, disabled = false, basicSetup = {}, onValueChange }) => {
const customStyle = EditorView.baseTheme({
'&': {
color: '#191b1f',
@@ -14,7 +27,18 @@ export const CodeEditor = ({ value, height, theme, lang, placeholder, disabled =
},
'.cm-placeholder': {
color: 'rgba(120, 120, 120, 0.5)'
}
},
'.cm-content':
lang !== 'js'
? {
fontFamily: 'Roboto, sans-serif',
fontSize: '0.95rem',
letterSpacing: '0em',
fontWeight: 400,
lineHeight: '1.5em',
color: colorTheme.darkTextPrimary
}
: {}
})
return (
@@ -31,6 +55,8 @@ export const CodeEditor = ({ value, height, theme, lang, placeholder, disabled =
onChange={onValueChange}
readOnly={disabled}
editable={!disabled}
// eslint-disable-next-line
autoFocus={autoFocus}
basicSetup={basicSetup}
/>
)
@@ -43,6 +69,7 @@ CodeEditor.propTypes = {
lang: PropTypes.string,
placeholder: PropTypes.string,
disabled: PropTypes.bool,
autoFocus: PropTypes.bool,
basicSetup: PropTypes.object,
onValueChange: PropTypes.func
}
@@ -0,0 +1,105 @@
import { styled } from '@mui/material/styles'
import Box from '@mui/material/Box'
import Slider from '@mui/material/Slider'
import { Grid, Input } from '@mui/material'
import PropTypes from 'prop-types'
const BoxShadow = '0 3px 1px rgba(0,0,0,0.1),0 4px 8px rgba(0,0,0,0.13),0 0 0 1px rgba(0,0,0,0.02)'
const CustomInputSlider = styled(Slider)(({ theme }) => ({
color: theme.palette.mode === 'dark' ? '#0a84ff' : '#007bff',
height: 5,
padding: '15px 0',
'& .MuiSlider-thumb': {
height: 20,
width: 20,
backgroundColor: '#333',
boxShadow: '0 0 2px 0px rgba(0, 0, 0, 0.1)',
'&:focus, &:hover, &.Mui-active': {
boxShadow: '0px 0px 3px 1px rgba(0, 0, 0, 0.1)',
// Reset on touch devices, it doesn't add specificity
'@media (hover: none)': {
boxShadow: BoxShadow
}
},
'&:before': {
boxShadow: '0px 0px 1px 0px rgba(0,0,0,0.2), 0px 0px 0px 0px rgba(0,0,0,0.14), 0px 0px 1px 0px rgba(0,0,0,0.12)'
}
},
'& .MuiSlider-valueLabel': {
fontSize: 12,
fontWeight: 'normal',
top: -1,
backgroundColor: 'unset',
color: theme.palette.text.primary,
'&::before': {
display: 'none'
},
'& *': {
background: 'transparent',
color: theme.palette.mode === 'dark' ? '#000' : '#000'
}
},
'& .MuiSlider-track': {
border: 'none',
height: 5
},
'& .MuiSlider-rail': {
opacity: 0.5,
boxShadow: 'inset 0px 0px 4px -2px #000',
backgroundColor: '#d0d0d0'
}
}))
export const InputSlider = ({ value, onChange }) => {
const handleSliderChange = (event, newValue) => onChange(newValue)
const handleInputChange = (event) => {
onChange(event.target.value === '' ? 0 : Number(event.target.value))
}
const handleBlur = () => {
if (value < 0) {
onChange(0)
}
}
return (
<Box sx={{ width: '100%' }}>
<Grid container spacing={2} sx={{ mt: 1 }} alignItems='center'>
<Grid item xs>
<CustomInputSlider
value={typeof value === 'number' ? value : 0}
onChange={handleSliderChange}
valueLabelDisplay='on'
aria-labelledby='input-slider'
step={10}
min={0}
max={5000}
/>
</Grid>
<Grid item>
<Input
sx={{ ml: 3, mr: 3 }}
value={value}
size='small'
onChange={handleInputChange}
onBlur={handleBlur}
inputProps={{
step: 10,
min: 0,
max: 10000,
type: 'number',
'aria-labelledby': 'input-slider'
}}
/>
</Grid>
</Grid>
</Box>
)
}
InputSlider.propTypes = {
value: PropTypes.number,
onChange: PropTypes.func
}
+33 -10
View File
@@ -737,14 +737,37 @@ export const getOS = () => {
return os
}
export const formatBytes = (bytes, decimals = 2) => {
if (!+bytes) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
export const formatBytes = (number) => {
if (number == null || number === undefined || number <= 0) {
return '0 Bytes'
}
var scaleCounter = 0
var scaleInitials = [' Bytes', ' KB', ' MB', ' GB', ' TB', ' PB', ' EB', ' ZB', ' YB']
while (number >= 1024 && scaleCounter < scaleInitials.length - 1) {
number /= 1024
scaleCounter++
}
if (scaleCounter >= scaleInitials.length) scaleCounter = scaleInitials.length - 1
let compactNumber = number
.toFixed(2)
.replace(/\.?0+$/, '')
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
compactNumber += scaleInitials[scaleCounter]
return compactNumber.trim()
}
// Formatter from: https://stackoverflow.com/a/9462382
export const kFormatter = (num) => {
const lookup = [
{ value: 1, symbol: '' },
{ value: 1e3, symbol: 'k' },
{ value: 1e6, symbol: 'M' },
{ value: 1e9, symbol: 'G' },
{ value: 1e12, symbol: 'T' },
{ value: 1e15, symbol: 'P' },
{ value: 1e18, symbol: 'E' }
]
const regexp = /\.0+$|(?<=\.[0-9]*[1-9])0+$/
const item = lookup.findLast((item) => num >= item.value)
return item ? (num / item.value).toFixed(1).replace(regexp, '').concat(item.symbol) : '0'
}
+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>