Bugfix/Remove postgres vector store data when deletion (#5536)

Remove postgres vector store data when deletion

- Introduced a new `doc_id` column in MySQL, Postgres, and SQLite record managers to support document identification.
- Updated the `update` method to handle both string and object formats for keys, allowing for better flexibility in document updates.
- Enhanced `listKeys` method to filter by `doc_id` when provided in options.
- Updated vector store integrations to utilize the new `doc_id` filtering capability
This commit is contained in:
Henry Heng
2025-11-30 12:01:36 +00:00
committed by GitHub
parent e6e0c2d07b
commit 465005a503
20 changed files with 620 additions and 217 deletions
+4 -1
View File
@@ -22,7 +22,10 @@ const refreshLoader = (storeId) => client.post(`/document-store/refresh/${storeI
const insertIntoVectorStore = (body) => client.post(`/document-store/vectorstore/insert`, body)
const saveVectorStoreConfig = (body) => client.post(`/document-store/vectorstore/save`, body)
const updateVectorStoreConfig = (body) => client.post(`/document-store/vectorstore/update`, body)
const deleteVectorStoreDataFromStore = (storeId) => client.delete(`/document-store/vectorstore/${storeId}`)
const deleteVectorStoreDataFromStore = (storeId, docId) => {
const url = docId ? `/document-store/vectorstore/${storeId}?docId=${docId}` : `/document-store/vectorstore/${storeId}`
return client.delete(url)
}
const queryVectorStore = (body) => client.post(`/document-store/vectorstore/query`, body)
const getVectorStoreProviders = () => client.get('/document-store/components/vectorstore')
const getEmbeddingProviders = () => client.get('/document-store/components/embeddings')
@@ -18,11 +18,15 @@ import {
TableContainer,
TableRow,
TableCell,
Checkbox,
FormControlLabel,
DialogActions
DialogActions,
Card,
Stack,
Link
} from '@mui/material'
import { useTheme } from '@mui/material/styles'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import SettingsIcon from '@mui/icons-material/Settings'
import { IconAlertTriangle } from '@tabler/icons-react'
import { TableViewOnly } from '@/ui-component/table/Table'
import { v4 as uuidv4 } from 'uuid'
@@ -36,12 +40,13 @@ import { initNode } from '@/utils/genericHelper'
const DeleteDocStoreDialog = ({ show, dialogProps, onCancel, onDelete }) => {
const portalElement = document.getElementById('portal')
const theme = useTheme()
const [nodeConfigExpanded, setNodeConfigExpanded] = useState({})
const [removeFromVS, setRemoveFromVS] = useState(false)
const [vsFlowData, setVSFlowData] = useState([])
const [rmFlowData, setRMFlowData] = useState([])
const getSpecificNodeApi = useApi(nodesApi.getSpecificNode)
const getVectorStoreNodeApi = useApi(nodesApi.getSpecificNode)
const getRecordManagerNodeApi = useApi(nodesApi.getSpecificNode)
const handleAccordionChange = (nodeName) => (event, isExpanded) => {
const accordianNodes = { ...nodeConfigExpanded }
@@ -52,42 +57,37 @@ const DeleteDocStoreDialog = ({ show, dialogProps, onCancel, onDelete }) => {
useEffect(() => {
if (dialogProps.recordManagerConfig) {
const nodeName = dialogProps.recordManagerConfig.name
if (nodeName) getSpecificNodeApi.request(nodeName)
if (nodeName) getRecordManagerNodeApi.request(nodeName)
}
if (dialogProps.vectorStoreConfig) {
const nodeName = dialogProps.vectorStoreConfig.name
if (nodeName) getSpecificNodeApi.request(nodeName)
}
if (dialogProps.vectorStoreConfig) {
const nodeName = dialogProps.vectorStoreConfig.name
if (nodeName) getVectorStoreNodeApi.request(nodeName)
}
return () => {
setNodeConfigExpanded({})
setRemoveFromVS(false)
setVSFlowData([])
setRMFlowData([])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialogProps])
// Process Vector Store node data
useEffect(() => {
if (getSpecificNodeApi.data) {
const nodeData = cloneDeep(initNode(getSpecificNodeApi.data, uuidv4()))
let config = 'vectorStoreConfig'
if (nodeData.category === 'Record Manager') config = 'recordManagerConfig'
if (getVectorStoreNodeApi.data && dialogProps.vectorStoreConfig) {
const nodeData = cloneDeep(initNode(getVectorStoreNodeApi.data, uuidv4()))
const paramValues = []
for (const inputName in dialogProps[config].config) {
for (const inputName in dialogProps.vectorStoreConfig.config) {
const inputParam = nodeData.inputParams.find((inp) => inp.name === inputName)
if (!inputParam) continue
if (inputParam.type === 'credential') continue
let paramValue = {}
const inputValue = dialogProps[config].config[inputName]
const inputValue = dialogProps.vectorStoreConfig.config[inputName]
if (!inputValue) continue
@@ -95,40 +95,71 @@ const DeleteDocStoreDialog = ({ show, dialogProps, onCancel, onDelete }) => {
continue
}
paramValue = {
paramValues.push({
label: inputParam?.label,
name: inputParam?.name,
type: inputParam?.type,
value: inputValue
}
paramValues.push(paramValue)
})
}
if (config === 'vectorStoreConfig') {
setVSFlowData([
{
label: nodeData.label,
name: nodeData.name,
category: nodeData.category,
id: nodeData.id,
paramValues
}
])
} else if (config === 'recordManagerConfig') {
setRMFlowData([
{
label: nodeData.label,
name: nodeData.name,
category: nodeData.category,
id: nodeData.id,
paramValues
}
])
}
setVSFlowData([
{
label: nodeData.label,
name: nodeData.name,
category: nodeData.category,
id: nodeData.id,
paramValues
}
])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getSpecificNodeApi.data])
}, [getVectorStoreNodeApi.data])
// Process Record Manager node data
useEffect(() => {
if (getRecordManagerNodeApi.data && dialogProps.recordManagerConfig) {
const nodeData = cloneDeep(initNode(getRecordManagerNodeApi.data, uuidv4()))
const paramValues = []
for (const inputName in dialogProps.recordManagerConfig.config) {
const inputParam = nodeData.inputParams.find((inp) => inp.name === inputName)
if (!inputParam) continue
if (inputParam.type === 'credential') continue
const inputValue = dialogProps.recordManagerConfig.config[inputName]
if (!inputValue) continue
if (typeof inputValue === 'string' && inputValue.startsWith('{{') && inputValue.endsWith('}}')) {
continue
}
paramValues.push({
label: inputParam?.label,
name: inputParam?.name,
type: inputParam?.type,
value: inputValue
})
}
setRMFlowData([
{
label: nodeData.label,
name: nodeData.name,
category: nodeData.category,
id: nodeData.id,
paramValues
}
])
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getRecordManagerNodeApi.data])
const component = show ? (
<Dialog
@@ -142,91 +173,130 @@ const DeleteDocStoreDialog = ({ show, dialogProps, onCancel, onDelete }) => {
<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 }}>
<DialogContent
sx={{
display: 'flex',
flexDirection: 'column',
gap: 2,
maxHeight: '75vh',
position: 'relative',
px: 3,
pb: 3,
overflow: 'auto'
}}
>
<span style={{ marginTop: '20px' }}>{dialogProps.description}</span>
{dialogProps.type === 'STORE' && dialogProps.recordManagerConfig && (
<FormControlLabel
control={<Checkbox checked={removeFromVS} onChange={(event) => setRemoveFromVS(event.target.checked)} />}
label='Remove data from vector store and record manager'
/>
{dialogProps.vectorStoreConfig && !dialogProps.recordManagerConfig && (
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
borderRadius: 10,
background: 'rgb(254,252,191)',
padding: 10
}}
>
<IconAlertTriangle size={70} color='orange' />
<span style={{ color: 'rgb(116,66,16)', marginLeft: 10 }}>
<strong>Note:</strong> Without a Record Manager configured, only the document chunks will be removed from the
document store. The actual vector embeddings in your vector store database will remain unchanged. To enable
automatic cleanup of vector store data, please configure a Record Manager.{' '}
<Link
href='https://docs.flowiseai.com/integrations/langchain/record-managers'
target='_blank'
rel='noopener noreferrer'
sx={{ fontWeight: 500, color: 'rgb(116,66,16)', textDecoration: 'underline' }}
>
Learn more
</Link>
</span>
</div>
)}
{removeFromVS && (
<div>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableBody>
<TableRow sx={{ '& td': { border: 0 } }}>
<TableCell sx={{ pb: 0, pt: 0 }} colSpan={6}>
<Box>
{([...vsFlowData, ...rmFlowData] || []).map((node, index) => {
return (
<Accordion
expanded={nodeConfigExpanded[node.name] || true}
onChange={handleAccordionChange(node.name)}
key={index}
disableGutters
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${node.name}`}
id={`nodes-accordian-header-${node.name}`}
{vsFlowData && vsFlowData.length > 0 && rmFlowData && rmFlowData.length > 0 && (
<Card sx={{ borderColor: theme.palette.primary[200] + 75, p: 2 }} variant='outlined'>
<Stack sx={{ mt: 1, mb: 2, ml: 1, alignItems: 'center' }} direction='row' spacing={2}>
<SettingsIcon />
<Typography variant='h4'>Configuration</Typography>
</Stack>
<Stack direction='column'>
<TableContainer component={Paper} sx={{ maxHeight: '400px', overflow: 'auto' }}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableBody>
<TableRow sx={{ '& td': { border: 0 } }}>
<TableCell sx={{ pb: 0, pt: 0 }} colSpan={6}>
<Box>
{([...vsFlowData, ...rmFlowData] || []).map((node, index) => {
return (
<Accordion
expanded={nodeConfigExpanded[node.name] || false}
onChange={handleAccordionChange(node.name)}
key={index}
disableGutters
>
<div
style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${node.name}`}
id={`nodes-accordian-header-${node.name}`}
>
<div
style={{
width: 40,
height: 40,
marginRight: 10,
borderRadius: '50%',
backgroundColor: 'white'
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<img
<div
style={{
width: '100%',
height: '100%',
padding: 7,
width: 40,
height: 40,
marginRight: 10,
borderRadius: '50%',
objectFit: 'contain'
backgroundColor: 'white'
}}
alt={node.name}
src={`${baseURL}/api/v1/node-icon/${node.name}`}
/>
>
<img
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={node.name}
src={`${baseURL}/api/v1/node-icon/${node.name}`}
/>
</div>
<Typography variant='h5'>{node.label}</Typography>
</div>
<Typography variant='h5'>{node.label}</Typography>
</div>
</AccordionSummary>
<AccordionDetails>
{node.paramValues[0] && (
<TableViewOnly
sx={{ minWidth: 150 }}
rows={node.paramValues}
columns={Object.keys(node.paramValues[0])}
/>
)}
</AccordionDetails>
</Accordion>
)
})}
</Box>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
<span style={{ marginTop: '30px', fontStyle: 'italic', color: '#b35702' }}>
* Only data that were upserted with Record Manager will be deleted from vector store
</span>
</div>
</AccordionSummary>
<AccordionDetails sx={{ p: 0 }}>
{node.paramValues[0] && (
<TableViewOnly
sx={{ minWidth: 150 }}
rows={node.paramValues}
columns={Object.keys(node.paramValues[0])}
/>
)}
</AccordionDetails>
</Accordion>
)
})}
</Box>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Stack>
</Card>
)}
</DialogContent>
<DialogActions sx={{ pr: 3, pb: 3 }}>
<Button onClick={onCancel} color='primary'>
Cancel
</Button>
<Button variant='contained' onClick={() => onDelete(dialogProps.type, dialogProps.file, removeFromVS)} color='error'>
<Button variant='contained' onClick={() => onDelete(dialogProps.type, dialogProps.file)} color='error'>
Delete
</Button>
</DialogActions>
@@ -186,19 +186,19 @@ const DocumentStoreDetails = () => {
setShowDocumentLoaderListDialog(true)
}
const deleteVectorStoreDataFromStore = async (storeId) => {
const deleteVectorStoreDataFromStore = async (storeId, docId) => {
try {
await documentsApi.deleteVectorStoreDataFromStore(storeId)
await documentsApi.deleteVectorStoreDataFromStore(storeId, docId)
} catch (error) {
console.error(error)
}
}
const onDocStoreDelete = async (type, file, removeFromVectorStore) => {
const onDocStoreDelete = async (type, file) => {
setBackdropLoading(true)
setShowDeleteDocStoreDialog(false)
if (type === 'STORE') {
if (removeFromVectorStore) {
if (documentStore.recordManagerConfig) {
await deleteVectorStoreDataFromStore(storeId)
}
try {
@@ -239,6 +239,9 @@ const DocumentStoreDetails = () => {
})
}
} else if (type === 'LOADER') {
if (documentStore.recordManagerConfig) {
await deleteVectorStoreDataFromStore(storeId, file.id)
}
try {
const deleteResp = await documentsApi.deleteLoaderFromStore(storeId, file.id)
setBackdropLoading(false)
@@ -280,9 +283,40 @@ const DocumentStoreDetails = () => {
}
const onLoaderDelete = (file, vectorStoreConfig, recordManagerConfig) => {
// Get the display name in the format "LoaderName (sourceName)"
const loaderName = file.loaderName || 'Unknown'
let sourceName = ''
// Prefer files.name when files array exists and has items
if (file.files && Array.isArray(file.files) && file.files.length > 0) {
sourceName = file.files.map((f) => f.name).join(', ')
} else if (file.source) {
// Fallback to source logic
if (typeof file.source === 'string' && file.source.includes('base64')) {
sourceName = getFileName(file.source)
} else if (typeof file.source === 'string' && file.source.startsWith('[') && file.source.endsWith(']')) {
sourceName = JSON.parse(file.source).join(', ')
} else if (typeof file.source === 'string') {
sourceName = file.source
}
}
const displayName = sourceName ? `${loaderName} (${sourceName})` : loaderName
let description = `Delete "${displayName}"? This will delete all the associated document chunks from the document store.`
if (
recordManagerConfig &&
vectorStoreConfig &&
Object.keys(recordManagerConfig).length > 0 &&
Object.keys(vectorStoreConfig).length > 0
) {
description = `Delete "${displayName}"? This will delete all the associated document chunks from the document store and remove the actual data from the vector store database.`
}
const props = {
title: `Delete`,
description: `Delete Loader ${file.loaderName} ? This will delete all the associated document chunks.`,
description,
vectorStoreConfig,
recordManagerConfig,
type: 'LOADER',
@@ -294,9 +328,20 @@ const DocumentStoreDetails = () => {
}
const onStoreDelete = (vectorStoreConfig, recordManagerConfig) => {
let description = `Delete Store ${getSpecificDocumentStore.data?.name}? This will delete all the associated loaders and document chunks from the document store.`
if (
recordManagerConfig &&
vectorStoreConfig &&
Object.keys(recordManagerConfig).length > 0 &&
Object.keys(vectorStoreConfig).length > 0
) {
description = `Delete Store ${getSpecificDocumentStore.data?.name}? This will delete all the associated loaders and document chunks from the document store, and remove the actual data from the vector store database.`
}
const props = {
title: `Delete`,
description: `Delete Store ${getSpecificDocumentStore.data?.name} ? This will delete all the associated loaders and document chunks.`,
description,
vectorStoreConfig,
recordManagerConfig,
type: 'STORE'
@@ -481,7 +526,10 @@ const DocumentStoreDetails = () => {
>
<MenuItem
disabled={documentStore?.totalChunks <= 0 || documentStore?.status === 'UPSERTING'}
onClick={() => showStoredChunks('all')}
onClick={() => {
handleClose()
showStoredChunks('all')
}}
disableRipple
>
<FileChunksIcon />
@@ -490,7 +538,10 @@ const DocumentStoreDetails = () => {
<Available permission={'documentStores:upsert-config'}>
<MenuItem
disabled={documentStore?.totalChunks <= 0 || documentStore?.status === 'UPSERTING'}
onClick={() => showVectorStore(documentStore.id)}
onClick={() => {
handleClose()
showVectorStore(documentStore.id)
}}
disableRipple
>
<NoteAddIcon />
@@ -499,7 +550,10 @@ const DocumentStoreDetails = () => {
</Available>
<MenuItem
disabled={documentStore?.totalChunks <= 0 || documentStore?.status !== 'UPSERTED'}
onClick={() => showVectorStoreQuery(documentStore.id)}
onClick={() => {
handleClose()
showVectorStoreQuery(documentStore.id)
}}
disableRipple
>
<SearchIcon />
@@ -518,7 +572,10 @@ const DocumentStoreDetails = () => {
</Available>
<Divider sx={{ my: 0.5 }} />
<MenuItem
onClick={() => onStoreDelete(documentStore.vectorStoreConfig, documentStore.recordManagerConfig)}
onClick={() => {
handleClose()
onStoreDelete(documentStore.vectorStoreConfig, documentStore.recordManagerConfig)
}}
disableRipple
>
<FileDeleteIcon />
@@ -756,20 +813,26 @@ function LoaderRow(props) {
setAnchorEl(null)
}
const formatSources = (files, source) => {
const formatSources = (files, source, loaderName) => {
let sourceName = ''
// Prefer files.name when files array exists and has items
if (files && Array.isArray(files) && files.length > 0) {
return files.map((file) => file.name).join(', ')
sourceName = files.map((file) => file.name).join(', ')
} else if (source && typeof source === 'string' && source.includes('base64')) {
// Fallback to original source logic
sourceName = getFileName(source)
} else if (source && typeof source === 'string' && source.startsWith('[') && source.endsWith(']')) {
sourceName = JSON.parse(source).join(', ')
} else if (source) {
sourceName = source
}
// Fallback to original source logic
if (source && typeof source === 'string' && source.includes('base64')) {
return getFileName(source)
// Return format: "LoaderName (sourceName)" or just "LoaderName" if no source
if (!sourceName) {
return loaderName || 'No source'
}
if (source && typeof source === 'string' && source.startsWith('[') && source.endsWith(']')) {
return JSON.parse(source).join(', ')
}
return source || 'No source'
return loaderName ? `${loaderName} (${sourceName})` : sourceName
}
return (
@@ -823,32 +886,62 @@ function LoaderRow(props) {
onClose={handleClose}
>
<Available permission={'documentStores:preview-process'}>
<MenuItem onClick={props.onEditClick} disableRipple>
<MenuItem
onClick={() => {
handleClose()
props.onEditClick()
}}
disableRipple
>
<FileEditIcon />
Preview & Process
</MenuItem>
</Available>
<Available permission={'documentStores:preview-process'}>
<MenuItem onClick={props.onViewChunksClick} disableRipple>
<MenuItem
onClick={() => {
handleClose()
props.onViewChunksClick()
}}
disableRipple
>
<FileChunksIcon />
View & Edit Chunks
</MenuItem>
</Available>
<Available permission={'documentStores:preview-process'}>
<MenuItem onClick={props.onChunkUpsert} disableRipple>
<MenuItem
onClick={() => {
handleClose()
props.onChunkUpsert()
}}
disableRipple
>
<NoteAddIcon />
Upsert Chunks
</MenuItem>
</Available>
<Available permission={'documentStores:preview-process'}>
<MenuItem onClick={props.onViewUpsertAPI} disableRipple>
<MenuItem
onClick={() => {
handleClose()
props.onViewUpsertAPI()
}}
disableRipple
>
<CodeIcon />
View API
</MenuItem>
</Available>
<Divider sx={{ my: 0.5 }} />
<Available permission={'documentStores:delete-loader'}>
<MenuItem onClick={props.onDeleteClick} disableRipple>
<MenuItem
onClick={() => {
handleClose()
props.onDeleteClick()
}}
disableRipple
>
<FileDeleteIcon />
Delete
</MenuItem>
@@ -26,6 +26,7 @@ import useApi from '@/hooks/useApi'
import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier'
import { useAuth } from '@/hooks/useAuth'
import { getFileName } from '@/utils/genericHelper'
// store
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
@@ -76,6 +77,7 @@ const ShowStoredChunks = () => {
const [showExpandedChunkDialog, setShowExpandedChunkDialog] = useState(false)
const [expandedChunkDialogProps, setExpandedChunkDialogProps] = useState({})
const [fileNames, setFileNames] = useState([])
const [loaderDisplayName, setLoaderDisplayName] = useState('')
const chunkSelected = (chunkId) => {
const selectedChunk = documentChunks.find((chunk) => chunk.id === chunkId)
@@ -212,13 +214,32 @@ const ShowStoredChunks = () => {
setCurrentPage(data.currentPage)
setStart(data.currentPage * 50 - 49)
setEnd(data.currentPage * 50 > data.count ? data.count : data.currentPage * 50)
// Build the loader display name in format "LoaderName (sourceName)"
const loaderName = data.file?.loaderName || data.storeName || ''
let sourceName = ''
if (data.file?.files && data.file.files.length > 0) {
const fileNames = []
for (const attachedFile of data.file.files) {
fileNames.push(attachedFile.name)
}
setFileNames(fileNames)
sourceName = fileNames.join(', ')
} else if (data.file?.source) {
const source = data.file.source
if (typeof source === 'string' && source.includes('base64')) {
sourceName = getFileName(source)
} else if (typeof source === 'string' && source.startsWith('[') && source.endsWith(']')) {
sourceName = JSON.parse(source).join(', ')
} else if (typeof source === 'string') {
sourceName = source
}
}
// Set display name in format "LoaderName (sourceName)" or just "LoaderName"
const displayName = sourceName ? `${loaderName} (${sourceName})` : loaderName
setLoaderDisplayName(displayName)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -234,7 +255,7 @@ const ShowStoredChunks = () => {
<ViewHeader
isBackButton={true}
search={false}
title={getChunksApi.data?.file?.loaderName || getChunksApi.data?.storeName}
title={loaderDisplayName}
description={getChunksApi.data?.file?.splitterName || getChunksApi.data?.description}
onBack={() => navigate(-1)}
></ViewHeader>
@@ -40,7 +40,7 @@ import Storage from '@mui/icons-material/Storage'
import DynamicFeed from '@mui/icons-material/Filter1'
// utils
import { initNode, showHideInputParams } from '@/utils/genericHelper'
import { initNode, showHideInputParams, getFileName } from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier'
// const
@@ -69,6 +69,7 @@ const VectorStoreConfigure = () => {
const [loading, setLoading] = useState(true)
const [documentStore, setDocumentStore] = useState({})
const [dialogProps, setDialogProps] = useState({})
const [currentLoader, setCurrentLoader] = useState(null)
const [showEmbeddingsListDialog, setShowEmbeddingsListDialog] = useState(false)
const [selectedEmbeddingsProvider, setSelectedEmbeddingsProvider] = useState({})
@@ -245,7 +246,8 @@ const VectorStoreConfigure = () => {
const prepareConfigData = () => {
const data = {
storeId: storeId,
docId: docId
docId: docId,
isStrictSave: true
}
// Set embedding config
if (selectedEmbeddingsProvider.inputs) {
@@ -353,6 +355,39 @@ const VectorStoreConfigure = () => {
return Object.keys(selectedEmbeddingsProvider).length === 0
}
const getLoaderDisplayName = (loader) => {
if (!loader) return ''
const loaderName = loader.loaderName || 'Unknown'
let sourceName = ''
// Prefer files.name when files array exists and has items
if (loader.files && Array.isArray(loader.files) && loader.files.length > 0) {
sourceName = loader.files.map((file) => file.name).join(', ')
} else if (loader.source) {
// Fallback to source logic
if (typeof loader.source === 'string' && loader.source.includes('base64')) {
sourceName = getFileName(loader.source)
} else if (typeof loader.source === 'string' && loader.source.startsWith('[') && loader.source.endsWith(']')) {
sourceName = JSON.parse(loader.source).join(', ')
} else if (typeof loader.source === 'string') {
sourceName = loader.source
}
}
// Return format: "LoaderName (sourceName)" or just "LoaderName" if no source
return sourceName ? `${loaderName} (${sourceName})` : loaderName
}
const getViewHeaderTitle = () => {
const storeName = getSpecificDocumentStoreApi.data?.name || ''
if (docId && currentLoader) {
const loaderName = getLoaderDisplayName(currentLoader)
return `${storeName} / ${loaderName}`
}
return storeName
}
useEffect(() => {
if (saveVectorStoreConfigApi.data) {
setLoading(false)
@@ -411,6 +446,15 @@ const VectorStoreConfigure = () => {
return
}
setDocumentStore(docStore)
// Find the current loader if docId is provided
if (docId && docStore.loaders) {
const loader = docStore.loaders.find((l) => l.id === docId)
if (loader) {
setCurrentLoader(loader)
}
}
if (docStore.embeddingConfig) {
getEmbeddingNodeDetailsApi.request(docStore.embeddingConfig.name)
}
@@ -473,7 +517,7 @@ const VectorStoreConfigure = () => {
<ViewHeader
isBackButton={true}
search={false}
title={getSpecificDocumentStoreApi.data?.name}
title={getViewHeaderTitle()}
description='Configure Embeddings, Vector Store and Record Manager'
onBack={() => navigate(-1)}
>