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:
Vinod Kiran
2024-09-16 19:14:39 +05:30
committed by GitHub
parent 44b70ca7e2
commit b02bdc74ad
23 changed files with 1217 additions and 170 deletions
+9 -1
View File
@@ -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
}
+11 -2
View File
@@ -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',
+11 -2
View File
@@ -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' }}>&nbsp;*</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'>
&nbsp;
</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
}
+37 -2
View File
@@ -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}
+403 -145
View File
@@ -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 />
</>
)
}
+45 -6
View File
@@ -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