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