mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
Add marketplaces
This commit is contained in:
@@ -14,7 +14,7 @@ import { isValidConnection } from 'utils/genericHelper'
|
||||
|
||||
// ===========================|| NodeInputHandler ||=========================== //
|
||||
|
||||
const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
|
||||
const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false }) => {
|
||||
const theme = useTheme()
|
||||
const ref = useRef(null)
|
||||
const updateNodeInternals = useUpdateNodeInternals()
|
||||
@@ -76,6 +76,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
|
||||
</Typography>
|
||||
{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'}
|
||||
@@ -83,6 +84,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
|
||||
)}
|
||||
{(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && (
|
||||
<Input
|
||||
disabled={disabled}
|
||||
inputParam={inputParam}
|
||||
onChange={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
value={data.inputs[inputParam.name] ?? inputParam.default ?? ''}
|
||||
@@ -90,6 +92,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
|
||||
)}
|
||||
{inputParam.type === 'options' && (
|
||||
<Dropdown
|
||||
disabled={disabled}
|
||||
name={inputParam.name}
|
||||
options={inputParam.options}
|
||||
onSelect={(newValue) => (data.inputs[inputParam.name] = newValue)}
|
||||
@@ -106,7 +109,8 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data }) => {
|
||||
NodeInputHandler.propTypes = {
|
||||
inputAnchor: PropTypes.object,
|
||||
inputParam: PropTypes.object,
|
||||
data: PropTypes.object
|
||||
data: PropTypes.object,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
export default NodeInputHandler
|
||||
|
||||
@@ -3,7 +3,7 @@ import ReactFlow, { addEdge, Controls, Background, useNodesState, useEdgesState
|
||||
import 'reactflow/dist/style.css'
|
||||
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { usePrompt } from '../../utils/usePrompt'
|
||||
import {
|
||||
REMOVE_DIRTY,
|
||||
@@ -50,6 +50,9 @@ const Canvas = () => {
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { state } = useLocation()
|
||||
const templateFlowData = state ? state.templateFlowData : ''
|
||||
|
||||
const URLpath = document.location.pathname.toString().split('/')
|
||||
const chatflowId = URLpath[URLpath.length - 1] === 'canvas' ? '' : URLpath[URLpath.length - 1]
|
||||
|
||||
@@ -59,6 +62,7 @@ const Canvas = () => {
|
||||
const canvas = useSelector((state) => state.canvas)
|
||||
const [canvasDataStore, setCanvasDataStore] = useState(canvas)
|
||||
const [chatflow, setChatflow] = useState(null)
|
||||
|
||||
const { reactFlowInstance, setReactFlowInstance } = useContext(flowContext)
|
||||
|
||||
// ==============================|| Snackbar ||============================== //
|
||||
@@ -437,6 +441,14 @@ const Canvas = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (templateFlowData && templateFlowData.includes('"nodes":[') && templateFlowData.includes('],"edges":[')) {
|
||||
handleLoadFlow(templateFlowData)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [templateFlowData])
|
||||
|
||||
usePrompt('You have unsaved changes! Do you want to navigate away?', canvasDataStore.isDirty)
|
||||
|
||||
return (
|
||||
|
||||
@@ -22,6 +22,9 @@ import useApi from 'hooks/useApi'
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
// icons
|
||||
import { IconPlus } from '@tabler/icons'
|
||||
|
||||
// ==============================|| CHATFLOWS ||============================== //
|
||||
|
||||
const Chatflows = () => {
|
||||
@@ -83,7 +86,7 @@ const Chatflows = () => {
|
||||
<Grid sx={{ mb: 1.25 }} container direction='row'>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<Grid item>
|
||||
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew}>
|
||||
<StyledButton variant='contained' sx={{ color: 'white' }} onClick={addNew} startIcon={<IconPlus />}>
|
||||
Add New
|
||||
</StyledButton>
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import ReactFlow, { Controls, Background, useNodesState, useEdgesState } from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import 'views/canvas/index.css'
|
||||
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { Toolbar, Box, AppBar } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import MarketplaceCanvasNode from './MarketplaceCanvasNode'
|
||||
|
||||
import MarketplaceCanvasHeader from './MarketplaceCanvasHeader'
|
||||
|
||||
const nodeTypes = { customNode: MarketplaceCanvasNode }
|
||||
const edgeTypes = { buttonedge: '' }
|
||||
|
||||
// ==============================|| CANVAS ||============================== //
|
||||
|
||||
const MarketplaceCanvas = () => {
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const { state } = useLocation()
|
||||
const { flowData, name } = state
|
||||
|
||||
// ==============================|| ReactFlow ||============================== //
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState()
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState()
|
||||
|
||||
const reactFlowWrapper = useRef(null)
|
||||
|
||||
// ==============================|| useEffect ||============================== //
|
||||
|
||||
useEffect(() => {
|
||||
if (flowData) {
|
||||
const initialFlow = JSON.parse(flowData)
|
||||
setNodes(initialFlow.nodes || [])
|
||||
setEdges(initialFlow.edges || [])
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [flowData])
|
||||
|
||||
const onChatflowCopy = (flowData) => {
|
||||
//navigator.clipboard.writeText(JSON.stringify(flowData))
|
||||
const templateFlowData = JSON.stringify(flowData)
|
||||
navigate(`/canvas`, { state: { templateFlowData } })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<AppBar
|
||||
enableColorOnDark
|
||||
position='fixed'
|
||||
color='inherit'
|
||||
elevation={1}
|
||||
sx={{
|
||||
bgcolor: theme.palette.background.default
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<MarketplaceCanvasHeader
|
||||
flowName={name}
|
||||
flowData={JSON.parse(flowData)}
|
||||
onChatflowCopy={(flowData) => onChatflowCopy(flowData)}
|
||||
/>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Box sx={{ pt: '70px', height: '100vh', width: '100%' }}>
|
||||
<div className='reactflow-parent-wrapper'>
|
||||
<div className='reactflow-wrapper' ref={reactFlowWrapper}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
nodesDraggable={false}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
fitView
|
||||
>
|
||||
<Controls
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)'
|
||||
}}
|
||||
/>
|
||||
<Background color='#aaa' gap={16} />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MarketplaceCanvas
|
||||
@@ -0,0 +1,76 @@
|
||||
import PropTypes from 'prop-types'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Avatar, Box, ButtonBase, Typography, Stack } from '@mui/material'
|
||||
import { StyledButton } from 'ui-component/button/StyledButton'
|
||||
|
||||
// icons
|
||||
import { IconCopy, IconChevronLeft } from '@tabler/icons'
|
||||
|
||||
// ==============================|| CANVAS HEADER ||============================== //
|
||||
|
||||
const MarketplaceCanvasHeader = ({ flowName, flowData, onChatflowCopy }) => {
|
||||
const theme = useTheme()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box>
|
||||
<ButtonBase title='Back' sx={{ borderRadius: '50%' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
color='inherit'
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
<IconChevronLeft stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Stack flexDirection='row'>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1.5rem',
|
||||
fontWeight: 600,
|
||||
ml: 2
|
||||
}}
|
||||
>
|
||||
{flowName}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box>
|
||||
<StyledButton
|
||||
color='secondary'
|
||||
variant='contained'
|
||||
title='Use Chatflow'
|
||||
onClick={() => onChatflowCopy(flowData)}
|
||||
startIcon={<IconCopy />}
|
||||
>
|
||||
Use Template
|
||||
</StyledButton>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
MarketplaceCanvasHeader.propTypes = {
|
||||
flowName: PropTypes.string,
|
||||
flowData: PropTypes.object,
|
||||
onChatflowCopy: PropTypes.func
|
||||
}
|
||||
|
||||
export default MarketplaceCanvasHeader
|
||||
@@ -0,0 +1,123 @@
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme } from '@mui/material/styles'
|
||||
import { Box, Typography, Divider } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import NodeInputHandler from 'views/canvas/NodeInputHandler'
|
||||
import NodeOutputHandler from 'views/canvas/NodeOutputHandler'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
const CardWrapper = styled(MainCard)(({ theme }) => ({
|
||||
background: theme.palette.card.main,
|
||||
color: theme.darkTextPrimary,
|
||||
border: 'solid 1px',
|
||||
borderColor: theme.palette.primary[200] + 75,
|
||||
width: '300px',
|
||||
height: 'auto',
|
||||
padding: '10px',
|
||||
boxShadow: '0 2px 14px 0 rgb(32 40 45 / 8%)',
|
||||
'&:hover': {
|
||||
borderColor: theme.palette.primary.main
|
||||
}
|
||||
}))
|
||||
|
||||
// ===========================|| CANVAS NODE ||=========================== //
|
||||
|
||||
const MarketplaceCanvasNode = ({ data }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
return (
|
||||
<>
|
||||
<CardWrapper
|
||||
content={false}
|
||||
sx={{
|
||||
padding: 0,
|
||||
borderColor: data.selected ? theme.palette.primary.main : theme.palette.text.secondary
|
||||
}}
|
||||
border={false}
|
||||
>
|
||||
<Box>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Box style={{ width: 50, marginRight: 10, padding: 5 }}>
|
||||
<div
|
||||
style={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.largeAvatar,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white',
|
||||
cursor: 'grab'
|
||||
}}
|
||||
>
|
||||
<img
|
||||
style={{ width: '100%', height: '100%', padding: 5, objectFit: 'contain' }}
|
||||
src={`${baseURL}/api/v1/node-icon/${data.name}`}
|
||||
alt='Notification'
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
fontWeight: 500
|
||||
}}
|
||||
>
|
||||
{data.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
</div>
|
||||
{(data.inputAnchors.length > 0 || data.inputParams.length > 0) && (
|
||||
<>
|
||||
<Divider />
|
||||
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Inputs
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
{data.inputAnchors.map((inputAnchor, index) => (
|
||||
<NodeInputHandler key={index} inputAnchor={inputAnchor} data={data} />
|
||||
))}
|
||||
{data.inputParams.map((inputParam, index) => (
|
||||
<NodeInputHandler disabled={true} key={index} inputParam={inputParam} data={data} />
|
||||
))}
|
||||
|
||||
<Divider />
|
||||
<Box sx={{ background: theme.palette.asyncSelect.main, p: 1 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
textAlign: 'center'
|
||||
}}
|
||||
>
|
||||
Output
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
|
||||
{data.outputAnchors.map((outputAnchor, index) => (
|
||||
<NodeOutputHandler key={index} outputAnchor={outputAnchor} data={data} />
|
||||
))}
|
||||
</Box>
|
||||
</CardWrapper>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
MarketplaceCanvasNode.propTypes = {
|
||||
data: PropTypes.object
|
||||
}
|
||||
|
||||
export default MarketplaceCanvasNode
|
||||
@@ -0,0 +1,101 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { Grid, Box, Stack } from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import MainCard from 'ui-component/cards/MainCard'
|
||||
import ItemCard from 'ui-component/cards/ItemCard'
|
||||
import { gridSpacing } from 'store/constant'
|
||||
import WorkflowEmptySVG from 'assets/images/workflow_empty.svg'
|
||||
|
||||
// API
|
||||
import marketplacesApi from 'api/marketplaces'
|
||||
|
||||
// Hooks
|
||||
import useApi from 'hooks/useApi'
|
||||
|
||||
// const
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
// ==============================|| Marketplace ||============================== //
|
||||
|
||||
const Marketplace = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [isLoading, setLoading] = useState(true)
|
||||
const [images, setImages] = useState({})
|
||||
|
||||
const getAllMarketplacesApi = useApi(marketplacesApi.getAllMarketplaces)
|
||||
|
||||
const goToCanvas = (selectedChatflow) => {
|
||||
navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAllMarketplacesApi.request()
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(getAllMarketplacesApi.loading)
|
||||
}, [getAllMarketplacesApi.loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllMarketplacesApi.data) {
|
||||
try {
|
||||
const chatflows = getAllMarketplacesApi.data
|
||||
const images = {}
|
||||
for (let i = 0; i < chatflows.length; i += 1) {
|
||||
const flowDataStr = chatflows[i].flowData
|
||||
const flowData = JSON.parse(flowDataStr)
|
||||
const nodes = flowData.nodes || []
|
||||
images[chatflows[i].id] = []
|
||||
for (let j = 0; j < nodes.length; j += 1) {
|
||||
const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}`
|
||||
if (!images[chatflows[i].id].includes(imageSrc)) {
|
||||
images[chatflows[i].id].push(imageSrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
setImages(images)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}, [getAllMarketplacesApi.data])
|
||||
|
||||
return (
|
||||
<MainCard sx={{ background: customization.isDarkMode ? theme.palette.common.black : '' }}>
|
||||
<Stack flexDirection='row'>
|
||||
<h1>Marketplace</h1>
|
||||
</Stack>
|
||||
<Grid container spacing={gridSpacing}>
|
||||
{!isLoading &&
|
||||
getAllMarketplacesApi.data &&
|
||||
getAllMarketplacesApi.data.map((data, index) => (
|
||||
<Grid key={index} item lg={3} md={4} sm={6} xs={12}>
|
||||
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
{!isLoading && (!getAllMarketplacesApi.data || getAllMarketplacesApi.data.length === 0) && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||
<Box sx={{ p: 2, height: 'auto' }}>
|
||||
<img style={{ objectFit: 'cover', height: '30vh', width: 'auto' }} src={WorkflowEmptySVG} alt='WorkflowEmptySVG' />
|
||||
</Box>
|
||||
<div>No Marketplace Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default Marketplace
|
||||
Reference in New Issue
Block a user