Merge branch 'main' of https://github.com/use-the-fork/Flowise into maintenance/pnpm-vite-jsx

# Conflicts:
#	packages/ui/src/views/canvas/index.jsx
#	packages/ui/src/views/chatflows/APICodeDialog.jsx
#	packages/ui/src/views/chatmessage/ChatMessage.jsx
This commit is contained in:
Greg L
2023-11-23 18:35:39 -05:00
124 changed files with 11050 additions and 4823 deletions
+7
View File
@@ -0,0 +1,7 @@
import client from './client'
const upsertVectorStore = (id, input) => client.post(`/vector/internal-upsert/${id}`, input)
export default {
upsertVectorStore
}
@@ -31,6 +31,11 @@ $orangeLight: #fbe9e7;
$orangeMain: #ffab91;
$orangeDark: #d84315;
// brown
$tealLight: #76c893;
$tealMain: #52b69a;
$tealDark: #34a0a4;
// warning
$warningLight: #fff8e1;
$warningMain: #ffe57f;
@@ -46,6 +51,9 @@ $grey600: #757575;
$grey700: #616161;
$grey900: #212121;
// transparent
$transparent: #ffffff00;
// ==============================|| DARK THEME VARIANTS ||============================== //
// paper & background
@@ -111,6 +119,11 @@ $darkTextSecondary: #8492c4;
orangeMain: $orangeMain;
orangeDark: $orangeDark;
// orange
tealLight: $tealLight;
tealMain: $tealMain;
tealDark: $tealDark;
// warning
warningLight: $warningLight;
warningMain: $warningMain;
@@ -154,4 +167,7 @@ $darkTextSecondary: #8492c4;
darkSecondaryDark: $darkSecondaryDark;
darkSecondary200: $darkSecondary200;
darkSecondary800: $darkSecondary800;
// transparent
transparent: $transparent;
}
@@ -97,9 +97,8 @@ export const ReactFlowContext = ({ children }) => {
selected: false
}
const dataKeys = ['inputParams', 'inputAnchors', 'outputAnchors']
for (const key of dataKeys) {
const inputKeys = ['inputParams', 'inputAnchors']
for (const key of inputKeys) {
for (const item of duplicatedNode.data[key]) {
if (item.id) {
item.id = item.id.replace(id, newNodeId)
@@ -107,6 +106,35 @@ export const ReactFlowContext = ({ children }) => {
}
}
const outputKeys = ['outputAnchors']
for (const key of outputKeys) {
for (const item of duplicatedNode.data[key]) {
if (item.id) {
item.id = item.id.replace(id, newNodeId)
}
if (item.options) {
for (const output of item.options) {
output.id = output.id.replace(id, newNodeId)
}
}
}
}
// Clear connected inputs
for (const inputName in duplicatedNode.data.inputs) {
if (
typeof duplicatedNode.data.inputs[inputName] === 'string' &&
duplicatedNode.data.inputs[inputName].startsWith('{{') &&
duplicatedNode.data.inputs[inputName].endsWith('}}')
) {
duplicatedNode.data.inputs[inputName] = ''
} else if (Array.isArray(duplicatedNode.data.inputs[inputName])) {
duplicatedNode.data.inputs[inputName] = duplicatedNode.data.inputs[inputName].filter(
(item) => !(typeof item === 'string' && item.startsWith('{{') && item.endsWith('}}'))
)
}
}
reactFlowInstance.setNodes([...nodes, duplicatedNode])
dispatch({ type: SET_DIRTY })
}
+6
View File
@@ -6,6 +6,7 @@
export default function themePalette(theme) {
return {
mode: theme?.customization?.navType,
transparent: theme.colors?.transparent,
common: {
black: theme.colors?.darkPaper,
dark: theme.colors?.darkPrimaryMain
@@ -34,6 +35,11 @@ export default function themePalette(theme) {
main: theme.colors?.orangeMain,
dark: theme.colors?.orangeDark
},
teal: {
light: theme.colors?.tealLight,
main: theme.colors?.tealMain,
dark: theme.colors?.tealDark
},
warning: {
light: theme.colors?.warningLight,
main: theme.colors?.warningMain,
@@ -106,6 +106,32 @@ const NodeInfoDialog = ({ show, dialogProps, onCancel }) => {
<span style={{ color: '#606c38', fontSize: '0.825rem' }}>version {dialogProps.data.version}</span>
</div>
)}
{dialogProps.data.badge && (
<div
style={{
display: 'flex',
flexDirection: 'row',
width: 'max-content',
borderRadius: 15,
background: dialogProps.data.badge === 'DEPRECATING' ? '#ffe57f' : '#52b69a',
padding: 5,
paddingLeft: 10,
paddingRight: 10,
marginTop: 5,
marginLeft: 10,
marginBottom: 5
}}
>
<span
style={{
color: dialogProps.data.badge !== 'DEPRECATING' ? 'white' : 'inherit',
fontSize: '0.825rem'
}}
>
{dialogProps.data.badge}
</span>
</div>
)}
</div>
</div>
</div>
@@ -123,7 +149,14 @@ const NodeInfoDialog = ({ show, dialogProps, onCancel }) => {
</div>
)}
{getNodeConfigApi.data && getNodeConfigApi.data.length > 0 && (
<TableViewOnly rows={getNodeConfigApi.data} columns={Object.keys(getNodeConfigApi.data[0]).slice(-3)} />
<TableViewOnly
rows={getNodeConfigApi.data.map((obj) => {
// eslint-disable-next-line
const { node, nodeId, ...rest } = obj
return rest
})}
columns={Object.keys(getNodeConfigApi.data[0]).slice(-3)}
/>
)}
</DialogContent>
</Dialog>
+7 -8
View File
@@ -1,11 +1,11 @@
import PropTypes from 'prop-types'
import { TableContainer, Table, TableHead, TableCell, TableRow, TableBody, Paper } from '@mui/material'
export const TableViewOnly = ({ columns, rows }) => {
export const TableViewOnly = ({ columns, rows, sx }) => {
return (
<>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<Table sx={{ minWidth: 650, ...sx }} aria-label='simple table'>
<TableHead>
<TableRow>
{columns.map((col, index) => (
@@ -16,11 +16,9 @@ export const TableViewOnly = ({ columns, rows }) => {
<TableBody>
{rows.map((row, index) => (
<TableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
{Object.keys(row)
.slice(-3)
.map((key, index) => (
<TableCell key={index}>{row[key]}</TableCell>
))}
{Object.keys(row).map((key, index) => (
<TableCell key={index}>{row[key]}</TableCell>
))}
</TableRow>
))}
</TableBody>
@@ -32,5 +30,6 @@ export const TableViewOnly = ({ columns, rows }) => {
TableViewOnly.propTypes = {
rows: PropTypes.array,
columns: PropTypes.array
columns: PropTypes.array,
sx: PropTypes.object
}
+154
View File
@@ -332,6 +332,57 @@ export const getAvailableNodesForVariable = (nodes, edges, target, targetHandle)
return parentNodes
}
export const getUpsertDetails = (nodes, edges) => {
const vsNodes = nodes.filter(
(node) =>
node.data.category === 'Vector Stores' && !node.data.label.includes('Upsert') && !node.data.label.includes('Load Existing')
)
const vsNodeIds = vsNodes.map((vs) => vs.data.id)
const upsertNodes = []
const seenVsNodeIds = []
for (const edge of edges) {
if (vsNodeIds.includes(edge.source) || vsNodeIds.includes(edge.target)) {
const vsNode = vsNodes.find((node) => node.data.id === edge.source || node.data.id === edge.target)
if (!vsNode || seenVsNodeIds.includes(vsNode.data.id)) continue
seenVsNodeIds.push(vsNode.data.id)
// Found Vector Store Node, proceed to find connected Document Loader node
let connectedDocs = []
if (vsNode.data.inputs.document) connectedDocs = [...new Set(vsNode.data.inputs.document)]
if (connectedDocs.length) {
const innerNodes = [vsNode]
if (vsNode.data.inputs.embeddings) {
const embeddingsId = vsNode.data.inputs.embeddings.replace(/{{|}}/g, '').split('.')[0]
innerNodes.push(nodes.find((node) => node.data.id === embeddingsId))
}
for (const doc of connectedDocs) {
const docId = doc.replace(/{{|}}/g, '').split('.')[0]
const docNode = nodes.find((node) => node.data.id === docId)
if (docNode) innerNodes.push(docNode)
// Found Document Loader Node, proceed to find connected Text Splitter node
if (docNode && docNode.data.inputs.textSplitter) {
const textSplitterId = docNode.data.inputs.textSplitter.replace(/{{|}}/g, '').split('.')[0]
const textSplitterNode = nodes.find((node) => node.data.id === textSplitterId)
if (textSplitterNode) innerNodes.push(textSplitterNode)
}
}
upsertNodes.push({
vectorNode: vsNode,
nodes: innerNodes.reverse()
})
}
}
}
return upsertNodes
}
export const rearrangeToolsOrdering = (newValues, sourceNodeId) => {
// RequestsGet and RequestsPost have to be in order before other tools
newValues.push(`{{${sourceNodeId}.data.instance}}`)
@@ -458,3 +509,106 @@ export const formatDataGridRows = (rows) => {
return []
}
}
export const setLocalStorageChatflow = (chatflowid, chatId, chatHistory) => {
const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`)
const obj = {}
if (chatId) obj.chatId = chatId
if (chatHistory) obj.chatHistory = chatHistory
if (!chatDetails) {
localStorage.setItem(`${chatflowid}_INTERNAL`, JSON.stringify(obj))
} else {
try {
const parsedChatDetails = JSON.parse(chatDetails)
localStorage.setItem(`${chatflowid}_INTERNAL`, JSON.stringify({ ...parsedChatDetails, ...obj }))
} catch (e) {
const chatId = chatDetails
obj.chatId = chatId
localStorage.setItem(`${chatflowid}_INTERNAL`, JSON.stringify(obj))
}
}
}
export const unshiftFiles = (configData) => {
const filesConfig = configData.find((config) => config.name === 'files')
if (filesConfig) {
configData = configData.filter((config) => config.name !== 'files')
configData.unshift(filesConfig)
}
return configData
}
export const getConfigExamplesForJS = (configData, bodyType, isMultiple, stopNodeId) => {
let finalStr = ''
configData = unshiftFiles(configData)
const loop = Math.min(configData.length, 4)
for (let i = 0; i < loop; i += 1) {
const config = configData[i]
let exampleVal = `"example"`
if (config.type === 'string') exampleVal = `"example"`
else if (config.type === 'boolean') exampleVal = `true`
else if (config.type === 'number') exampleVal = `1`
else if (config.type === 'json') exampleVal = `{ "key": "val" }`
else if (config.name === 'files') exampleVal = `input.files[0]`
finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `formData.append("${config.name}", ${exampleVal})\n`
if (i === loop - 1 && bodyType !== 'json')
finalStr += !isMultiple
? ``
: stopNodeId
? `formData.append("stopNodeId", "${stopNodeId}")\n`
: `formData.append("question", "Hey, how are you?")\n`
}
return finalStr
}
export const getConfigExamplesForPython = (configData, bodyType, isMultiple, stopNodeId) => {
let finalStr = ''
configData = unshiftFiles(configData)
const loop = Math.min(configData.length, 4)
for (let i = 0; i < loop; i += 1) {
const config = configData[i]
let exampleVal = `"example"`
if (config.type === 'string') exampleVal = `"example"`
else if (config.type === 'boolean') exampleVal = `true`
else if (config.type === 'number') exampleVal = `1`
else if (config.type === 'json') exampleVal = `{ "key": "val" }`
else if (config.name === 'files') continue
finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `\n "${config.name}": ${exampleVal},`
if (i === loop - 1 && bodyType !== 'json')
finalStr += !isMultiple
? `\n`
: stopNodeId
? `\n "stopNodeId": "${stopNodeId}"\n`
: `\n "question": "Hey, how are you?"\n`
}
return finalStr
}
export const getConfigExamplesForCurl = (configData, bodyType, isMultiple, stopNodeId) => {
let finalStr = ''
configData = unshiftFiles(configData)
const loop = Math.min(configData.length, 4)
for (let i = 0; i < loop; i += 1) {
const config = configData[i]
let exampleVal = `example`
if (config.type === 'string') exampleVal = bodyType === 'json' ? `"example"` : `example`
else if (config.type === 'boolean') exampleVal = `true`
else if (config.type === 'number') exampleVal = `1`
else if (config.type === 'json') exampleVal = `{key:val}`
else if (config.name === 'files')
exampleVal = `@/home/user1/Desktop/example${config.type.includes(',') ? config.type.split(',')[0] : config.type}`
finalStr += bodyType === 'json' ? `"${config.name}": ${exampleVal}` : `\n -F "${config.name}=${exampleVal}"`
if (i === loop - 1)
finalStr +=
bodyType === 'json'
? ` }`
: !isMultiple
? ``
: stopNodeId
? ` \\\n -F "stopNodeId=${stopNodeId}"`
: ` \\\n -F "question=Hey, how are you?"`
else finalStr += bodyType === 'json' ? `, ` : ` \\`
}
return finalStr
}
+147 -59
View File
@@ -21,7 +21,8 @@ import {
Paper,
Popper,
Stack,
Typography
Typography,
Chip
} from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
@@ -56,6 +57,24 @@ const AddNodes = ({ nodesData, node }) => {
const prevOpen = useRef(open)
const ps = useRef()
// Temporary method to handle Deprecating Vector Store and New ones
const categorizeVectorStores = (nodes, accordianCategories, isFilter) => {
const obj = { ...nodes }
const vsNodes = obj['Vector Stores'] ?? []
const deprecatingNodes = []
const newNodes = []
for (const vsNode of vsNodes) {
if (vsNode.badge === 'DEPRECATING') deprecatingNodes.push(vsNode)
else newNodes.push(vsNode)
}
delete obj['Vector Stores']
obj['Vector Stores;DEPRECATING'] = deprecatingNodes
accordianCategories['Vector Stores;DEPRECATING'] = isFilter ? true : false
obj['Vector Stores;NEW'] = newNodes
accordianCategories['Vector Stores;NEW'] = isFilter ? true : false
setNodes(obj)
}
const scrollTop = () => {
const curr = ps.current
if (curr) {
@@ -95,6 +114,7 @@ const AddNodes = ({ nodesData, node }) => {
return r
}, Object.create(null))
setNodes(result)
categorizeVectorStores(result, accordianCategories, isFilter)
setCategoryExpanded(accordianCategories)
}
@@ -137,6 +157,8 @@ const AddNodes = ({ nodesData, node }) => {
groupByCategory(nodesData)
dispatch({ type: SET_COMPONENT_NODES, componentNodes: nodesData })
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodesData, dispatch])
return (
@@ -249,69 +271,135 @@ const AddNodes = ({ nodesData, node }) => {
>
{Object.keys(nodes)
.sort()
.map((category) => (
<Accordion
expanded={categoryExpanded[category] || false}
onChange={handleAccordionChange(category)}
key={category}
disableGutters
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${category}`}
id={`nodes-accordian-header-${category}`}
.map((category) =>
category === 'Vector Stores' ? (
<></>
) : (
<Accordion
expanded={categoryExpanded[category] || false}
onChange={handleAccordionChange(category)}
key={category}
disableGutters
>
<Typography variant='h5'>{category}</Typography>
</AccordionSummary>
<AccordionDetails>
{nodes[category].map((node, index) => (
<div
key={node.name}
onDragStart={(event) => onDragStart(event, node)}
draggable
>
<ListItemButton
sx={{
p: 0,
borderRadius: `${customization.borderRadius}px`,
cursor: 'move'
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${category}`}
id={`nodes-accordian-header-${category}`}
>
{category.split(';').length > 1 ? (
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<ListItem alignItems='center'>
<ListItemAvatar>
<div
style={{
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
<Typography variant='h5'>{category.split(';')[0]}</Typography>
&nbsp;
<Chip
sx={{
width: 'max-content',
fontWeight: 700,
fontSize: '0.65rem',
background:
category.split(';')[1] === 'DEPRECATING'
? theme.palette.warning.main
: theme.palette.teal.main,
color:
category.split(';')[1] !== 'DEPRECATING'
? 'white'
: 'inherit'
}}
size='small'
label={category.split(';')[1]}
/>
</div>
) : (
<Typography variant='h5'>{category}</Typography>
)}
</AccordionSummary>
<AccordionDetails>
{nodes[category].map((node, index) => (
<div
key={node.name}
onDragStart={(event) => onDragStart(event, node)}
draggable
>
<ListItemButton
sx={{
p: 0,
borderRadius: `${customization.borderRadius}px`,
cursor: 'move'
}}
>
<ListItem alignItems='center'>
<ListItemAvatar>
<div
style={{
width: '100%',
height: '100%',
padding: 10,
objectFit: 'contain'
width: 50,
height: 50,
borderRadius: '50%',
backgroundColor: 'white'
}}
alt={node.name}
src={`${baseURL}/api/v1/node-icon/${node.name}`}
/>
</div>
</ListItemAvatar>
<ListItemText
sx={{ ml: 1 }}
primary={node.label}
secondary={node.description}
/>
</ListItem>
</ListItemButton>
{index === nodes[category].length - 1 ? null : <Divider />}
</div>
))}
</AccordionDetails>
</Accordion>
))}
>
<img
style={{
width: '100%',
height: '100%',
padding: 10,
objectFit: 'contain'
}}
alt={node.name}
src={`${baseURL}/api/v1/node-icon/${node.name}`}
/>
</div>
</ListItemAvatar>
<ListItemText
sx={{ ml: 1 }}
primary={
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<span>{node.label}</span>
&nbsp;
{node.badge && (
<Chip
sx={{
width: 'max-content',
fontWeight: 700,
fontSize: '0.65rem',
background:
node.badge === 'DEPRECATING'
? theme.palette.warning
.main
: theme.palette.teal
.main,
color:
node.badge !== 'DEPRECATING'
? 'white'
: 'inherit'
}}
size='small'
label={node.badge}
/>
)}
</div>
}
secondary={node.description}
/>
</ListItem>
</ListItemButton>
{index === nodes[category].length - 1 ? null : <Divider />}
</div>
))}
</AccordionDetails>
</Accordion>
)
)}
</List>
</Box>
</PerfectScrollbar>
+4 -2
View File
@@ -83,8 +83,10 @@ const CanvasNode = ({ data }) => {
if (componentNode) {
if (!data.version) {
setWarningMessage(nodeVersionEmptyMessage(componentNode.version))
} else {
if (componentNode.version > data.version) setWarningMessage(nodeOutdatedMessage(data.version, componentNode.version))
} else if (data.version && componentNode.version > data.version) {
setWarningMessage(nodeOutdatedMessage(data.version, componentNode.version))
} else if (componentNode.badge === 'DEPRECATING') {
setWarningMessage('This node will be deprecated in the next release. Change to a new node tagged with NEW')
}
}
}, [canvas.componentNodes, data.name, data.version])
+28 -12
View File
@@ -11,7 +11,7 @@ import {
SET_CHATFLOW,
enqueueSnackbar as enqueueSnackbarAction,
closeSnackbar as closeSnackbarAction
} from '@/store/actions'
} from 'store/actions'
import { omit, cloneDeep } from 'lodash'
// material-ui
@@ -23,27 +23,28 @@ import CanvasNode from './CanvasNode'
import ButtonEdge from './ButtonEdge'
import CanvasHeader from './CanvasHeader'
import AddNodes from './AddNodes'
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import { ChatPopUp } from '@/views/chatmessage/ChatPopUp'
import { flowContext } from '@/store/context/ReactFlowContext'
import ConfirmDialog from 'ui-component/dialog/ConfirmDialog'
import { ChatPopUp } from 'views/chatmessage/ChatPopUp'
import { VectorStorePopUp } from 'views/vectorstore/VectorStorePopUp'
import { flowContext } from 'store/context/ReactFlowContext'
// API
import nodesApi from '@/api/nodes'
import chatflowsApi from '@/api/chatflows'
import nodesApi from 'api/nodes'
import chatflowsApi from 'api/chatflows'
// Hooks
import useApi from '@/hooks/useApi'
import useConfirm from '@/hooks/useConfirm'
import useApi from 'hooks/useApi'
import useConfirm from 'hooks/useConfirm'
// icons
import { IconX } from '@tabler/icons'
// utils
import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering } from '@/utils/genericHelper'
import useNotifier from '@/utils/useNotifier'
import { getUniqueNodeId, initNode, getEdgeLabelName, rearrangeToolsOrdering, getUpsertDetails } from 'utils/genericHelper'
import useNotifier from 'utils/useNotifier'
// const
import { FLOWISE_CREDENTIAL_ID } from '@/store/constant'
import { FLOWISE_CREDENTIAL_ID } from 'store/constant'
const nodeTypes = { customNode: CanvasNode }
const edgeTypes = { buttonedge: ButtonEdge }
@@ -81,6 +82,7 @@ const Canvas = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState()
const [selectedNode, setSelectedNode] = useState(null)
const [isUpsertButtonEnabled, setIsUpsertButtonEnabled] = useState(false)
const reactFlowWrapper = useRef(null)
@@ -167,6 +169,7 @@ const Canvas = () => {
if (isConfirmed) {
try {
await chatflowsApi.deleteChatflow(chatflow.id)
localStorage.removeItem(`${chatflow.id}_INTERNAL`)
navigate(-1)
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
@@ -339,6 +342,12 @@ const Canvas = () => {
dispatch({ type: SET_DIRTY })
}
const checkIfUpsertAvailable = (nodes, edges) => {
const upsertNodeDetails = getUpsertDetails(nodes, edges)
if (upsertNodeDetails.length) setIsUpsertButtonEnabled(true)
else setIsUpsertButtonEnabled(false)
}
// ==============================|| useEffect ||============================== //
// Get specific chatflow successful
@@ -409,7 +418,13 @@ const Canvas = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [testChatflowApi.error])
useEffect(() => setChatflow(canvasDataStore.chatflow), [canvasDataStore.chatflow])
useEffect(() => {
setChatflow(canvasDataStore.chatflow)
if (canvasDataStore.chatflow) {
const flowData = canvasDataStore.chatflow.flowData ? JSON.parse(canvasDataStore.chatflow.flowData) : []
checkIfUpsertAvailable(flowData.nodes || [], flowData.edges || [])
}
}, [canvasDataStore.chatflow])
// Initialization
useEffect(() => {
@@ -524,6 +539,7 @@ const Canvas = () => {
/>
<Background color='#aaa' gap={16} />
<AddNodes nodesData={getNodesApi.data} node={selectedNode} />
{isUpsertButtonEnabled && <VectorStorePopUp chatflowid={chatflowId} />}
<ChatPopUp chatflowid={chatflowId} />
</ReactFlow>
</div>
@@ -20,34 +20,36 @@ import { CopyBlock, atomOneDark } from 'react-code-blocks'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
// Project import
import { Dropdown } from '@/ui-component/dropdown/Dropdown'
import { Dropdown } from 'ui-component/dropdown/Dropdown'
import ShareChatbot from './ShareChatbot'
import EmbedChat from './EmbedChat'
import Configuration from './Configuration'
// Const
import { baseURL } from '@/store/constant'
import { SET_CHATFLOW } from '@/store/actions'
import { baseURL } from 'store/constant'
import { SET_CHATFLOW } from 'store/actions'
// Images
import pythonSVG from '@/assets/images/python.svg'
import javascriptSVG from '@/assets/images/javascript.svg'
import cURLSVG from '@/assets/images/cURL.svg'
import EmbedSVG from '@/assets/images/embed.svg'
import ShareChatbotSVG from '@/assets/images/sharing.png'
import settingsSVG from '@/assets/images/settings.svg'
import pythonSVG from 'assets/images/python.svg'
import javascriptSVG from 'assets/images/javascript.svg'
import cURLSVG from 'assets/images/cURL.svg'
import EmbedSVG from 'assets/images/embed.svg'
import ShareChatbotSVG from 'assets/images/sharing.png'
import settingsSVG from 'assets/images/settings.svg'
import { IconBulb } from '@tabler/icons'
// API
import apiKeyApi from '@/api/apikey'
import chatflowsApi from '@/api/chatflows'
import configApi from '@/api/config'
import apiKeyApi from 'api/apikey'
import chatflowsApi from 'api/chatflows'
import configApi from 'api/config'
// Hooks
import useApi from '@/hooks/useApi'
import { CheckboxInput } from '@/ui-component/checkbox/Checkbox'
import { TableViewOnly } from '@/ui-component/table/Table'
import useApi from 'hooks/useApi'
import { CheckboxInput } from 'ui-component/checkbox/Checkbox'
import { TableViewOnly } from 'ui-component/table/Table'
import { IconBulb } from '@tabler/icons'
import Configuration from './Configuration'
// Helpers
import { unshiftFiles, getConfigExamplesForJS, getConfigExamplesForPython, getConfigExamplesForCurl } from 'utils/genericHelper'
function TabPanel(props) {
const { children, value, index, ...other } = props
@@ -77,67 +79,6 @@ function a11yProps(index) {
}
}
const unshiftFiles = (configData) => {
const filesConfig = configData.find((config) => config.name === 'files')
if (filesConfig) {
configData = configData.filter((config) => config.name !== 'files')
configData.unshift(filesConfig)
}
return configData
}
const getConfigExamplesForJS = (configData, bodyType) => {
let finalStr = ''
configData = unshiftFiles(configData)
const loop = Math.min(configData.length, 4)
for (let i = 0; i < loop; i += 1) {
const config = configData[i]
let exampleVal = `"example"`
if (config.type === 'string') exampleVal = `"example"`
else if (config.type === 'boolean') exampleVal = `true`
else if (config.type === 'number') exampleVal = `1`
else if (config.name === 'files') exampleVal = `input.files[0]`
finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `formData.append("${config.name}", ${exampleVal})\n`
if (i === loop - 1 && bodyType !== 'json') finalStr += `formData.append("question", "Hey, how are you?")\n`
}
return finalStr
}
const getConfigExamplesForPython = (configData, bodyType) => {
let finalStr = ''
configData = unshiftFiles(configData)
const loop = Math.min(configData.length, 4)
for (let i = 0; i < loop; i += 1) {
const config = configData[i]
let exampleVal = `"example"`
if (config.type === 'string') exampleVal = `"example"`
else if (config.type === 'boolean') exampleVal = `true`
else if (config.type === 'number') exampleVal = `1`
else if (config.name === 'files') continue
finalStr += bodyType === 'json' ? `\n "${config.name}": ${exampleVal},` : `\n "${config.name}": ${exampleVal},`
if (i === loop - 1 && bodyType !== 'json') finalStr += `\n "question": "Hey, how are you?"\n`
}
return finalStr
}
const getConfigExamplesForCurl = (configData, bodyType) => {
let finalStr = ''
configData = unshiftFiles(configData)
const loop = Math.min(configData.length, 4)
for (let i = 0; i < loop; i += 1) {
const config = configData[i]
let exampleVal = `example`
if (config.type === 'string') exampleVal = bodyType === 'json' ? `"example"` : `example`
else if (config.type === 'boolean') exampleVal = `true`
else if (config.type === 'number') exampleVal = `1`
else if (config.name === 'files') exampleVal = `@/home/user1/Desktop/example${config.type}`
finalStr += bodyType === 'json' ? `"${config.name}": ${exampleVal}` : `\n -F "${config.name}=${exampleVal}"`
if (i === loop - 1) finalStr += bodyType === 'json' ? ` }` : ` \\\n -F "question=Hey, how are you?"`
else finalStr += bodyType === 'json' ? `, ` : ` \\`
}
return finalStr
}
const APICodeDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const navigate = useNavigate()
@@ -334,7 +275,8 @@ query({"question": "Hey, how are you?"}).then((response) => {
const getConfigCodeWithFormData = (codeLang, configData) => {
if (codeLang === 'Python') {
configData = unshiftFiles(configData)
const fileType = configData[0].type
let fileType = configData[0].type
if (fileType.includes(',')) fileType = fileType.split(',')[0]
return `import requests
API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}"
@@ -384,7 +326,8 @@ query(formData).then((response) => {
const getConfigCodeWithFormDataWithAuth = (codeLang, configData) => {
if (codeLang === 'Python') {
configData = unshiftFiles(configData)
const fileType = configData[0].type
let fileType = configData[0].type
if (fileType.includes(',')) fileType = fileType.split(',')[0]
return `import requests
API_URL = "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}"
@@ -700,7 +643,11 @@ formData.append("openAIApiKey[openAIEmbeddings_0]", "sk-my-openai-2nd-key")`
</AccordionSummary>
<AccordionDetails>
<TableViewOnly
rows={nodeConfig[nodeLabel]}
rows={nodeConfig[nodeLabel].map((obj) => {
// eslint-disable-next-line
const { node, nodeId, ...rest } = obj
return rest
})}
columns={Object.keys(nodeConfig[nodeLabel][0]).slice(-3)}
/>
</AccordionDetails>
@@ -14,25 +14,25 @@ import { useTheme } from '@mui/material/styles'
import { IconSend, IconDownload } from '@tabler/icons'
// project import
import { CodeBlock } from '@/ui-component/markdown/CodeBlock'
import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown'
import SourceDocDialog from '@/ui-component/dialog/SourceDocDialog'
import { CodeBlock } from 'ui-component/markdown/CodeBlock'
import { MemoizedReactMarkdown } from 'ui-component/markdown/MemoizedReactMarkdown'
import SourceDocDialog from 'ui-component/dialog/SourceDocDialog'
import './ChatMessage.css'
// api
import chatmessageApi from '@/api/chatmessage'
import chatflowsApi from '@/api/chatflows'
import predictionApi from '@/api/prediction'
import chatmessageApi from 'api/chatmessage'
import chatflowsApi from 'api/chatflows'
import predictionApi from 'api/prediction'
// Hooks
import useApi from '@/hooks/useApi'
import useApi from 'hooks/useApi'
// Const
import { baseURL, maxScroll } from '@/store/constant'
import { baseURL, maxScroll } from 'store/constant'
import robotPNG from '@/assets/images/robot.png'
import userPNG from '@/assets/images/account.png'
import { isValidURL, removeDuplicateURL } from '@/utils/genericHelper'
import robotPNG from 'assets/images/robot.png'
import userPNG from 'assets/images/account.png'
import { isValidURL, removeDuplicateURL, setLocalStorageChatflow } from 'utils/genericHelper'
export const ChatMessage = ({ open, chatflowid, isDialog }) => {
const theme = useTheme()
@@ -128,10 +128,9 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (response.data) {
const data = response.data
if (!chatId) {
setChatId(data.chatId)
localStorage.setItem(`${chatflowid}_INTERNAL`, data.chatId)
}
if (!chatId) setChatId(data.chatId)
if (!isChatFlowAvailableToStream) {
let text = ''
if (data.text) text = data.text
@@ -149,7 +148,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
}
])
}
setLocalStorageChatflow(chatflowid, data.chatId, messages)
setLoading(false)
setUserInput('')
setTimeout(() => {
@@ -202,7 +201,6 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
if (getChatmessageApi.data?.length) {
const chatId = getChatmessageApi.data[0]?.chatId
setChatId(chatId)
localStorage.setItem(`${chatflowid}_INTERNAL`, chatId)
const loadedMessages = getChatmessageApi.data.map((message) => {
const obj = {
message: message.content,
@@ -214,6 +212,7 @@ export const ChatMessage = ({ open, chatflowid, isDialog }) => {
return obj
})
setMessages((prevMessages) => [...prevMessages, ...loadedMessages])
setLocalStorageChatflow(chatflowid, chatId, messages)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -85,8 +85,10 @@ export const ChatPopUp = ({ chatflowid }) => {
if (isConfirmed) {
try {
const chatId = localStorage.getItem(`${chatflowid}_INTERNAL`)
await chatmessageApi.deleteChatmessage(chatflowid, { chatId, chatType: 'INTERNAL' })
const chatDetails = localStorage.getItem(`${chatflowid}_INTERNAL`)
if (!chatDetails) return
const objChatDetails = JSON.parse(chatDetails)
await chatmessageApi.deleteChatmessage(chatflowid, { chatId: objChatDetails.chatId, chatType: 'INTERNAL' })
localStorage.removeItem(`${chatflowid}_INTERNAL`)
resetChatDialog()
enqueueSnackbar({
@@ -0,0 +1,556 @@
import { createPortal } from 'react-dom'
import PropTypes from 'prop-types'
import { useDispatch } from 'react-redux'
import { useContext, useState, useEffect } from 'react'
import PerfectScrollbar from 'react-perfect-scrollbar'
import { CopyBlock, atomOneDark } from 'react-code-blocks'
import {
Dialog,
DialogContent,
DialogTitle,
Button,
Box,
Tabs,
Tab,
Accordion,
AccordionSummary,
AccordionDetails,
Typography
} from '@mui/material'
import { CheckboxInput } from 'ui-component/checkbox/Checkbox'
import { BackdropLoader } from 'ui-component/loading/BackdropLoader'
import { TableViewOnly } from 'ui-component/table/Table'
import { IconX } from '@tabler/icons'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import pythonSVG from 'assets/images/python.svg'
import javascriptSVG from 'assets/images/javascript.svg'
import cURLSVG from 'assets/images/cURL.svg'
import useApi from 'hooks/useApi'
import configApi from 'api/config'
import vectorstoreApi from 'api/vectorstore'
// Utils
import {
getUpsertDetails,
getFileName,
unshiftFiles,
getConfigExamplesForJS,
getConfigExamplesForPython,
getConfigExamplesForCurl
} from 'utils/genericHelper'
import useNotifier from 'utils/useNotifier'
// Store
import { flowContext } from 'store/context/ReactFlowContext'
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions'
import { baseURL } from 'store/constant'
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
function TabPanel(props) {
const { children, value, index, ...other } = props
return (
<div
role='tabpanel'
hidden={value !== index}
id={`attachment-tabpanel-${index}`}
aria-labelledby={`attachment-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ p: 1 }}>{children}</Box>}
</div>
)
}
TabPanel.propTypes = {
children: PropTypes.node,
index: PropTypes.number.isRequired,
value: PropTypes.number.isRequired
}
function a11yProps(index) {
return {
id: `attachment-tab-${index}`,
'aria-controls': `attachment-tabpanel-${index}`
}
}
const VectorStoreDialog = ({ show, dialogProps, onCancel }) => {
const portalElement = document.getElementById('portal')
const { reactFlowInstance } = useContext(flowContext)
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const getConfigApi = useApi(configApi.getConfig)
const [nodes, setNodes] = useState([])
const [loading, setLoading] = useState(false)
const [isFormDataRequired, setIsFormDataRequired] = useState({})
const [nodeConfigExpanded, setNodeConfigExpanded] = useState({})
const [nodeCheckboxExpanded, setCheckboxExpanded] = useState({})
const [tabValue, setTabValue] = useState(0)
const [expandedVectorNodeId, setExpandedVectorNodeId] = useState('')
const [configData, setConfigData] = useState({})
const reformatConfigData = (configData, nodes) => {
return configData.filter((item1) => nodes.some((item2) => item1.nodeId === item2.id))
}
const getCode = (codeLang, vectorNodeId, isMultiple, configData) => {
if (codeLang === 'Python') {
return `import requests
API_URL = "${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid}"
def query(payload):
response = requests.post(API_URL, json=payload)
return response.json()
output = query({
${isMultiple ? `"stopNodeId": "${vectorNodeId}",\n ` : ``}"overrideConfig": {${getConfigExamplesForPython(
configData,
'json',
isMultiple,
vectorNodeId
)}
}
})
`
} else if (codeLang === 'JavaScript') {
return `async function query(data) {
const response = await fetch(
"${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid}",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
}
);
const result = await response.json();
return result;
}
query({
${isMultiple ? `"stopNodeId": "${vectorNodeId}",\n ` : ``}"overrideConfig": {${getConfigExamplesForJS(
configData,
'json',
isMultiple,
vectorNodeId
)}
}
}).then((response) => {
console.log(response);
});
`
} else if (codeLang === 'cURL') {
return `curl ${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid} \\
-X POST \\
${
isMultiple
? `-d '{"stopNodeId": "${vectorNodeId}", "overrideConfig": {${getConfigExamplesForCurl(
configData,
'json',
isMultiple,
vectorNodeId
)}}' \\`
: `-d '{"overrideConfig": {${getConfigExamplesForCurl(configData, 'json', isMultiple, vectorNodeId)}}' \\`
}
-H "Content-Type: application/json"`
}
return ''
}
const getCodeWithFormData = (codeLang, vectorNodeId, isMultiple, configData) => {
if (codeLang === 'Python') {
configData = unshiftFiles(configData)
let fileType = configData[0].type
if (fileType.includes(',')) fileType = fileType.split(',')[0]
return `import requests
API_URL = "${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid}"
# use form data to upload files
form_data = {
"files": ${`('example${fileType}', open('example${fileType}', 'rb'))`}
}
body_data = {${getConfigExamplesForPython(configData, 'formData', isMultiple, vectorNodeId)}}
def query(form_data, body_data):
response = requests.post(API_URL, files=form_data, data=body_data)
return response.json()
output = query(form_data, body_data)
`
} else if (codeLang === 'JavaScript') {
return `// use FormData to upload files
let formData = new FormData();
${getConfigExamplesForJS(configData, 'formData', isMultiple, vectorNodeId)}
async function query(formData) {
const response = await fetch(
"${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid}",
{
method: "POST",
body: formData
}
);
const result = await response.json();
return result;
}
query(formData).then((response) => {
console.log(response);
});
`
} else if (codeLang === 'cURL') {
return `curl ${baseURL}/api/v1/vector/upsert/${dialogProps.chatflowid} \\
-X POST \\${getConfigExamplesForCurl(configData, 'formData', isMultiple, vectorNodeId)} \\
-H "Content-Type: multipart/form-data"`
}
return ''
}
const getLang = (codeLang) => {
if (codeLang === 'Python') {
return 'python'
} else if (codeLang === 'JavaScript') {
return 'javascript'
} else if (codeLang === 'cURL') {
return 'bash'
}
return 'python'
}
const getSVG = (codeLang) => {
if (codeLang === 'Python') {
return pythonSVG
} else if (codeLang === 'JavaScript') {
return javascriptSVG
} else if (codeLang === 'Embed') {
return EmbedSVG
} else if (codeLang === 'cURL') {
return cURLSVG
} else if (codeLang === 'Share Chatbot') {
return ShareChatbotSVG
} else if (codeLang === 'Configuration') {
return settingsSVG
}
return pythonSVG
}
const handleAccordionChange = (nodeLabel) => (event, isExpanded) => {
const accordianNodes = { ...nodeConfigExpanded }
accordianNodes[nodeLabel] = isExpanded
setNodeConfigExpanded(accordianNodes)
}
const onCheckBoxChanged = (vectorNodeId) => {
const checkboxNodes = { ...nodeCheckboxExpanded }
if (Object.keys(checkboxNodes).includes(vectorNodeId)) checkboxNodes[vectorNodeId] = !checkboxNodes[vectorNodeId]
else checkboxNodes[vectorNodeId] = true
if (checkboxNodes[vectorNodeId] === true) getConfigApi.request(dialogProps.chatflowid)
setCheckboxExpanded(checkboxNodes)
setExpandedVectorNodeId(vectorNodeId)
const newIsFormDataRequired = { ...isFormDataRequired }
newIsFormDataRequired[vectorNodeId] = false
setIsFormDataRequired(newIsFormDataRequired)
const newNodes = nodes.find((node) => node.vectorNode.data.id === vectorNodeId)?.nodes ?? []
for (const node of newNodes) {
if (node.data.inputParams.find((param) => param.type === 'file')) {
newIsFormDataRequired[vectorNodeId] = true
setIsFormDataRequired(newIsFormDataRequired)
break
}
}
}
const onUpsertClicked = async (vectorStoreNode) => {
setLoading(true)
try {
await vectorstoreApi.upsertVectorStore(dialogProps.chatflowid, { stopNodeId: vectorStoreNode.data.id })
enqueueSnackbar({
message: 'Succesfully upserted vector store. You can start chatting now!',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: errorData,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
setLoading(false)
}
}
const getNodeDetail = (node) => {
const nodeDetails = []
const inputKeys = Object.keys(node.data.inputs)
for (let i = 0; i < node.data.inputParams.length; i += 1) {
if (inputKeys.includes(node.data.inputParams[i].name)) {
nodeDetails.push({
label: node.data.inputParams[i].label,
name: node.data.inputParams[i].name,
type: node.data.inputParams[i].type,
value:
node.data.inputParams[i].type === 'file'
? getFileName(node.data.inputs[node.data.inputParams[i].name])
: node.data.inputs[node.data.inputParams[i].name] ?? ''
})
}
}
return nodeDetails
}
useEffect(() => {
if (getConfigApi.data) {
const newConfigData = { ...configData }
newConfigData[expandedVectorNodeId] = reformatConfigData(
getConfigApi.data,
nodes.find((node) => node.vectorNode.data.id === expandedVectorNodeId)?.nodes ?? []
)
setConfigData(newConfigData)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getConfigApi.data])
useEffect(() => {
if (dialogProps && reactFlowInstance) {
const nodes = reactFlowInstance.getNodes()
const edges = reactFlowInstance.getEdges()
setNodes(getUpsertDetails(nodes, edges))
}
return () => {
setNodes([])
setLoading(false)
setIsFormDataRequired({})
setNodeConfigExpanded({})
setCheckboxExpanded({})
setTabValue(0)
setConfigData({})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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
open={show}
fullWidth
maxWidth='md'
onClose={onCancel}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
sx={{ overflow: 'visible' }}
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
{dialogProps.title}
</DialogTitle>
<DialogContent sx={{ display: 'flex', justifyContent: 'flex-end', flexDirection: 'column' }}>
<PerfectScrollbar
style={{
height: '100%',
maxHeight: 'calc(100vh - 220px)',
overflowX: 'hidden'
}}
>
{nodes.length > 0 &&
nodes.map((data, index) => {
return (
<div key={index}>
{data.nodes.length > 0 &&
data.nodes.map((node, index) => {
return (
<Accordion
expanded={nodeConfigExpanded[node.data.id] || false}
onChange={handleAccordionChange(node.data.id)}
key={index}
disableGutters
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls={`nodes-accordian-${node.data.name}`}
id={`nodes-accordian-header-${node.data.name}`}
>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div
style={{
width: 40,
height: 40,
marginRight: 10,
borderRadius: '50%',
backgroundColor: 'white'
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 7,
borderRadius: '50%',
objectFit: 'contain'
}}
alt={node.data.name}
src={`${baseURL}/api/v1/node-icon/${node.data.name}`}
/>
</div>
<Typography variant='h5'>{node.data.label}</Typography>
<div
style={{
display: 'flex',
flexDirection: 'row',
width: 'max-content',
borderRadius: 15,
background: 'rgb(254,252,191)',
padding: 5,
paddingLeft: 10,
paddingRight: 10,
marginLeft: 10
}}
>
<span style={{ color: 'rgb(116,66,16)', fontSize: '0.825rem' }}>
{node.data.id}
</span>
</div>
</div>
</AccordionSummary>
<AccordionDetails>
<TableViewOnly
sx={{ minWidth: 'max-content' }}
rows={getNodeDetail(node)}
columns={Object.keys(getNodeDetail(node)[0])}
/>
</AccordionDetails>
</Accordion>
)
})}
<Box sx={{ p: 2 }}>
<CheckboxInput
key={JSON.stringify(nodeCheckboxExpanded)}
label='Show API'
value={nodeCheckboxExpanded[data.vectorNode.data.id]}
onChange={() => onCheckBoxChanged(data.vectorNode.data.id)}
/>
{nodeCheckboxExpanded[data.vectorNode.data.id] && (
<div>
<Tabs value={tabValue} onChange={(event, val) => setTabValue(val)} aria-label='tabs'>
{['Python', 'JavaScript', 'cURL'].map((codeLang, index) => (
<Tab
icon={
<img
style={{ objectFit: 'cover', height: 15, width: 'auto' }}
src={getSVG(codeLang)}
alt='code'
/>
}
iconPosition='start'
key={index}
label={codeLang}
{...a11yProps(index)}
></Tab>
))}
</Tabs>
</div>
)}
{nodeCheckboxExpanded[data.vectorNode.data.id] &&
isFormDataRequired[data.vectorNode.data.id] !== undefined &&
configData[data.vectorNode.data.id] &&
configData[data.vectorNode.data.id].length > 0 && (
<>
<div style={{ marginTop: 10 }}>
{['Python', 'JavaScript', 'cURL'].map((codeLang, index) => (
<TabPanel key={index} value={tabValue} index={index}>
<CopyBlock
theme={atomOneDark}
text={
isFormDataRequired[data.vectorNode.data.id]
? getCodeWithFormData(
codeLang,
data.vectorNode.data.id,
nodes.length > 1 ? true : false,
configData[data.vectorNode.data.id]
)
: getCode(
codeLang,
data.vectorNode.data.id,
nodes.length > 1 ? true : false,
configData[data.vectorNode.data.id]
)
}
language={getLang(codeLang)}
showLineNumbers={false}
wrapLines
/>
</TabPanel>
))}
</div>
</>
)}
</Box>
<div style={{ marginBottom: '20px' }}>
{loading && <BackdropLoader open={loading} />}
{!loading && (
<Button
sx={{ color: 'white' }}
fullWidth
variant='contained'
color='teal'
title='Upsert'
onClick={() => onUpsertClicked(data.vectorNode)}
>
Upsert
</Button>
)}
</div>
</div>
)
})}
</PerfectScrollbar>
</DialogContent>
</Dialog>
) : null
return createPortal(component, portalElement)
}
VectorStoreDialog.propTypes = {
show: PropTypes.bool,
dialogProps: PropTypes.object,
onCancel: PropTypes.func
}
export default VectorStoreDialog
@@ -0,0 +1,114 @@
import { useState, useRef, useEffect } from 'react'
import { useDispatch } from 'react-redux'
import PropTypes from 'prop-types'
import { Button } from '@mui/material'
import { IconDatabaseImport, IconX } from '@tabler/icons'
// project import
import { StyledFab } from 'ui-component/button/StyledFab'
import VectorStoreDialog from './VectorStoreDialog'
// api
import vectorstoreApi from 'api/vectorstore'
// Hooks
import useNotifier from 'utils/useNotifier'
// Const
import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackbarAction } from 'store/actions'
export const VectorStorePopUp = ({ chatflowid }) => {
const dispatch = useDispatch()
useNotifier()
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
const [open, setOpen] = useState(false)
const [showExpandDialog, setShowExpandDialog] = useState(false)
const [expandDialogProps, setExpandDialogProps] = useState({})
const anchorRef = useRef(null)
const prevOpen = useRef(open)
const handleToggle = () => {
setOpen((prevopen) => !prevopen)
const props = {
open: true,
title: 'Upsert Vector Store',
chatflowid
}
setExpandDialogProps(props)
setShowExpandDialog(true)
}
const onUpsert = async () => {
try {
await vectorstoreApi.upsertVectorStore(chatflowid, {})
enqueueSnackbar({
message: 'Succesfully upserted vector store',
options: {
key: new Date().getTime() + Math.random(),
variant: 'success',
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
} catch (error) {
const errorData = error.response.data || `${error.response.status}: ${error.response.statusText}`
enqueueSnackbar({
message: errorData,
options: {
key: new Date().getTime() + Math.random(),
variant: 'error',
persist: true,
action: (key) => (
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
<IconX />
</Button>
)
}
})
}
}
useEffect(() => {
if (prevOpen.current === true && open === false) {
anchorRef.current.focus()
}
prevOpen.current = open
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, chatflowid])
return (
<>
<StyledFab
sx={{ position: 'absolute', right: 80, top: 20 }}
ref={anchorRef}
size='small'
color='teal'
aria-label='upsert'
title='Upsert Vector Database'
onClick={handleToggle}
>
{open ? <IconX /> : <IconDatabaseImport />}
</StyledFab>
<VectorStoreDialog
show={showExpandDialog}
dialogProps={expandDialogProps}
onUpsert={onUpsert}
onCancel={() => {
setShowExpandDialog(false)
setOpen((prevopen) => !prevopen)
}}
></VectorStoreDialog>
</>
)
}
VectorStorePopUp.propTypes = { chatflowid: PropTypes.string }