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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1217 additions and 170 deletions

View File

@ -273,5 +273,18 @@ export interface IApiKey {
updatedDate: Date
}
export interface ICustomTemplate {
id: string
name: string
flowData: string
updatedDate: Date
createdDate: Date
description?: string
type?: string
badge?: string
framework?: string
usecases?: string
}
// DocumentStore related
export * from './Interface.DocumentStore'

View File

@ -1,5 +1,7 @@
import { Request, Response, NextFunction } from 'express'
import marketplacesService from '../../services/marketplaces'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { StatusCodes } from 'http-status-codes'
// Get all templates for marketplaces
const getAllTemplates = async (req: Request, res: Response, next: NextFunction) => {
@ -11,6 +13,48 @@ const getAllTemplates = async (req: Request, res: Response, next: NextFunction)
}
}
export default {
getAllTemplates
const deleteCustomTemplate = async (req: Request, res: Response, next: NextFunction) => {
try {
if (typeof req.params === 'undefined' || !req.params.id) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: marketplacesService.deleteCustomTemplate - id not provided!`
)
}
const apiResponse = await marketplacesService.deleteCustomTemplate(req.params.id)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const getAllCustomTemplates = async (req: Request, res: Response, next: NextFunction) => {
try {
const apiResponse = await marketplacesService.getAllCustomTemplates()
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
const saveCustomTemplate = async (req: Request, res: Response, next: NextFunction) => {
try {
if ((!req.body && !(req.body.chatflowId || req.body.tool)) || !req.body.name) {
throw new InternalFlowiseError(
StatusCodes.PRECONDITION_FAILED,
`Error: marketplacesService.saveCustomTemplate - body not provided!`
)
}
const apiResponse = await marketplacesService.saveCustomTemplate(req.body)
return res.json(apiResponse)
} catch (error) {
next(error)
}
}
export default {
getAllTemplates,
getAllCustomTemplates,
saveCustomTemplate,
deleteCustomTemplate
}

View File

@ -0,0 +1,37 @@
import { ICustomTemplate } from '../../Interface'
import { Column, CreateDateColumn, Entity, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
@Entity('custom_template')
export class CustomTemplate implements ICustomTemplate {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
name: string
@Column({ type: 'text' })
flowData: string
@Column({ nullable: true, type: 'text' })
description?: string
@Column({ nullable: true, type: 'text' })
badge?: string
@Column({ nullable: true, type: 'text' })
framework?: string
@Column({ nullable: true, type: 'text' })
usecases?: string
@Column({ nullable: true, type: 'text' })
type?: string
@Column({ type: 'timestamp' })
@CreateDateColumn()
createdDate: Date
@Column({ type: 'timestamp' })
@UpdateDateColumn()
updatedDate: Date
}

View File

@ -10,6 +10,7 @@ import { DocumentStoreFileChunk } from './DocumentStoreFileChunk'
import { Lead } from './Lead'
import { UpsertHistory } from './UpsertHistory'
import { ApiKey } from './ApiKey'
import { CustomTemplate } from './CustomTemplate'
export const entities = {
ChatFlow,
@ -23,5 +24,6 @@ export const entities = {
DocumentStoreFileChunk,
Lead,
UpsertHistory,
ApiKey
ApiKey,
CustomTemplate
}

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddCustomTemplate1725629836652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS \`custom_template\` (
\`id\` varchar(36) NOT NULL,
\`name\` varchar(255) NOT NULL,
\`flowData\` text NOT NULL,
\`description\` varchar(255) DEFAULT NULL,
\`badge\` varchar(255) DEFAULT NULL,
\`framework\` varchar(255) DEFAULT NULL,
\`usecases\` varchar(255) DEFAULT NULL,
\`type\` varchar(30) DEFAULT NULL,
\`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
\`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE custom_template`)
}
}

View File

@ -23,6 +23,7 @@ import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlo
import { AddApiKey1720230151480 } from './1720230151480-AddApiKey'
import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage'
import { LongTextColumn1722301395521 } from './1722301395521-LongTextColumn'
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
export const mariadbMigrations = [
Init1693840429259,
@ -49,5 +50,6 @@ export const mariadbMigrations = [
AddTypeToChatFlow1716300000000,
AddApiKey1720230151480,
AddActionToChatMessage1721078251523,
LongTextColumn1722301395521
LongTextColumn1722301395521,
AddCustomTemplate1725629836652
]

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddCustomTemplate1725629836652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS \`custom_template\` (
\`id\` varchar(36) NOT NULL,
\`name\` varchar(255) NOT NULL,
\`flowData\` text NOT NULL,
\`description\` varchar(255) DEFAULT NULL,
\`badge\` varchar(255) DEFAULT NULL,
\`framework\` varchar(255) DEFAULT NULL,
\`usecases\` varchar(255) DEFAULT NULL,
\`type\` varchar(30) DEFAULT NULL,
\`createdDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
\`updatedDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
PRIMARY KEY (\`id\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE custom_template`)
}
}

