Add marketplaces

This commit is contained in:
Henry
2023-04-10 17:47:25 +01:00
parent 58e06718d1
commit c576ea5b67
24 changed files with 2665 additions and 15 deletions
@@ -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
+13 -1
View File
@@ -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 (
+4 -1
View File
@@ -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
+101
View File
@@ -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