mirror of
https://github.com/farcasclaudiu/Flowise.git
synced 2026-06-28 15:00:57 +03:00
Feature: Custom Templates (#3169)
* New Feature: Custom Templates in the marketplace. * New Feature: Custom Templates in the marketplace. * Custom Template Delete and Shortcut in the dropdown menu * auto detect framework * minor ui fixes * adding custom template feature for tools * ui tool dialog save template --------- Co-authored-by: Henry <hzj94@hotmail.com>
This commit is contained in:
@@ -4,8 +4,16 @@ const getAllChatflowsMarketplaces = () => client.get('/marketplaces/chatflows')
|
||||
const getAllToolsMarketplaces = () => client.get('/marketplaces/tools')
|
||||
const getAllTemplatesFromMarketplaces = () => client.get('/marketplaces/templates')
|
||||
|
||||
const getAllCustomTemplates = () => client.get('/marketplaces/custom')
|
||||
const saveAsCustomTemplate = (body) => client.post('/marketplaces/custom', body)
|
||||
const deleteCustomTemplate = (id) => client.delete(`/marketplaces/custom/${id}`)
|
||||
|
||||
export default {
|
||||
getAllChatflowsMarketplaces,
|
||||
getAllToolsMarketplaces,
|
||||
getAllTemplatesFromMarketplaces
|
||||
getAllTemplatesFromMarketplaces,
|
||||
|
||||
getAllCustomTemplates,
|
||||
saveAsCustomTemplate,
|
||||
deleteCustomTemplate
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
IconMessage,
|
||||
IconDatabaseExport,
|
||||
IconAdjustmentsHorizontal,
|
||||
IconUsers
|
||||
IconUsers,
|
||||
IconTemplate
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
// constant
|
||||
@@ -19,7 +20,8 @@ const icons = {
|
||||
IconMessage,
|
||||
IconDatabaseExport,
|
||||
IconAdjustmentsHorizontal,
|
||||
IconUsers
|
||||
IconUsers,
|
||||
IconTemplate
|
||||
}
|
||||
|
||||
// ==============================|| SETTINGS MENU ITEMS ||============================== //
|
||||
@@ -50,6 +52,13 @@ const agent_settings = {
|
||||
url: '',
|
||||
icon: icons.IconAdjustmentsHorizontal
|
||||
},
|
||||
{
|
||||
id: 'saveAsTemplate',
|
||||
title: 'Save As Template',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconTemplate
|
||||
},
|
||||
{
|
||||
id: 'duplicateChatflow',
|
||||
title: 'Duplicate Agents',
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
IconMessage,
|
||||
IconDatabaseExport,
|
||||
IconAdjustmentsHorizontal,
|
||||
IconUsers
|
||||
IconUsers,
|
||||
IconTemplate
|
||||
} from '@tabler/icons-react'
|
||||
|
||||
// constant
|
||||
@@ -19,7 +20,8 @@ const icons = {
|
||||
IconMessage,
|
||||
IconDatabaseExport,
|
||||
IconAdjustmentsHorizontal,
|
||||
IconUsers
|
||||
IconUsers,
|
||||
IconTemplate
|
||||
}
|
||||
|
||||
// ==============================|| SETTINGS MENU ITEMS ||============================== //
|
||||
@@ -57,6 +59,13 @@ const settings = {
|
||||
url: '',
|
||||
icon: icons.IconAdjustmentsHorizontal
|
||||
},
|
||||
{
|
||||
id: 'saveAsTemplate',
|
||||
title: 'Save As Template',
|
||||
type: 'item',
|
||||
url: '',
|
||||
icon: icons.IconTemplate
|
||||
},
|
||||
{
|
||||
id: 'duplicateChatflow',
|
||||
title: 'Duplicate Chatflow',
|
||||
|
||||
@@ -15,6 +15,7 @@ import PictureInPictureAltIcon from '@mui/icons-material/PictureInPictureAlt'
|
||||
import ThumbsUpDownOutlinedIcon from '@mui/icons-material/ThumbsUpDownOutlined'
|
||||
import VpnLockOutlinedIcon from '@mui/icons-material/VpnLockOutlined'
|
||||
import MicNoneOutlinedIcon from '@mui/icons-material/MicNoneOutlined'
|
||||
import ExportTemplateOutlinedIcon from '@mui/icons-material/BookmarksOutlined'
|
||||
import Button from '@mui/material/Button'
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
@@ -35,6 +36,7 @@ import useNotifier from '@/utils/useNotifier'
|
||||
import ChatFeedbackDialog from '../dialog/ChatFeedbackDialog'
|
||||
import AllowedDomainsDialog from '../dialog/AllowedDomainsDialog'
|
||||
import SpeechToTextDialog from '../dialog/SpeechToTextDialog'
|
||||
import ExportAsTemplateDialog from '@/ui-component/dialog/ExportAsTemplateDialog'
|
||||
|
||||
const StyledMenu = styled((props) => (
|
||||
<Menu
|
||||
@@ -95,6 +97,9 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
const [speechToTextDialogOpen, setSpeechToTextDialogOpen] = useState(false)
|
||||
const [speechToTextDialogProps, setSpeechToTextDialogProps] = useState({})
|
||||
|
||||
const [exportTemplateDialogOpen, setExportTemplateDialogOpen] = useState(false)
|
||||
const [exportTemplateDialogProps, setExportTemplateDialogProps] = useState({})
|
||||
|
||||
const title = isAgentCanvas ? 'Agents' : 'Chatflow'
|
||||
|
||||
const handleClick = (event) => {
|
||||
@@ -119,6 +124,14 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
setConversationStartersDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleExportTemplate = () => {
|
||||
setAnchorEl(null)
|
||||
setExportTemplateDialogProps({
|
||||
chatflow: chatflow
|
||||
})
|
||||
setExportTemplateDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleFlowChatFeedback = () => {
|
||||
setAnchorEl(null)
|
||||
setChatFeedbackDialogProps({
|
||||
@@ -306,6 +319,10 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
<FileDownloadIcon />
|
||||
Export
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleExportTemplate} disableRipple>
|
||||
<ExportTemplateOutlinedIcon />
|
||||
Save As Template
|
||||
</MenuItem>
|
||||
<Divider sx={{ my: 0.5 }} />
|
||||
<MenuItem onClick={handleFlowStarterPrompts} disableRipple>
|
||||
<PictureInPictureAltIcon />
|
||||
@@ -369,6 +386,13 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update
|
||||
dialogProps={speechToTextDialogProps}
|
||||
onCancel={() => setSpeechToTextDialogOpen(false)}
|
||||
/>
|
||||
{exportTemplateDialogOpen && (
|
||||
<ExportAsTemplateDialog
|
||||
show={exportTemplateDialogOpen}
|
||||
dialogProps={exportTemplateDialogProps}
|
||||
onCancel={() => setExportTemplateDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useDispatch } from 'react-redux'
|
||||
import { useEffect, useState } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, OutlinedInput, Typography } from '@mui/material'
|
||||
|
||||
// store
|
||||
import {
|
||||
closeSnackbar as closeSnackbarAction,
|
||||
enqueueSnackbar as enqueueSnackbarAction,
|
||||
HIDE_CANVAS_DIALOG,
|
||||
SHOW_CANVAS_DIALOG
|
||||
} from '@/store/actions'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { StyledButton } from '@/ui-component/button/StyledButton'
|
||||
import Chip from '@mui/material/Chip'
|
||||
import { IconX } from '@tabler/icons-react'
|
||||
|
||||
// API
|
||||
import marketplacesApi from '@/api/marketplaces'
|
||||
import useApi from '@/hooks/useApi'
|
||||
|
||||
// Project imports
|
||||
|
||||
const ExportAsTemplateDialog = ({ show, dialogProps, onCancel }) => {
|
||||
const portalElement = document.getElementById('portal')
|
||||
const dispatch = useDispatch()
|
||||
const [name, setName] = useState('')
|
||||
const [flowType, setFlowType] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [badge, setBadge] = useState('')
|
||||
const [usecases, setUsecases] = useState([])
|
||||
const [usecaseInput, setUsecaseInput] = useState('')
|
||||
|
||||
const saveCustomTemplateApi = useApi(marketplacesApi.saveAsCustomTemplate)
|
||||
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
useNotifier()
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogProps.chatflow) {
|
||||
setName(dialogProps.chatflow.name)
|
||||
setFlowType(dialogProps.chatflow.type === 'MULTIAGENT' ? 'Agentflow' : 'Chatflow')
|
||||
}
|
||||
|
||||
if (dialogProps.tool) {
|
||||
setName(dialogProps.tool.name)
|
||||
setDescription(dialogProps.tool.description)
|
||||
setFlowType('Tool')
|
||||
}
|
||||
|
||||
return () => {
|
||||
setName('')
|
||||
setDescription('')
|
||||
setBadge('')
|
||||
setUsecases([])
|
||||
setFlowType('')
|
||||
setUsecaseInput('')
|
||||
}
|
||||
|
||||
// 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 handleUsecaseInputChange = (event) => {
|
||||
setUsecaseInput(event.target.value)
|
||||
}
|
||||
|
||||
const handleUsecaseInputKeyDown = (event) => {
|
||||
if (event.key === 'Enter' && usecaseInput.trim()) {
|
||||
event.preventDefault()
|
||||
if (!usecases.includes(usecaseInput)) {
|
||||
setUsecases([...usecases, usecaseInput])
|
||||
setUsecaseInput('')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleUsecaseDelete = (toDelete) => {
|
||||
setUsecases(usecases.filter((category) => category !== toDelete))
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
if (name.trim() === '') {
|
||||
enqueueSnackbar({
|
||||
message: 'Template Name is mandatory!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const template = {
|
||||
name,
|
||||
description,
|
||||
badge: badge ? badge.toUpperCase() : undefined,
|
||||
usecases,
|
||||
type: flowType
|
||||
}
|
||||
if (dialogProps.chatflow) {
|
||||
template.chatflowId = dialogProps.chatflow.id
|
||||
}
|
||||
if (dialogProps.tool) {
|
||||
template.tool = {
|
||||
iconSrc: dialogProps.tool.iconSrc,
|
||||
schema: dialogProps.tool.schema,
|
||||
func: dialogProps.tool.func
|
||||
}
|
||||
}
|
||||
saveCustomTemplateApi.request(template)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (saveCustomTemplateApi.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Saved as template successfully!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [saveCustomTemplateApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (saveCustomTemplateApi.error) {
|
||||
enqueueSnackbar({
|
||||
message: 'Failed to save as template!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
onCancel()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [saveCustomTemplateApi.error])
|
||||
|
||||
const component = show ? (
|
||||
<Dialog
|
||||
onClose={onCancel}
|
||||
open={show}
|
||||
fullWidth
|
||||
maxWidth='sm'
|
||||
aria-labelledby='alert-dialog-title'
|
||||
aria-describedby='alert-dialog-description'
|
||||
>
|
||||
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
|
||||
{dialogProps.title || 'Export As Template'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>
|
||||
Name<span style={{ color: 'red' }}> *</span>
|
||||
</Typography>
|
||||
<OutlinedInput
|
||||
id={'name'}
|
||||
type={'string'}
|
||||
fullWidth
|
||||
value={name}
|
||||
name='name'
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
setName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>Description</Typography>
|
||||
<OutlinedInput
|
||||
id={'description'}
|
||||
type={'string'}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
value={description}
|
||||
name='description'
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>Badge</Typography>
|
||||
<OutlinedInput
|
||||
id={'badge'}
|
||||
type={'string'}
|
||||
fullWidth
|
||||
value={badge}
|
||||
name='badge'
|
||||
size='small'
|
||||
onChange={(e) => {
|
||||
setBadge(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
<Box sx={{ pt: 2, pb: 2 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start' }}>
|
||||
<Typography sx={{ mb: 1 }}>Usecases</Typography>
|
||||
{usecases.length > 0 && (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
{usecases.map((uc, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={uc}
|
||||
onDelete={() => handleUsecaseDelete(uc)}
|
||||
style={{ marginRight: 5, marginBottom: 5 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<OutlinedInput
|
||||
fullWidth
|
||||
value={usecaseInput}
|
||||
onChange={handleUsecaseInputChange}
|
||||
onKeyDown={handleUsecaseInputKeyDown}
|
||||
variant='outlined'
|
||||
/>
|
||||
<Typography variant='body2' sx={{ fontStyle: 'italic', mt: 1 }} color='text.secondary'>
|
||||
Type a usecase and press enter to add it to the list. You can add as many items as you want.
|
||||
</Typography>
|
||||
</div>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>{dialogProps.cancelButtonName || 'Cancel'}</Button>
|
||||
<StyledButton disabled={dialogProps.disabled} variant='contained' onClick={onConfirm}>
|
||||
{dialogProps.confirmButtonName || 'Save Template'}
|
||||
</StyledButton>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
return createPortal(component, portalElement)
|
||||
}
|
||||
|
||||
ExportAsTemplateDialog.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
dialogProps: PropTypes.object,
|
||||
onCancel: PropTypes.func,
|
||||
onConfirm: PropTypes.func
|
||||
}
|
||||
|
||||
export default ExportAsTemplateDialog
|
||||
@@ -15,8 +15,10 @@ import {
|
||||
TableRow,
|
||||
Typography,
|
||||
Stack,
|
||||
useTheme
|
||||
useTheme,
|
||||
IconButton
|
||||
} from '@mui/material'
|
||||
import { IconTrash } from '@tabler/icons-react'
|
||||
|
||||
const StyledTableCell = styled(TableCell)(({ theme }) => ({
|
||||
borderColor: theme.palette.grey[900] + 25,
|
||||
@@ -46,7 +48,8 @@ export const MarketplaceTable = ({
|
||||
filterByUsecases,
|
||||
goToCanvas,
|
||||
goToTool,
|
||||
isLoading
|
||||
isLoading,
|
||||
onDelete
|
||||
}) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
@@ -87,6 +90,11 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell component='th' scope='row' key='6'>
|
||||
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell component='th' scope='row' key='7'>
|
||||
Delete
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
@@ -114,6 +122,11 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
<StyledTableRow>
|
||||
<StyledTableCell>
|
||||
@@ -137,6 +150,11 @@ export const MarketplaceTable = ({
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell>
|
||||
<Skeleton variant='text' />
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
</>
|
||||
) : (
|
||||
@@ -234,6 +252,13 @@ export const MarketplaceTable = ({
|
||||
))}
|
||||
</Typography>
|
||||
</StyledTableCell>
|
||||
{onDelete && (
|
||||
<StyledTableCell key='7'>
|
||||
<IconButton title='Delete' color='error' onClick={() => onDelete(row)}>
|
||||
<IconTrash />
|
||||
</IconButton>
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</>
|
||||
@@ -254,5 +279,6 @@ MarketplaceTable.propTypes = {
|
||||
filterByUsecases: PropTypes.func,
|
||||
goToTool: PropTypes.func,
|
||||
goToCanvas: PropTypes.func,
|
||||
isLoading: PropTypes.bool
|
||||
isLoading: PropTypes.bool,
|
||||
onDelete: PropTypes.func
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Avatar, Box, ButtonBase, Typography, Stack, TextField } from '@mui/material'
|
||||
import { Avatar, Box, ButtonBase, Typography, Stack, TextField, Button } from '@mui/material'
|
||||
|
||||
// icons
|
||||
import { IconSettings, IconChevronLeft, IconDeviceFloppy, IconPencil, IconCheck, IconX, IconCode } from '@tabler/icons-react'
|
||||
@@ -27,8 +27,9 @@ import useApi from '@/hooks/useApi'
|
||||
// utils
|
||||
import { generateExportFlowData } from '@/utils/genericHelper'
|
||||
import { uiBaseURL } from '@/store/constant'
|
||||
import { SET_CHATFLOW } from '@/store/actions'
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction, SET_CHATFLOW } from '@/store/actions'
|
||||
import ViewLeadsDialog from '@/ui-component/dialog/ViewLeadsDialog'
|
||||
import ExportAsTemplateDialog from '@/ui-component/dialog/ExportAsTemplateDialog'
|
||||
|
||||
// ==============================|| CANVAS HEADER ||============================== //
|
||||
|
||||
@@ -54,6 +55,11 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlo
|
||||
const [chatflowConfigurationDialogOpen, setChatflowConfigurationDialogOpen] = useState(false)
|
||||
const [chatflowConfigurationDialogProps, setChatflowConfigurationDialogProps] = useState({})
|
||||
|
||||
const [exportAsTemplateDialogOpen, setExportAsTemplateDialogOpen] = useState(false)
|
||||
const [exportAsTemplateDialogProps, setExportAsTemplateDialogProps] = useState({})
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
|
||||
const title = isAgentCanvas ? 'Agents' : 'Chatflow'
|
||||
|
||||
const updateChatflowApi = useApi(chatflowsApi.updateChatflow)
|
||||
@@ -76,6 +82,28 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlo
|
||||
chatflow: chatflow
|
||||
})
|
||||
setViewLeadsDialogOpen(true)
|
||||
} else if (setting === 'saveAsTemplate') {
|
||||
if (canvas.isDirty) {
|
||||
enqueueSnackbar({
|
||||
message: 'Please save the flow before exporting as template',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
setExportAsTemplateDialogProps({
|
||||
title: 'Export As Template',
|
||||
chatflow: chatflow
|
||||
})
|
||||
setExportAsTemplateDialogOpen(true)
|
||||
} else if (setting === 'viewUpsertHistory') {
|
||||
setUpsertHistoryDialogProps({
|
||||
title: 'View Upsert History',
|
||||
@@ -419,6 +447,13 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, handleSaveFlow, handleDeleteFlo
|
||||
onCancel={() => setViewMessagesDialogOpen(false)}
|
||||
/>
|
||||
<ViewLeadsDialog show={viewLeadsDialogOpen} dialogProps={viewLeadsDialogProps} onCancel={() => setViewLeadsDialogOpen(false)} />
|
||||
{exportAsTemplateDialogOpen && (
|
||||
<ExportAsTemplateDialog
|
||||
show={exportAsTemplateDialogOpen}
|
||||
dialogProps={exportAsTemplateDialogProps}
|
||||
onCancel={() => setExportAsTemplateDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<UpsertHistoryDialog
|
||||
show={upsertHistoryDialogOpen}
|
||||
dialogProps={upsertHistoryDialogProps}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import {
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
FormControlLabel,
|
||||
ToggleButtonGroup,
|
||||
MenuItem,
|
||||
Button
|
||||
Button,
|
||||
Tabs,
|
||||
Tab
|
||||
} from '@mui/material'
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { IconLayoutGrid, IconList, IconX } from '@tabler/icons-react'
|
||||
@@ -32,37 +34,21 @@ import ToolDialog from '@/views/tools/ToolDialog'
|
||||
import { MarketplaceTable } from '@/ui-component/table/MarketplaceTable'
|
||||
import ViewHeader from '@/layout/MainLayout/ViewHeader'
|
||||
import ErrorBoundary from '@/ErrorBoundary'
|
||||
import { TabPanel } from '@/ui-component/tabs/TabPanel'
|
||||
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
|
||||
import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
|
||||
|
||||
// API
|
||||
import marketplacesApi from '@/api/marketplaces'
|
||||
|
||||
// Hooks
|
||||
import useApi from '@/hooks/useApi'
|
||||
import useConfirm from '@/hooks/useConfirm'
|
||||
|
||||
// const
|
||||
import { baseURL } from '@/store/constant'
|
||||
import { gridSpacing } from '@/store/constant'
|
||||
|
||||
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
|
||||
}
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
|
||||
const badges = ['POPULAR', 'NEW']
|
||||
const types = ['Chatflow', 'Agentflow', 'Tool']
|
||||
@@ -83,6 +69,8 @@ const SelectStyles = {
|
||||
|
||||
const Marketplace = () => {
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useDispatch()
|
||||
useNotifier()
|
||||
|
||||
const theme = useTheme()
|
||||
|
||||
@@ -104,8 +92,26 @@ const Marketplace = () => {
|
||||
const [typeFilter, setTypeFilter] = useState([])
|
||||
const [frameworkFilter, setFrameworkFilter] = useState([])
|
||||
|
||||
const getAllCustomTemplatesApi = useApi(marketplacesApi.getAllCustomTemplates)
|
||||
const [activeTabValue, setActiveTabValue] = useState(0)
|
||||
const [templateImages, setTemplateImages] = useState({})
|
||||
const [templateUsecases, setTemplateUsecases] = useState([])
|
||||
const [eligibleTemplateUsecases, setEligibleTemplateUsecases] = useState([])
|
||||
const [selectedTemplateUsecases, setSelectedTemplateUsecases] = useState([])
|
||||
const enqueueSnackbar = (...args) => dispatch(enqueueSnackbarAction(...args))
|
||||
const closeSnackbar = (...args) => dispatch(closeSnackbarAction(...args))
|
||||
const { confirm } = useConfirm()
|
||||
|
||||
const handleTabChange = (event, newValue) => {
|
||||
if (newValue === 1 && !getAllCustomTemplatesApi.data) {
|
||||
getAllCustomTemplatesApi.request()
|
||||
}
|
||||
setActiveTabValue(newValue)
|
||||
}
|
||||
|
||||
const clearAllUsecases = () => {
|
||||
setSelectedUsecases([])
|
||||
if (activeTabValue === 0) setSelectedUsecases([])
|
||||
else setSelectedTemplateUsecases([])
|
||||
}
|
||||
|
||||
const handleBadgeFilterChange = (event) => {
|
||||
@@ -116,7 +122,13 @@ const Marketplace = () => {
|
||||
// On autofill we get a stringified value.
|
||||
typeof value === 'string' ? value.split(',') : value
|
||||
)
|
||||
getEligibleUsecases({ typeFilter, badgeFilter: typeof value === 'string' ? value.split(',') : value, frameworkFilter, search })
|
||||
const data = activeTabValue === 0 ? getAllTemplatesMarketplacesApi.data : getAllCustomTemplatesApi.data
|
||||
getEligibleUsecases(data, {
|
||||
typeFilter,
|
||||
badgeFilter: typeof value === 'string' ? value.split(',') : value,
|
||||
frameworkFilter,
|
||||
search
|
||||
})
|
||||
}
|
||||
|
||||
const handleTypeFilterChange = (event) => {
|
||||
@@ -127,7 +139,13 @@ const Marketplace = () => {
|
||||
// On autofill we get a stringified value.
|
||||
typeof value === 'string' ? value.split(',') : value
|
||||
)
|
||||
getEligibleUsecases({ typeFilter: typeof value === 'string' ? value.split(',') : value, badgeFilter, frameworkFilter, search })
|
||||
const data = activeTabValue === 0 ? getAllTemplatesMarketplacesApi.data : getAllCustomTemplatesApi.data
|
||||
getEligibleUsecases(data, {
|
||||
typeFilter: typeof value === 'string' ? value.split(',') : value,
|
||||
badgeFilter,
|
||||
frameworkFilter,
|
||||
search
|
||||
})
|
||||
}
|
||||
|
||||
const handleFrameworkFilterChange = (event) => {
|
||||
@@ -138,7 +156,13 @@ const Marketplace = () => {
|
||||
// On autofill we get a stringified value.
|
||||
typeof value === 'string' ? value.split(',') : value
|
||||
)
|
||||
getEligibleUsecases({ typeFilter, badgeFilter, frameworkFilter: typeof value === 'string' ? value.split(',') : value, search })
|
||||
const data = activeTabValue === 0 ? getAllTemplatesMarketplacesApi.data : getAllCustomTemplatesApi.data
|
||||
getEligibleUsecases(data, {
|
||||
typeFilter,
|
||||
badgeFilter,
|
||||
frameworkFilter: typeof value === 'string' ? value.split(',') : value,
|
||||
search
|
||||
})
|
||||
}
|
||||
|
||||
const handleViewChange = (event, nextView) => {
|
||||
@@ -149,7 +173,56 @@ const Marketplace = () => {
|
||||
|
||||
const onSearchChange = (event) => {
|
||||
setSearch(event.target.value)
|
||||
getEligibleUsecases({ typeFilter, badgeFilter, frameworkFilter, search: event.target.value })
|
||||
const data = activeTabValue === 0 ? getAllTemplatesMarketplacesApi.data : getAllCustomTemplatesApi.data
|
||||
|
||||
getEligibleUsecases(data, { typeFilter, badgeFilter, frameworkFilter, search: event.target.value })
|
||||
}
|
||||
|
||||
const onDeleteCustomTemplate = async (template) => {
|
||||
const confirmPayload = {
|
||||
title: `Delete`,
|
||||
description: `Delete Custom Template ${template.name}?`,
|
||||
confirmButtonName: 'Delete',
|
||||
cancelButtonName: 'Cancel'
|
||||
}
|
||||
const isConfirmed = await confirm(confirmPayload)
|
||||
|
||||
if (isConfirmed) {
|
||||
try {
|
||||
const deleteResp = await marketplacesApi.deleteCustomTemplate(template.id)
|
||||
if (deleteResp.data) {
|
||||
enqueueSnackbar({
|
||||
message: 'Custom Template deleted successfully!',
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'success',
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
getAllCustomTemplatesApi.request()
|
||||
}
|
||||
} catch (error) {
|
||||
enqueueSnackbar({
|
||||
message: `Failed to delete custom template: ${
|
||||
typeof error.response.data === 'object' ? error.response.data.message : error.response.data
|
||||
}`,
|
||||
options: {
|
||||
key: new Date().getTime() + Math.random(),
|
||||
variant: 'error',
|
||||
persist: true,
|
||||
action: (key) => (
|
||||
<Button style={{ color: 'white' }} onClick={() => closeSnackbar(key)}>
|
||||
<IconX />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterFlows(data) {
|
||||
@@ -173,13 +246,18 @@ const Marketplace = () => {
|
||||
}
|
||||
|
||||
function filterByUsecases(data) {
|
||||
return selectedUsecases.length > 0 ? (data.usecases || []).some((item) => selectedUsecases.includes(item)) : true
|
||||
if (activeTabValue === 0)
|
||||
return selectedUsecases.length > 0 ? (data.usecases || []).some((item) => selectedUsecases.includes(item)) : true
|
||||
else
|
||||
return selectedTemplateUsecases.length > 0
|
||||
? (data.usecases || []).some((item) => selectedTemplateUsecases.includes(item))
|
||||
: true
|
||||
}
|
||||
|
||||
const getEligibleUsecases = (filter) => {
|
||||
if (!getAllTemplatesMarketplacesApi.data) return
|
||||
const getEligibleUsecases = (data, filter) => {
|
||||
if (!data) return
|
||||
|
||||
let filteredData = getAllTemplatesMarketplacesApi.data
|
||||
let filteredData = data
|
||||
if (filter.badgeFilter.length > 0) filteredData = filteredData.filter((data) => filter.badgeFilter.includes(data.badge))
|
||||
if (filter.typeFilter.length > 0) filteredData = filteredData.filter((data) => filter.typeFilter.includes(data.type))
|
||||
if (filter.frameworkFilter.length > 0)
|
||||
@@ -199,7 +277,8 @@ const Marketplace = () => {
|
||||
usecases.push(...filteredData[i].usecases)
|
||||
}
|
||||
}
|
||||
setEligibleUsecases(Array.from(new Set(usecases)).sort())
|
||||
if (activeTabValue === 0) setEligibleUsecases(Array.from(new Set(usecases)).sort())
|
||||
else setEligibleTemplateUsecases(Array.from(new Set(usecases)).sort())
|
||||
}
|
||||
|
||||
const onUseTemplate = (selectedTool) => {
|
||||
@@ -274,13 +353,57 @@ const Marketplace = () => {
|
||||
}
|
||||
}, [getAllTemplatesMarketplacesApi.error])
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(getAllCustomTemplatesApi.loading)
|
||||
}, [getAllCustomTemplatesApi.loading])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllCustomTemplatesApi.data) {
|
||||
try {
|
||||
const flows = getAllCustomTemplatesApi.data
|
||||
const usecases = []
|
||||
const tImages = {}
|
||||
for (let i = 0; i < flows.length; i += 1) {
|
||||
if (flows[i].flowData) {
|
||||
const flowDataStr = flows[i].flowData
|
||||
const flowData = JSON.parse(flowDataStr)
|
||||
usecases.push(...flows[i].usecases)
|
||||
if (flows[i].framework) {
|
||||
flows[i].framework = [flows[i].framework] || []
|
||||
}
|
||||
const nodes = flowData.nodes || []
|
||||
tImages[flows[i].id] = []
|
||||
for (let j = 0; j < nodes.length; j += 1) {
|
||||
const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}`
|
||||
if (!tImages[flows[i].id].includes(imageSrc)) {
|
||||
tImages[flows[i].id].push(imageSrc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setTemplateImages(tImages)
|
||||
setTemplateUsecases(Array.from(new Set(usecases)).sort())
|
||||
setEligibleTemplateUsecases(Array.from(new Set(usecases)).sort())
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [getAllCustomTemplatesApi.data])
|
||||
|
||||
useEffect(() => {
|
||||
if (getAllCustomTemplatesApi.error) {
|
||||
setError(getAllCustomTemplatesApi.error)
|
||||
}
|
||||
}, [getAllCustomTemplatesApi.error])
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainCard>
|
||||
{error ? (
|
||||
<ErrorBoundary error={error} />
|
||||
) : (
|
||||
<Stack flexDirection='column' sx={{ gap: 3 }}>
|
||||
<Stack flexDirection='column'>
|
||||
<ViewHeader
|
||||
filters={
|
||||
<>
|
||||
@@ -432,119 +555,253 @@ const Marketplace = () => {
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</ViewHeader>
|
||||
<Stack direction='row' sx={{ gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{usecases.map((usecase, index) => (
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
size='small'
|
||||
control={
|
||||
<Checkbox
|
||||
disabled={eligibleUsecases.length === 0 ? true : !eligibleUsecases.includes(usecase)}
|
||||
color='success'
|
||||
checked={selectedUsecases.includes(usecase)}
|
||||
onChange={(event) => {
|
||||
setSelectedUsecases(
|
||||
event.target.checked
|
||||
? [...selectedUsecases, usecase]
|
||||
: selectedUsecases.filter((item) => item !== usecase)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={usecase}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
{selectedUsecases.length > 0 && (
|
||||
<Button
|
||||
sx={{ width: 'max-content', borderRadius: '20px' }}
|
||||
variant='outlined'
|
||||
onClick={() => clearAllUsecases()}
|
||||
startIcon={<IconX />}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
{!view || view === 'card' ? (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
|
||||
{getAllTemplatesMarketplacesApi.data
|
||||
?.filter(filterByBadge)
|
||||
.filter(filterByType)
|
||||
.filter(filterFlows)
|
||||
.filter(filterByFramework)
|
||||
.filter(filterByUsecases)
|
||||
.map((data, index) => (
|
||||
<Box key={index}>
|
||||
{data.badge && (
|
||||
<Badge
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'& .MuiBadge-badge': {
|
||||
right: 20
|
||||
}
|
||||
}}
|
||||
badgeContent={data.badge}
|
||||
color={data.badge === 'POPULAR' ? 'primary' : 'error'}
|
||||
>
|
||||
{(data.type === 'Chatflow' || data.type === 'Agentflow') && (
|
||||
<ItemCard
|
||||
onClick={() => goToCanvas(data)}
|
||||
data={data}
|
||||
images={images[data.id]}
|
||||
/>
|
||||
)}
|
||||
{data.type === 'Tool' && (
|
||||
<ItemCard data={data} onClick={() => goToTool(data)} />
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
{!data.badge && (data.type === 'Chatflow' || data.type === 'Agentflow') && (
|
||||
<ItemCard onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
|
||||
)}
|
||||
{!data.badge && data.type === 'Tool' && (
|
||||
<ItemCard data={data} onClick={() => goToTool(data)} />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<MarketplaceTable
|
||||
data={getAllTemplatesMarketplacesApi.data}
|
||||
filterFunction={filterFlows}
|
||||
filterByType={filterByType}
|
||||
filterByBadge={filterByBadge}
|
||||
filterByFramework={filterByFramework}
|
||||
filterByUsecases={filterByUsecases}
|
||||
goToTool={goToTool}
|
||||
goToCanvas={goToCanvas}
|
||||
isLoading={isLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||
<Box sx={{ p: 2, height: 'auto' }}>
|
||||
<img
|
||||
style={{ objectFit: 'cover', height: '16vh', width: 'auto' }}
|
||||
src={WorkflowEmptySVG}
|
||||
alt='WorkflowEmptySVG'
|
||||
<Tabs value={activeTabValue} onChange={handleTabChange} textColor='primary' aria-label='tabs' centered>
|
||||
<Tab value={0} label='Community Templates'></Tab>
|
||||
<Tab value={1} label='My Templates' />
|
||||
</Tabs>
|
||||
<TabPanel value={activeTabValue} index={0}>
|
||||
<Stack direction='row' sx={{ gap: 2, my: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{usecases.map((usecase, index) => (
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
size='small'
|
||||
control={
|
||||
<Checkbox
|
||||
disabled={eligibleUsecases.length === 0 ? true : !eligibleUsecases.includes(usecase)}
|
||||
color='success'
|
||||
checked={selectedUsecases.includes(usecase)}
|
||||
onChange={(event) => {
|
||||
setSelectedUsecases(
|
||||
event.target.checked
|
||||
? [...selectedUsecases, usecase]
|
||||
: selectedUsecases.filter((item) => item !== usecase)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={usecase}
|
||||
/>
|
||||
</Box>
|
||||
<div>No Marketplace Yet</div>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
{selectedUsecases.length > 0 && (
|
||||
<Button
|
||||
sx={{ width: 'max-content', mb: 2, borderRadius: '20px' }}
|
||||
variant='outlined'
|
||||
onClick={() => clearAllUsecases()}
|
||||
startIcon={<IconX />}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!view || view === 'card' ? (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
|
||||
{getAllTemplatesMarketplacesApi.data
|
||||
?.filter(filterByBadge)
|
||||
.filter(filterByType)
|
||||
.filter(filterFlows)
|
||||
.filter(filterByFramework)
|
||||
.filter(filterByUsecases)
|
||||
.map((data, index) => (
|
||||
<Box key={index}>
|
||||
{data.badge && (
|
||||
<Badge
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'& .MuiBadge-badge': {
|
||||
right: 20
|
||||
}
|
||||
}}
|
||||
badgeContent={data.badge}
|
||||
color={data.badge === 'POPULAR' ? 'primary' : 'error'}
|
||||
>
|
||||
{(data.type === 'Chatflow' || data.type === 'Agentflow') && (
|
||||
<ItemCard
|
||||
onClick={() => goToCanvas(data)}
|
||||
data={data}
|
||||
images={images[data.id]}
|
||||
/>
|
||||
)}
|
||||
{data.type === 'Tool' && (
|
||||
<ItemCard data={data} onClick={() => goToTool(data)} />
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
{!data.badge && (data.type === 'Chatflow' || data.type === 'Agentflow') && (
|
||||
<ItemCard
|
||||
onClick={() => goToCanvas(data)}
|
||||
data={data}
|
||||
images={images[data.id]}
|
||||
/>
|
||||
)}
|
||||
{!data.badge && data.type === 'Tool' && (
|
||||
<ItemCard data={data} onClick={() => goToTool(data)} />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<MarketplaceTable
|
||||
data={getAllTemplatesMarketplacesApi.data}
|
||||
filterFunction={filterFlows}
|
||||
filterByType={filterByType}
|
||||
filterByBadge={filterByBadge}
|
||||
filterByFramework={filterByFramework}
|
||||
filterByUsecases={filterByUsecases}
|
||||
goToTool={goToTool}
|
||||
goToCanvas={goToCanvas}
|
||||
isLoading={isLoading}
|
||||
setError={setError}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||
<Box sx={{ p: 2, height: 'auto' }}>
|
||||
<img
|
||||
style={{ objectFit: 'cover', height: '25vh', width: 'auto' }}
|
||||
src={WorkflowEmptySVG}
|
||||
alt='WorkflowEmptySVG'
|
||||
/>
|
||||
</Box>
|
||||
<div>No Marketplace Yet</div>
|
||||
</Stack>
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel value={activeTabValue} index={1}>
|
||||
<Stack direction='row' sx={{ gap: 2, my: 2, alignItems: 'center', flexWrap: 'wrap' }}>
|
||||
{templateUsecases.map((usecase, index) => (
|
||||
<FormControlLabel
|
||||
key={index}
|
||||
size='small'
|
||||
control={
|
||||
<Checkbox
|
||||
disabled={
|
||||
eligibleTemplateUsecases.length === 0
|
||||
? true
|
||||
: !eligibleTemplateUsecases.includes(usecase)
|
||||
}
|
||||
color='success'
|
||||
checked={selectedTemplateUsecases.includes(usecase)}
|
||||
onChange={(event) => {
|
||||
setSelectedTemplateUsecases(
|
||||
event.target.checked
|
||||
? [...selectedTemplateUsecases, usecase]
|
||||
: selectedTemplateUsecases.filter((item) => item !== usecase)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label={usecase}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
{selectedTemplateUsecases.length > 0 && (
|
||||
<Button
|
||||
sx={{ width: 'max-content', mb: 2, borderRadius: '20px' }}
|
||||
variant='outlined'
|
||||
onClick={() => clearAllUsecases()}
|
||||
startIcon={<IconX />}
|
||||
>
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
{!view || view === 'card' ? (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
<Skeleton variant='rounded' height={160} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
|
||||
{getAllCustomTemplatesApi.data
|
||||
?.filter(filterByBadge)
|
||||
.filter(filterByType)
|
||||
.filter(filterFlows)
|
||||
.filter(filterByFramework)
|
||||
.filter(filterByUsecases)
|
||||
.map((data, index) => (
|
||||
<Box key={index}>
|
||||
{data.badge && (
|
||||
<Badge
|
||||
sx={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
'& .MuiBadge-badge': {
|
||||
right: 20
|
||||
}
|
||||
}}
|
||||
badgeContent={data.badge}
|
||||
color={data.badge === 'POPULAR' ? 'primary' : 'error'}
|
||||
>
|
||||
{(data.type === 'Chatflow' || data.type === 'Agentflow') && (
|
||||
<ItemCard
|
||||
onClick={() => goToCanvas(data)}
|
||||
data={data}
|
||||
images={templateImages[data.id]}
|
||||
/>
|
||||
)}
|
||||
{data.type === 'Tool' && (
|
||||
<ItemCard data={data} onClick={() => goToTool(data)} />
|
||||
)}
|
||||
</Badge>
|
||||
)}
|
||||
{!data.badge && (data.type === 'Chatflow' || data.type === 'Agentflow') && (
|
||||
<ItemCard
|
||||
onClick={() => goToCanvas(data)}
|
||||
data={data}
|
||||
images={templateImages[data.id]}
|
||||
/>
|
||||
)}
|
||||
{!data.badge && data.type === 'Tool' && (
|
||||
<ItemCard data={data} onClick={() => goToTool(data)} />
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<MarketplaceTable
|
||||
data={getAllCustomTemplatesApi.data}
|
||||
filterFunction={filterFlows}
|
||||
filterByType={filterByType}
|
||||
filterByBadge={filterByBadge}
|
||||
filterByFramework={filterByFramework}
|
||||
filterByUsecases={filterByUsecases}
|
||||
goToTool={goToTool}
|
||||
goToCanvas={goToCanvas}
|
||||
isLoading={isLoading}
|
||||
setError={setError}
|
||||
onDelete={onDeleteCustomTemplate}
|
||||
/>
|
||||
)}
|
||||
{!isLoading && (!getAllCustomTemplatesApi.data || getAllCustomTemplatesApi.data.length === 0) && (
|
||||
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
|
||||
<Box sx={{ p: 2, height: 'auto' }}>
|
||||
<img
|
||||
style={{ objectFit: 'cover', height: '25vh', width: 'auto' }}
|
||||
src={WorkflowEmptySVG}
|
||||
alt='WorkflowEmptySVG'
|
||||
/>
|
||||
</Box>
|
||||
<div>No Saved Custom Templates</div>
|
||||
</Stack>
|
||||
)}
|
||||
</TabPanel>
|
||||
</Stack>
|
||||
)}
|
||||
</MainCard>
|
||||
@@ -555,6 +812,7 @@ const Marketplace = () => {
|
||||
onConfirm={() => setShowToolDialog(false)}
|
||||
onUseTemplate={(tool) => onUseTemplate(tool)}
|
||||
></ToolDialog>
|
||||
<ConfirmDialog />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { CodeEditor } from '@/ui-component/editor/CodeEditor'
|
||||
import HowToUseFunctionDialog from './HowToUseFunctionDialog'
|
||||
|
||||
// Icons
|
||||
import { IconX, IconFileDownload, IconPlus } from '@tabler/icons-react'
|
||||
import { IconX, IconFileDownload, IconPlus, IconTemplate } from '@tabler/icons-react'
|
||||
|
||||
// API
|
||||
import toolsApi from '@/api/tools'
|
||||
@@ -29,6 +29,7 @@ import useApi from '@/hooks/useApi'
|
||||
import useNotifier from '@/utils/useNotifier'
|
||||
import { generateRandomGradient, formatDataGridRows } from '@/utils/genericHelper'
|
||||
import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from '@/store/actions'
|
||||
import ExportAsTemplateDialog from '@/ui-component/dialog/ExportAsTemplateDialog'
|
||||
|
||||
const exampleAPIFunc = `/*
|
||||
* You can use any libraries imported in Flowise
|
||||
@@ -79,6 +80,9 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
|
||||
const [toolFunc, setToolFunc] = useState('')
|
||||
const [showHowToDialog, setShowHowToDialog] = useState(false)
|
||||
|
||||
const [exportAsTemplateDialogOpen, setExportAsTemplateDialogOpen] = useState(false)
|
||||
const [exportAsTemplateDialogProps, setExportAsTemplateDialogProps] = useState({})
|
||||
|
||||
const deleteItem = useCallback(
|
||||
(id) => () => {
|
||||
setTimeout(() => {
|
||||
@@ -105,6 +109,20 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
|
||||
})
|
||||
}
|
||||
|
||||
const onSaveAsTemplate = () => {
|
||||
setExportAsTemplateDialogProps({
|
||||
title: 'Export As Template',
|
||||
tool: {
|
||||
name: toolName,
|
||||
description: toolDesc,
|
||||
iconSrc: toolIcon,
|
||||
schema: toolSchema,
|
||||
func: toolFunc
|
||||
}
|
||||
})
|
||||
setExportAsTemplateDialogOpen(true)
|
||||
}
|
||||
|
||||
const onRowUpdate = (newRow) => {
|
||||
setTimeout(() => {
|
||||
setToolSchema((prevRows) => {
|
||||
@@ -401,11 +419,24 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
|
||||
<DialogTitle sx={{ fontSize: '1rem', p: 3, pb: 0 }} id='alert-dialog-title'>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||
{dialogProps.title}
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<Button variant='outlined' onClick={() => exportTool()} startIcon={<IconFileDownload />}>
|
||||
Export
|
||||
</Button>
|
||||
)}
|
||||
<Box>
|
||||
{dialogProps.type === 'EDIT' && (
|
||||
<>
|
||||
<Button
|
||||
style={{ marginRight: '10px' }}
|
||||
variant='outlined'
|
||||
onClick={() => onSaveAsTemplate()}
|
||||
startIcon={<IconTemplate />}
|
||||
color='secondary'
|
||||
>
|
||||
Save As Template
|
||||
</Button>
|
||||
<Button variant='outlined' onClick={() => exportTool()} startIcon={<IconFileDownload />}>
|
||||
Export
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ display: 'flex', flexDirection: 'column', gap: 2, maxHeight: '75vh', position: 'relative', px: 3, pb: 3 }}>
|
||||
@@ -535,6 +566,14 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm, set
|
||||
)}
|
||||
</DialogActions>
|
||||
<ConfirmDialog />
|
||||
{exportAsTemplateDialogOpen && (
|
||||
<ExportAsTemplateDialog
|
||||
show={exportAsTemplateDialogOpen}
|
||||
dialogProps={exportAsTemplateDialogProps}
|
||||
onCancel={() => setExportAsTemplateDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<HowToUseFunctionDialog show={showHowToDialog} onCancel={() => setShowHowToDialog(false)} />
|
||||
</Dialog>
|
||||
) : null
|
||||
|
||||
Reference in New Issue
Block a user