mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 19:00:59 +03:00
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:
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' }}> *</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' }}> *</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> </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
|
||||
@@ -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> </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>
|
||||
|
||||
Reference in New Issue
Block a user