View File

@ -24,6 +24,7 @@ import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlo
import { AddApiKey1720230151480 } from './1720230151480-AddApiKey'
import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage'
import { LongTextColumn1722301395521 } from './1722301395521-LongTextColumn'
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
export const mysqlMigrations = [
Init1693840429259,
@ -51,5 +52,6 @@ export const mysqlMigrations = [
AddVectorStoreConfigToDocStore1715861032479,
AddApiKey1720230151480,
AddActionToChatMessage1721078251523,
LongTextColumn1722301395521
LongTextColumn1722301395521,
AddCustomTemplate1725629836652
]

View File

@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddCustomTemplate1725629836652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS custom_template (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
"name" varchar NOT NULL,
"flowData" text NOT NULL,
"description" varchar NULL,
"badge" varchar NULL,
"framework" varchar NULL,
"usecases" varchar NULL,
"type" varchar NULL,
"createdDate" timestamp NOT NULL DEFAULT now(),
"updatedDate" timestamp NOT NULL DEFAULT now(),
CONSTRAINT "PK_3c7cea7d087ac4b91764574cdbf" PRIMARY KEY (id)
);`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE custom_template`)
}
}

View File

@ -24,6 +24,7 @@ import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-Add
import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlow'
import { AddApiKey1720230151480 } from './1720230151480-AddApiKey'
import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage'
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
export const postgresMigrations = [
Init1693891895163,
@ -51,5 +52,6 @@ export const postgresMigrations = [
AddTypeToChatFlow1716300000000,
AddVectorStoreConfigToDocStore1715861032479,
AddApiKey1720230151480,
AddActionToChatMessage1721078251523
AddActionToChatMessage1721078251523,
AddCustomTemplate1725629836652
]

View File

@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm'
export class AddCustomTemplate1725629836652 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE IF NOT EXISTS "custom_template" (
"id" varchar PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"flowData" text NOT NULL,
"description" varchar,
"badge" varchar,
"framework" varchar,
"usecases" varchar,
"type" varchar,
"updatedDate" datetime NOT NULL DEFAULT (datetime('now')),
"createdDate" datetime NOT NULL DEFAULT (datetime('now')));`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS "custom_template";`)
}
}

View File

