diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index dbb5717df..8878da948 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1223,7 +1223,6 @@ export class App { // Marketplaces // ---------------------------------------- - // Get all chatflows for marketplaces this.app.get('/api/v1/marketplaces/chatflows', async (req: Request, res: Response) => { const marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'chatflows') const jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') @@ -1250,6 +1249,49 @@ export class App { return res.json(templates) }) + // Get all chatflows for marketplaces + this.app.get('/api/v1/marketplaces/templates', async (req: Request, res: Response) => { + let marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'chatflows') + let jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') + let templates: any[] = [] + jsonsInDir.forEach((file, index) => { + const filePath = path.join(__dirname, '..', 'marketplaces', 'chatflows', file) + const fileData = fs.readFileSync(filePath) + const fileDataObj = JSON.parse(fileData.toString()) + const template = { + id: index, + templateName: file.split('.json')[0], + flowData: fileData.toString(), + badge: fileDataObj?.badge, + type: 'Chatflow', + description: fileDataObj?.description || '' + } + templates.push(template) + }) + + marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'tools') + jsonsInDir = fs.readdirSync(marketplaceDir).filter((file) => path.extname(file) === '.json') + jsonsInDir.forEach((file, index) => { + const filePath = path.join(__dirname, '..', 'marketplaces', 'tools', file) + const fileData = fs.readFileSync(filePath) + const fileDataObj = JSON.parse(fileData.toString()) + const template = { + ...fileDataObj, + id: index, + type: 'Tool', + templateName: file.split('.json')[0] + } + templates.push(template) + }) + const FlowiseDocsQnA = templates.find((tmp) => tmp.name === 'Flowise Docs QnA') + const FlowiseDocsQnAIndex = templates.findIndex((tmp) => tmp.name === 'Flowise Docs QnA') + if (FlowiseDocsQnA && FlowiseDocsQnAIndex > 0) { + templates.splice(FlowiseDocsQnAIndex, 1) + templates.unshift(FlowiseDocsQnA) + } + return res.json(templates.sort((a, b) => a.templateName.localeCompare(b.templateName))) + }) + // Get all tools for marketplaces this.app.get('/api/v1/marketplaces/tools', async (req: Request, res: Response) => { const marketplaceDir = path.join(__dirname, '..', 'marketplaces', 'tools') diff --git a/packages/ui/craco.config.js b/packages/ui/craco.config.js index 142305e01..093e5ecea 100644 --- a/packages/ui/craco.config.js +++ b/packages/ui/craco.config.js @@ -10,7 +10,8 @@ module.exports = { } } ] - } + }, + ignoreWarnings: [/Failed to parse source map/] // Ignore warnings about source maps } } } diff --git a/packages/ui/src/api/marketplaces.js b/packages/ui/src/api/marketplaces.js index 3fd4ae872..bba914a70 100644 --- a/packages/ui/src/api/marketplaces.js +++ b/packages/ui/src/api/marketplaces.js @@ -2,8 +2,10 @@ import client from './client' const getAllChatflowsMarketplaces = () => client.get('/marketplaces/chatflows') const getAllToolsMarketplaces = () => client.get('/marketplaces/tools') +const getAllTemplatesFromMarketplaces = () => client.get('/marketplaces/templates') export default { getAllChatflowsMarketplaces, - getAllToolsMarketplaces + getAllToolsMarketplaces, + getAllTemplatesFromMarketplaces } diff --git a/packages/ui/src/ui-component/table/MarketplaceTable.js b/packages/ui/src/ui-component/table/MarketplaceTable.js new file mode 100644 index 000000000..135d19e90 --- /dev/null +++ b/packages/ui/src/ui-component/table/MarketplaceTable.js @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types' +import { useNavigate } from 'react-router-dom' +import { styled } from '@mui/material/styles' +import Table from '@mui/material/Table' +import TableBody from '@mui/material/TableBody' +import TableCell, { tableCellClasses } from '@mui/material/TableCell' +import TableContainer from '@mui/material/TableContainer' +import TableHead from '@mui/material/TableHead' +import TableRow from '@mui/material/TableRow' +import Paper from '@mui/material/Paper' +import Chip from '@mui/material/Chip' +import { Button, Typography } from '@mui/material' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + backgroundColor: theme.palette.common.black, + color: theme.palette.common.white + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14 + } +})) + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + backgroundColor: theme.palette.action.hover + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +export const MarketplaceTable = ({ data, images, filterFunction, filterByBadge, filterByType }) => { + const navigate = useNavigate() + const openTemplate = (selectedTemplate) => { + if (selectedTemplate.flowData) { + goToCanvas(selectedTemplate) + } else { + goToTool(selectedTemplate) + } + } + + const goToTool = (selectedTool) => { + const dialogProp = { + title: selectedTool.templateName, + type: 'TEMPLATE', + data: selectedTool + } + setToolDialogProps(dialogProp) + setShowToolDialog(true) + } + + const goToCanvas = (selectedChatflow) => { + navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow }) + } + + return ( + <> + + + + + + Name + + + Type + + + Description + + + Nodes + + + + + + + + {data + .filter(filterByBadge) + .filter(filterByType) + .filter(filterFunction) + .map((row, index) => ( + + + + openTemplate(row)} sx={{ textAlign: 'left' }}> + {row.templateName || row.name} + + + + + {row.type} + + + + {row.description || ''} + + + + {row.type === 'Chatflow' && images[row.id] && ( + + {images[row.id] + .slice(0, images[row.id].length > 5 ? 5 : images[row.id].length) + .map((img) => ( + + + + ))} + {images[row.id].length > 5 && ( + + + {images[row.id].length - 5} More + + )} + + )} + + + + {row.badge && + row.badge + .split(';') + .map((tag, index) => ( + + ))} + + + + ))} + + + + > + ) +} + +MarketplaceTable.propTypes = { + data: PropTypes.array, + images: PropTypes.object, + filterFunction: PropTypes.func, + filterByBadge: PropTypes.func, + filterByType: PropTypes.func +} diff --git a/packages/ui/src/views/marketplaces/index.js b/packages/ui/src/views/marketplaces/index.js index 665341c45..ac0186635 100644 --- a/packages/ui/src/views/marketplaces/index.js +++ b/packages/ui/src/views/marketplaces/index.js @@ -4,9 +4,25 @@ import { useSelector } from 'react-redux' import PropTypes from 'prop-types' // material-ui -import { Grid, Box, Stack, Tabs, Tab, Badge } from '@mui/material' +import { + Grid, + Box, + Stack, + Badge, + Toolbar, + TextField, + InputAdornment, + ButtonGroup, + ToggleButton, + InputLabel, + FormControl, + Select, + OutlinedInput, + Checkbox, + ListItemText +} from '@mui/material' import { useTheme } from '@mui/material/styles' -import { IconHierarchy, IconTool } from '@tabler/icons' +import { IconLayoutGrid, IconList, IconSearch } from '@tabler/icons' // project imports import MainCard from 'ui-component/cards/MainCard' @@ -23,6 +39,10 @@ import useApi from 'hooks/useApi' // const import { baseURL } from 'store/constant' +import * as React from 'react' +import ToggleButtonGroup from '@mui/material/ToggleButtonGroup' +import { MarketplaceTable } from '../../ui-component/table/MarketplaceTable' +import MenuItem from '@mui/material/MenuItem' function TabPanel(props) { const { children, value, index, ...other } = props @@ -45,6 +65,18 @@ TabPanel.propTypes = { value: PropTypes.number.isRequired } +const ITEM_HEIGHT = 48 +const ITEM_PADDING_TOP = 8 +const badges = ['POPULAR', 'NEW'] +const types = ['Chatflow', 'Tool'] +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 250 + } + } +} // ==============================|| Marketplace ||============================== // const Marketplace = () => { @@ -53,16 +85,62 @@ const Marketplace = () => { const theme = useTheme() const customization = useSelector((state) => state.customization) - const [isChatflowsLoading, setChatflowsLoading] = useState(true) - const [isToolsLoading, setToolsLoading] = useState(true) + const [isLoading, setLoading] = useState(true) const [images, setImages] = useState({}) - const tabItems = ['Chatflows', 'Tools'] - const [value, setValue] = useState(0) + const [showToolDialog, setShowToolDialog] = useState(false) const [toolDialogProps, setToolDialogProps] = useState({}) - const getAllChatflowsMarketplacesApi = useApi(marketplacesApi.getAllChatflowsMarketplaces) - const getAllToolsMarketplacesApi = useApi(marketplacesApi.getAllToolsMarketplaces) + const getAllTemplatesMarketplacesApi = useApi(marketplacesApi.getAllTemplatesFromMarketplaces) + + const [view, setView] = React.useState(localStorage.getItem('mpDisplayStyle') || 'card') + const [search, setSearch] = useState('') + + const [badgeFilter, setBadgeFilter] = useState([]) + const [typeFilter, setTypeFilter] = useState([]) + + const handleBadgeFilterChange = (event) => { + const { + target: { value } + } = event + setBadgeFilter( + // On autofill we get a stringified value. + typeof value === 'string' ? value.split(',') : value + ) + } + const handleTypeFilterChange = (event) => { + const { + target: { value } + } = event + setTypeFilter( + // On autofill we get a stringified value. + typeof value === 'string' ? value.split(',') : value + ) + } + + const handleViewChange = (event, nextView) => { + localStorage.setItem('mpDisplayStyle', nextView) + setView(nextView) + } + + const onSearchChange = (event) => { + setSearch(event.target.value) + } + + function filterFlows(data) { + return ( + data.templateName.toLowerCase().indexOf(search.toLowerCase()) > -1 || + (data.description && data.description.toLowerCase().indexOf(search.toLowerCase()) > -1) + ) + } + + function filterByBadge(data) { + return badgeFilter.length > 0 ? badgeFilter.includes(data.badge) : true + } + + function filterByType(data) { + return typeFilter.length > 0 ? typeFilter.includes(data.type) : true + } const onUseTemplate = (selectedTool) => { const dialogProp = { @@ -90,39 +168,33 @@ const Marketplace = () => { navigate(`/marketplace/${selectedChatflow.id}`, { state: selectedChatflow }) } - const handleChange = (event, newValue) => { - setValue(newValue) - } - useEffect(() => { - getAllChatflowsMarketplacesApi.request() - getAllToolsMarketplacesApi.request() + getAllTemplatesMarketplacesApi.request() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) useEffect(() => { - setChatflowsLoading(getAllChatflowsMarketplacesApi.loading) - }, [getAllChatflowsMarketplacesApi.loading]) + setLoading(getAllTemplatesMarketplacesApi.loading) + }, [getAllTemplatesMarketplacesApi.loading]) useEffect(() => { - setToolsLoading(getAllToolsMarketplacesApi.loading) - }, [getAllToolsMarketplacesApi.loading]) - - useEffect(() => { - if (getAllChatflowsMarketplacesApi.data) { + if (getAllTemplatesMarketplacesApi.data) { try { - const chatflows = getAllChatflowsMarketplacesApi.data + const flows = getAllTemplatesMarketplacesApi.data + const images = {} - for (let i = 0; i < chatflows.length; i += 1) { - const flowDataStr = chatflows[i].flowData - const flowData = JSON.parse(flowDataStr) - const nodes = flowData.nodes || [] - images[chatflows[i].id] = [] - for (let j = 0; j < nodes.length; j += 1) { - const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}` - if (!images[chatflows[i].id].includes(imageSrc)) { - images[chatflows[i].id].push(imageSrc) + for (let i = 0; i < flows.length; i += 1) { + if (flows[i].flowData) { + const flowDataStr = flows[i].flowData + const flowData = JSON.parse(flowDataStr) + const nodes = flowData.nodes || [] + images[flows[i].id] = [] + for (let j = 0; j < nodes.length; j += 1) { + const imageSrc = `${baseURL}/api/v1/node-icon/${nodes[j].data.name}` + if (!images[flows[i].id].includes(imageSrc)) { + images[flows[i].id].push(imageSrc) + } } } } @@ -131,80 +203,161 @@ const Marketplace = () => { console.error(e) } } - }, [getAllChatflowsMarketplacesApi.data]) + }, [getAllTemplatesMarketplacesApi.data]) return ( <> - - Marketplace - - - {tabItems.map((item, index) => ( - : } - iconPosition='start' - label={{item}} + + + Marketplace + + + + ) + }} /> - ))} - - {tabItems.map((item, index) => ( - - {item === 'Chatflows' && ( - - {!isChatflowsLoading && - getAllChatflowsMarketplacesApi.data && - getAllChatflowsMarketplacesApi.data.map((data, index) => ( - - {data.badge && ( - + + + Type + + } + renderValue={(selected) => selected.join(', ')} + MenuProps={MenuProps} + > + {types.map((name) => ( + + -1} /> + + + ))} + + + + + Tag + + } + renderValue={(selected) => selected.join(', ')} + MenuProps={MenuProps} + > + {badges.map((name) => ( + + -1} /> + + + ))} + + + + + + + + + + + + + + + + + + {!isLoading && (!view || view === 'card') && getAllTemplatesMarketplacesApi.data && ( + <> + + {getAllTemplatesMarketplacesApi.data + .filter(filterByBadge) + .filter(filterByType) + .filter(filterFlows) + .map((data, index) => ( + + {data.badge && ( + + {data.type === 'Chatflow' && ( goToCanvas(data)} data={data} images={images[data.id]} /> - - )} - {!data.badge && ( - goToCanvas(data)} data={data} images={images[data.id]} /> - )} - - ))} - - )} - {item === 'Tools' && ( - - {!isToolsLoading && - getAllToolsMarketplacesApi.data && - getAllToolsMarketplacesApi.data.map((data, index) => ( - - {data.badge && ( - - goToTool(data)} /> - - )} - {!data.badge && goToTool(data)} />} - - ))} - - )} - - ))} - {((!isChatflowsLoading && (!getAllChatflowsMarketplacesApi.data || getAllChatflowsMarketplacesApi.data.length === 0)) || - (!isToolsLoading && (!getAllToolsMarketplacesApi.data || getAllToolsMarketplacesApi.data.length === 0))) && ( + )} + {data.type === 'Tool' && goToTool(data)} />} + + )} + {!data.badge && data.type === 'Chatflow' && ( + goToCanvas(data)} data={data} images={images[data.id]} /> + )} + {!data.badge && data.type === 'Tool' && goToTool(data)} />} + + ))} + + > + )} + {!isLoading && view === 'list' && getAllTemplatesMarketplacesApi.data && ( + + )} + + {!isLoading && (!getAllTemplatesMarketplacesApi.data || getAllTemplatesMarketplacesApi.data.length === 0) && (