diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index dc904fc9b..73300d574 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -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' diff --git a/packages/server/src/controllers/marketplaces/index.ts b/packages/server/src/controllers/marketplaces/index.ts index 62371a734..db947151f 100644 --- a/packages/server/src/controllers/marketplaces/index.ts +++ b/packages/server/src/controllers/marketplaces/index.ts @@ -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 } diff --git a/packages/server/src/database/entities/CustomTemplate.ts b/packages/server/src/database/entities/CustomTemplate.ts new file mode 100644 index 000000000..27b88d78e --- /dev/null +++ b/packages/server/src/database/entities/CustomTemplate.ts @@ -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 +} diff --git a/packages/server/src/database/entities/index.ts b/packages/server/src/database/entities/index.ts index 0e715e68b..4cb079b8b 100644 --- a/packages/server/src/database/entities/index.ts +++ b/packages/server/src/database/entities/index.ts @@ -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 } diff --git a/packages/server/src/database/migrations/mariadb/1725629836652-AddCustomTemplate.ts b/packages/server/src/database/migrations/mariadb/1725629836652-AddCustomTemplate.ts new file mode 100644 index 000000000..b88ab5306 --- /dev/null +++ b/packages/server/src/database/migrations/mariadb/1725629836652-AddCustomTemplate.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCustomTemplate1725629836652 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE custom_template`) + } +} diff --git a/packages/server/src/database/migrations/mariadb/index.ts b/packages/server/src/database/migrations/mariadb/index.ts index 4d6866751..dfab43925 100644 --- a/packages/server/src/database/migrations/mariadb/index.ts +++ b/packages/server/src/database/migrations/mariadb/index.ts @@ -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 ] diff --git a/packages/server/src/database/migrations/mysql/1725629836652-AddCustomTemplate.ts b/packages/server/src/database/migrations/mysql/1725629836652-AddCustomTemplate.ts new file mode 100644 index 000000000..b88ab5306 --- /dev/null +++ b/packages/server/src/database/migrations/mysql/1725629836652-AddCustomTemplate.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCustomTemplate1725629836652 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE custom_template`) + } +} diff --git a/packages/server/src/database/migrations/mysql/index.ts b/packages/server/src/database/migrations/mysql/index.ts index fda6caaec..4851fe566 100644 --- a/packages/server/src/database/migrations/mysql/index.ts +++ b/packages/server/src/database/migrations/mysql/index.ts @@ -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 ] diff --git a/packages/server/src/database/migrations/postgres/1725629836652-AddCustomTemplate.ts b/packages/server/src/database/migrations/postgres/1725629836652-AddCustomTemplate.ts new file mode 100644 index 000000000..04a61c0ff --- /dev/null +++ b/packages/server/src/database/migrations/postgres/1725629836652-AddCustomTemplate.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCustomTemplate1725629836652 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE custom_template`) + } +} diff --git a/packages/server/src/database/migrations/postgres/index.ts b/packages/server/src/database/migrations/postgres/index.ts index e6e154bee..702cdc8e1 100644 --- a/packages/server/src/database/migrations/postgres/index.ts +++ b/packages/server/src/database/migrations/postgres/index.ts @@ -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 ] diff --git a/packages/server/src/database/migrations/sqlite/1725629836652-AddCustomTemplate.ts b/packages/server/src/database/migrations/sqlite/1725629836652-AddCustomTemplate.ts new file mode 100644 index 000000000..80e633b6d --- /dev/null +++ b/packages/server/src/database/migrations/sqlite/1725629836652-AddCustomTemplate.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm' + +export class AddCustomTemplate1725629836652 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + 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 { + await queryRunner.query(`DROP TABLE IF EXISTS "custom_template";`) + } +} diff --git a/packages/server/src/database/migrations/sqlite/index.ts b/packages/server/src/database/migrations/sqlite/index.ts index 69bd8a69e..0695ccfe1 100644 --- a/packages/server/src/database/migrations/sqlite/index.ts +++ b/packages/server/src/database/migrations/sqlite/index.ts @@ -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 ] diff --git a/packages/server/src/routes/marketplaces/index.ts b/packages/server/src/routes/marketplaces/index.ts index c7eae1f3e..d97f96f38 100644 --- a/packages/server/src/routes/marketplaces/index.ts +++ b/packages/server/src/routes/marketplaces/index.ts @@ -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 diff --git a/packages/server/src/services/marketplaces/index.ts b/packages/server/src/services/marketplaces/index.ts index 5e044adb7..ee5d13132 100644 --- a/packages/server/src/services/marketplaces/index.ts +++ b/packages/server/src/services/marketplaces/index.ts @@ -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 => { + 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 => { + 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 => { + 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 } diff --git a/packages/ui/src/api/marketplaces.js b/packages/ui/src/api/marketplaces.js index bba914a70..f7099826a 100644 --- a/packages/ui/src/api/marketplaces.js +++ b/packages/ui/src/api/marketplaces.js @@ -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 } diff --git a/packages/ui/src/menu-items/agentsettings.js b/packages/ui/src/menu-items/agentsettings.js index 0fef22254..741ce0322 100644 --- a/packages/ui/src/menu-items/agentsettings.js +++ b/packages/ui/src/menu-items/agentsettings.js @@ -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', diff --git a/packages/ui/src/menu-items/settings.js b/packages/ui/src/menu-items/settings.js index 6b8d8bc73..94ff397c3 100644 --- a/packages/ui/src/menu-items/settings.js +++ b/packages/ui/src/menu-items/settings.js @@ -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', diff --git a/packages/ui/src/ui-component/button/FlowListMenu.jsx b/packages/ui/src/ui-component/button/FlowListMenu.jsx index 1a7e620b0..c720c53c6 100644 --- a/packages/ui/src/ui-component/button/FlowListMenu.jsx +++ b/packages/ui/src/ui-component/button/FlowListMenu.jsx @@ -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) => ( { @@ -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 Export + + + Save As Template + @@ -369,6 +386,13 @@ export default function FlowListMenu({ chatflow, isAgentCanvas, setError, update dialogProps={speechToTextDialogProps} onCancel={() => setSpeechToTextDialogOpen(false)} /> + {exportTemplateDialogOpen && ( + setExportTemplateDialogOpen(false)} + /> + )} ) } diff --git a/packages/ui/src/ui-component/dialog/ExportAsTemplateDialog.jsx b/packages/ui/src/ui-component/dialog/ExportAsTemplateDialog.jsx new file mode 100644 index 000000000..e7251c257 --- /dev/null +++ b/packages/ui/src/ui-component/dialog/ExportAsTemplateDialog.jsx @@ -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) => ( + + ) + } + }) + 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) => ( + + ) + } + }) + 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) => ( + + ) + } + }) + onCancel() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [saveCustomTemplateApi.error]) + + const component = show ? ( + + + {dialogProps.title || 'Export As Template'} + + + +
+ + Name * + + { + setName(e.target.value) + }} + /> +
+
+ +
+ Description + { + setDescription(e.target.value) + }} + /> +
+
+ +
+ Badge + { + setBadge(e.target.value) + }} + /> +
+
+ +
+ Usecases + {usecases.length > 0 && ( +
+ {usecases.map((uc, index) => ( + handleUsecaseDelete(uc)} + style={{ marginRight: 5, marginBottom: 5 }} + /> + ))} +
+ )} + + + Type a usecase and press enter to add it to the list. You can add as many items as you want. + +
+
+
+ + + + {dialogProps.confirmButtonName || 'Save Template'} + + +
+ ) : null + + return createPortal(component, portalElement) +} + +ExportAsTemplateDialog.propTypes = { + show: PropTypes.bool, + dialogProps: PropTypes.object, + onCancel: PropTypes.func, + onConfirm: PropTypes.func +} + +export default ExportAsTemplateDialog diff --git a/packages/ui/src/ui-component/table/MarketplaceTable.jsx b/packages/ui/src/ui-component/table/MarketplaceTable.jsx index 5b3cc8795..bc252f7c1 100644 --- a/packages/ui/src/ui-component/table/MarketplaceTable.jsx +++ b/packages/ui/src/ui-component/table/MarketplaceTable.jsx @@ -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 = ({   + {onDelete && ( + + Delete + + )} @@ -114,6 +122,11 @@ export const MarketplaceTable = ({ + {onDelete && ( + + + + )} @@ -137,6 +150,11 @@ export const MarketplaceTable = ({ + {onDelete && ( + + + + )} ) : ( @@ -234,6 +252,13 @@ export const MarketplaceTable = ({ ))} + {onDelete && ( + + onDelete(row)}> + + + + )} ))} @@ -254,5 +279,6 @@ MarketplaceTable.propTypes = { filterByUsecases: PropTypes.func, goToTool: PropTypes.func, goToCanvas: PropTypes.func, - isLoading: PropTypes.bool + isLoading: PropTypes.bool, + onDelete: PropTypes.func } diff --git a/packages/ui/src/views/canvas/CanvasHeader.jsx b/packages/ui/src/views/canvas/CanvasHeader.jsx index 69c2909b8..228d18ef5 100644 --- a/packages/ui/src/views/canvas/CanvasHeader.jsx +++ b/packages/ui/src/views/canvas/CanvasHeader.jsx @@ -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) => ( + + ) + } + }) + 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)} /> setViewLeadsDialogOpen(false)} /> + {exportAsTemplateDialogOpen && ( + setExportAsTemplateDialogOpen(false)} + /> + )}