@ -23,6 +23,7 @@ import { AddAgentReasoningToChatMessage1714679514451 } from './1714679514451-Add
import { AddTypeToChatFlow1716300000000 } from './1716300000000-AddTypeToChatFlow'
import { AddApiKey1720230151480 } from './1720230151480-AddApiKey'
import { AddActionToChatMessage1721078251523 } from './1721078251523-AddActionToChatMessage'
import { AddCustomTemplate1725629836652 } from './1725629836652-AddCustomTemplate'
export const sqliteMigrations = [
Init1693835579790,
@ -49,5 +50,6 @@ export const sqliteMigrations = [
AddTypeToChatFlow1716300000000,
AddVectorStoreConfigToDocStore1715861032479,
AddApiKey1720230151480,
AddActionToChatMessage1721078251523
AddActionToChatMessage1721078251523,
AddCustomTemplate1725629836652
]

View File

@ -5,4 +5,12 @@ const router = express.Router()
// READ
router.get('/templates', marketplacesController.getAllTemplates)
router.post('/custom', marketplacesController.saveCustomTemplate)
// READ
router.get('/custom', marketplacesController.getAllCustomTemplates)
// DELETE
router.delete(['/', '/custom/:id'], marketplacesController.deleteCustomTemplate)
export default router

View File

@ -4,6 +4,11 @@ import { StatusCodes } from 'http-status-codes'
import { InternalFlowiseError } from '../../errors/internalFlowiseError'
import { getErrorMessage } from '../../errors/utils'
import { IReactFlowEdge, IReactFlowNode } from '../../Interface'
import { getRunningExpressApp } from '../../utils/getRunningExpressApp'
import { DeleteResult } from 'typeorm'
import { CustomTemplate } from '../../database/entities/CustomTemplate'
import chatflowsService from '../chatflows'
type ITemplate = {
badge: string
@ -96,6 +101,148 @@ const getAllTemplates = async () => {
}
}
export default {
getAllTemplates
const deleteCustomTemplate = async (templateId: string): Promise<DeleteResult> => {
try {
const appServer = getRunningExpressApp()
return await appServer.AppDataSource.getRepository(CustomTemplate).delete({ id: templateId })
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: marketplacesService.deleteCustomTemplate - ${getErrorMessage(error)}`
)
}
}
const getAllCustomTemplates = async (): Promise<any> => {
try {
const appServer = getRunningExpressApp()
const templates: any[] = await appServer.AppDataSource.getRepository(CustomTemplate).find()
templates.map((template) => {
template.usecases = template.usecases ? JSON.parse(template.usecases) : ''
if (template.type === 'Tool') {
template.flowData = JSON.parse(template.flowData)
template.iconSrc = template.flowData.iconSrc
template.schema = template.flowData.schema
template.func = template.flowData.func
template.categories = []
template.flowData = undefined
} else {
template.categories = getCategories(JSON.parse(template.flowData))
}
if (!template.badge) {
template.badge = ''
}
if (!template.framework) {
template.framework = ''
}
})
return templates
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: marketplacesService.getAllCustomTemplates - ${getErrorMessage(error)}`
)
}
}
const saveCustomTemplate = async (body: any): Promise<any> => {
try {
const appServer = getRunningExpressApp()
let flowDataStr = ''
let derivedFramework = ''
const customTemplate = new CustomTemplate()
Object.assign(customTemplate, body)
if (body.chatflowId) {
const chatflow = await chatflowsService.getChatflowById(body.chatflowId)
const flowData = JSON.parse(chatflow.flowData)
const { framework, exportJson } = _generateExportFlowData(flowData)
flowDataStr = JSON.stringify(exportJson)
customTemplate.framework = framework
} else if (body.tool) {
const flowData = {
iconSrc: body.tool.iconSrc,
schema: body.tool.schema,
func: body.tool.func
}
customTemplate.framework = ''
customTemplate.type = 'Tool'
flowDataStr = JSON.stringify(flowData)
}
customTemplate.framework = derivedFramework
if (customTemplate.usecases) {
customTemplate.usecases = JSON.stringify(customTemplate.usecases)
}
const entity = appServer.AppDataSource.getRepository(CustomTemplate).create(customTemplate)
entity.flowData = flowDataStr
const flowTemplate = await appServer.AppDataSource.getRepository(CustomTemplate).save(entity)
return flowTemplate
} catch (error) {
throw new InternalFlowiseError(
StatusCodes.INTERNAL_SERVER_ERROR,
`Error: marketplacesService.saveCustomTemplate - ${getErrorMessage(error)}`
)
}
}
const _generateExportFlowData = (flowData: any) => {
const nodes = flowData.nodes
const edges = flowData.edges
let framework = 'Langchain'
for (let i = 0; i < nodes.length; i += 1) {
nodes[i].selected = false
const node = nodes[i]
const newNodeData = {
id: node.data.id,
label: node.data.label,
version: node.data.version,
name: node.data.name,
type: node.data.type,
baseClasses: node.data.baseClasses,
tags: node.data.tags,
category: node.data.category,
description: node.data.description,
inputParams: node.data.inputParams,
inputAnchors: node.data.inputAnchors,
inputs: {},
outputAnchors: node.data.outputAnchors,
outputs: node.data.outputs,
selected: false
}
if (node.data.tags && node.data.tags.length) {
if (node.data.tags.includes('LlamaIndex')) {
framework = 'LlamaIndex'
}
}
// Remove password, file & folder
if (node.data.inputs && Object.keys(node.data.inputs).length) {
const nodeDataInputs: any = {}
for (const input in node.data.inputs) {
const inputParam = node.data.inputParams.find((inp: any) => inp.name === input)
if (inputParam && inputParam.type === 'password') continue
if (inputParam && inputParam.type === 'file') continue
if (inputParam && inputParam.type === 'folder') continue
nodeDataInputs[input] = node.data.inputs[input]
}
newNodeData.inputs = nodeDataInputs
}
nodes[i].data = newNodeData
}
const exportJson = {
nodes,
edges
}
return { exportJson, framework }
}
export default {
getAllTemplates,
getAllCustomTemplates,
saveCustomTemplate,
deleteCustomTemplate
}

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
}

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',

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',

View File

@ -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>
)
}

View File

@ -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

View File

@ -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
}

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}

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 />
</>
)
}

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