diff --git a/.gitignore b/.gitignore index 5604f482a..9f5ef2e56 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,8 @@ **/*.key **/api.json +## uploads +**/uploads + ## compressed **/*.tgz \ No newline at end of file diff --git a/packages/components/nodes/documentloaders/Csv/Csv.ts b/packages/components/nodes/documentloaders/Csv/Csv.ts index 2ead7055c..5a4ca76e8 100644 --- a/packages/components/nodes/documentloaders/Csv/Csv.ts +++ b/packages/components/nodes/documentloaders/Csv/Csv.ts @@ -1,6 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { TextSplitter } from 'langchain/text_splitter' import { CSVLoader } from 'langchain/document_loaders/fs/csv' +import { getBlob } from '../../../src/utils' class Csv_DocumentLoaders implements INode { label: string @@ -48,11 +49,8 @@ class Csv_DocumentLoaders implements INode { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const csvFileBase64 = nodeData.inputs?.csvFile as string const columnName = nodeData.inputs?.columnName as string - const splitDataURI = csvFileBase64.split(',') - splitDataURI.pop() - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - const blob = new Blob([bf]) + const blob = new Blob(getBlob(csvFileBase64)) const loader = new CSVLoader(blob, columnName.trim().length === 0 ? undefined : columnName.trim()) if (textSplitter) { diff --git a/packages/components/nodes/documentloaders/Docx/Docx.ts b/packages/components/nodes/documentloaders/Docx/Docx.ts index bfc859b96..87a9cc44d 100644 --- a/packages/components/nodes/documentloaders/Docx/Docx.ts +++ b/packages/components/nodes/documentloaders/Docx/Docx.ts @@ -1,6 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { TextSplitter } from 'langchain/text_splitter' import { DocxLoader } from 'langchain/document_loaders/fs/docx' +import { getBlob } from '../../../src/utils' class Docx_DocumentLoaders implements INode { label: string @@ -39,11 +40,8 @@ class Docx_DocumentLoaders implements INode { async init(nodeData: INodeData): Promise { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const docxFileBase64 = nodeData.inputs?.docxFile as string - const splitDataURI = docxFileBase64.split(',') - splitDataURI.pop() - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - const blob = new Blob([bf]) + const blob = new Blob(getBlob(docxFileBase64)) const loader = new DocxLoader(blob) if (textSplitter) { diff --git a/packages/components/nodes/documentloaders/Json/Json.ts b/packages/components/nodes/documentloaders/Json/Json.ts index ee0d0ba4f..3ecdda6bd 100644 --- a/packages/components/nodes/documentloaders/Json/Json.ts +++ b/packages/components/nodes/documentloaders/Json/Json.ts @@ -1,6 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { TextSplitter } from 'langchain/text_splitter' import { JSONLoader } from 'langchain/document_loaders/fs/json' +import { getBlob } from '../../../src/utils' class Json_DocumentLoaders implements INode { label: string @@ -48,9 +49,6 @@ class Json_DocumentLoaders implements INode { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const jsonFileBase64 = nodeData.inputs?.jsonFile as string const pointersName = nodeData.inputs?.pointersName as string - const splitDataURI = jsonFileBase64.split(',') - splitDataURI.pop() - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') let pointers: string[] = [] if (pointersName) { @@ -58,7 +56,7 @@ class Json_DocumentLoaders implements INode { pointers = outputString.split(',').map((pointer) => '/' + pointer.trim()) } - const blob = new Blob([bf]) + const blob = new Blob(getBlob(jsonFileBase64)) const loader = new JSONLoader(blob, pointers.length != 0 ? pointers : undefined) if (textSplitter) { diff --git a/packages/components/nodes/documentloaders/Pdf/Pdf.ts b/packages/components/nodes/documentloaders/Pdf/Pdf.ts index 7933e6a04..59c0fab7a 100644 --- a/packages/components/nodes/documentloaders/Pdf/Pdf.ts +++ b/packages/components/nodes/documentloaders/Pdf/Pdf.ts @@ -1,6 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { TextSplitter } from 'langchain/text_splitter' import { PDFLoader } from 'langchain/document_loaders/fs/pdf' +import { getBlob } from '../../../src/utils' class Pdf_DocumentLoaders implements INode { label: string @@ -57,10 +58,7 @@ class Pdf_DocumentLoaders implements INode { const pdfFileBase64 = nodeData.inputs?.pdfFile as string const usage = nodeData.inputs?.usage as string - const splitDataURI = pdfFileBase64.split(',') - splitDataURI.pop() - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - const blob = new Blob([bf]) + const blob = new Blob(getBlob(pdfFileBase64)) if (usage === 'perFile') { // @ts-ignore diff --git a/packages/components/nodes/documentloaders/Text/Text.ts b/packages/components/nodes/documentloaders/Text/Text.ts index bad20792f..73c48ad32 100644 --- a/packages/components/nodes/documentloaders/Text/Text.ts +++ b/packages/components/nodes/documentloaders/Text/Text.ts @@ -1,6 +1,7 @@ import { INode, INodeData, INodeParams } from '../../../src/Interface' import { TextSplitter } from 'langchain/text_splitter' import { TextLoader } from 'langchain/document_loaders/fs/text' +import { getBlob } from '../../../src/utils' class Text_DocumentLoaders implements INode { label: string @@ -39,11 +40,8 @@ class Text_DocumentLoaders implements INode { async init(nodeData: INodeData): Promise { const textSplitter = nodeData.inputs?.textSplitter as TextSplitter const txtFileBase64 = nodeData.inputs?.txtFile as string - const splitDataURI = txtFileBase64.split(',') - splitDataURI.pop() - const bf = Buffer.from(splitDataURI.pop() || '', 'base64') - const blob = new Blob([bf]) + const blob = new Blob(getBlob(txtFileBase64)) const loader = new TextLoader(blob) if (textSplitter) { diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 7a2a4d25d..a2a415254 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -149,3 +149,27 @@ export const getInputVariables = (paramValue: string): string[] => { } return inputVariables } + +/** + * Get blob + * @param {string} fileBase64Str + * @returns {Buffer[]} + */ +export const getBlob = (fileBase64Str: string) => { + let bufferArray: Buffer[] = [] + let files: string[] = [] + + if (fileBase64Str.startsWith('[') && fileBase64Str.endsWith(']')) { + files = JSON.parse(fileBase64Str) + } else { + files = [fileBase64Str] + } + + for (const file of files) { + const splitDataURI = file.split(',') + splitDataURI.pop() + const bf = Buffer.from(splitDataURI.pop() || '', 'base64') + bufferArray.push(bf) + } + return bufferArray +} diff --git a/packages/server/package.json b/packages/server/package.json index a9d71d311..785b02053 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -54,12 +54,14 @@ "flowise-components": "*", "flowise-ui": "*", "moment-timezone": "^0.5.34", + "multer": "^1.4.5-lts.1", "reflect-metadata": "^0.1.13", "sqlite3": "^5.1.6", "typeorm": "^0.3.6" }, "devDependencies": { "@types/cors": "^2.8.12", + "@types/multer": "^1.4.7", "concurrently": "^7.1.0", "nodemon": "^2.0.15", "oclif": "^3", diff --git a/packages/server/src/ChatflowPool.ts b/packages/server/src/ChatflowPool.ts index 3b363a8b3..35b0d9478 100644 --- a/packages/server/src/ChatflowPool.ts +++ b/packages/server/src/ChatflowPool.ts @@ -1,3 +1,4 @@ +import { ICommonObject } from 'flowise-components' import { IActiveChatflows, INodeData, IReactFlowNode } from './Interface' /** @@ -12,13 +13,15 @@ export class ChatflowPool { * @param {string} chatflowid * @param {INodeData} endingNodeData * @param {IReactFlowNode[]} startingNodes + * @param {ICommonObject} overrideConfig */ - add(chatflowid: string, endingNodeData: INodeData, startingNodes: IReactFlowNode[]) { + add(chatflowid: string, endingNodeData: INodeData, startingNodes: IReactFlowNode[], overrideConfig?: ICommonObject) { this.activeChatflows[chatflowid] = { startingNodes, endingNodeData, inSync: true } + if (overrideConfig) this.activeChatflows[chatflowid].overrideConfig = overrideConfig } /** diff --git a/packages/server/src/Interface.ts b/packages/server/src/Interface.ts index 8e3c7b6b1..bba3ac8ac 100644 --- a/packages/server/src/Interface.ts +++ b/packages/server/src/Interface.ts @@ -1,4 +1,4 @@ -import { INode, INodeData as INodeDataFromComponent, INodeParams } from 'flowise-components' +import { ICommonObject, INode, INodeData as INodeDataFromComponent, INodeParams } from 'flowise-components' export type MessageType = 'apiMessage' | 'userMessage' @@ -114,6 +114,7 @@ export interface IMessage { export interface IncomingInput { question: string history: IMessage[] + overrideConfig?: ICommonObject } export interface IActiveChatflows { @@ -121,5 +122,13 @@ export interface IActiveChatflows { startingNodes: IReactFlowNode[] endingNodeData: INodeData inSync: boolean + overrideConfig?: ICommonObject } } + +export interface IOverrideConfig { + node: string + label: string + name: string + type: string +} diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 2318aab30..68c301722 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -1,4 +1,5 @@ import express, { Request, Response } from 'express' +import multer from 'multer' import path from 'path' import cors from 'cors' import http from 'http' @@ -17,7 +18,10 @@ import { addAPIKey, updateAPIKey, deleteAPIKey, - compareKeys + compareKeys, + mapMimeTypeToInputField, + findAvailableConfigs, + isSameOverrideConfig } from './utils' import { cloneDeep } from 'lodash' import { getDataSource } from './DataSource' @@ -25,6 +29,7 @@ import { NodesPool } from './NodesPool' import { ChatFlow } from './entity/ChatFlow' import { ChatMessage } from './entity/ChatMessage' import { ChatflowPool } from './ChatflowPool' +import { ICommonObject } from 'flowise-components' export class App { app: express.Application @@ -66,6 +71,8 @@ export class App { this.app.use(cors({ credentials: true, origin: 'http://localhost:8080' })) } + const upload = multer({ dest: `${path.join(__dirname, '..', 'uploads')}/` }) + // ---------------------------------------- // Nodes // ---------------------------------------- @@ -199,6 +206,47 @@ export class App { return res.json(results) }) + // ---------------------------------------- + // Configuration + // ---------------------------------------- + + this.app.get('/api/v1/flow-config/:id', async (req: Request, res: Response) => { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + const flowData = chatflow.flowData + const parsedFlowData: IReactFlowObject = JSON.parse(flowData) + const nodes = parsedFlowData.nodes + const availableConfigs = findAvailableConfigs(nodes) + return res.json(availableConfigs) + }) + + this.app.post('/api/v1/flow-config/:id', upload.array('files'), async (req: Request, res: Response) => { + const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({ + id: req.params.id + }) + if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`) + await this.validateKey(req, res, chatflow) + + const overrideConfig: ICommonObject = { ...req.body } + const files = req.files as any[] + if (!files || !files.length) return + + for (const file of files) { + const fileData = fs.readFileSync(file.path, { encoding: 'base64' }) + const dataBase64String = `data:${file.mimetype};base64,${fileData},filename:${file.filename}` + + const fileInputField = mapMimeTypeToInputField(file.mimetype) + if (overrideConfig[fileInputField]) { + overrideConfig[fileInputField] = JSON.stringify([...JSON.parse(overrideConfig[fileInputField]), dataBase64String]) + } else { + overrideConfig[fileInputField] = JSON.stringify([dataBase64String]) + } + } + return res.json(overrideConfig) + }) + // ---------------------------------------- // Prediction // ---------------------------------------- @@ -281,6 +329,20 @@ export class App { }) } + async validateKey(req: Request, res: Response, chatflow: ChatFlow) { + const chatFlowApiKeyId = chatflow.apikeyid + const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? '' + + if (chatFlowApiKeyId && !authorizationHeader) return res.status(401).send(`Unauthorized`) + + const suppliedKey = authorizationHeader.split(`Bearer `).pop() + if (chatFlowApiKeyId && suppliedKey) { + const keys = await getAPIKeys() + const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret + if (!compareKeys(apiSecret, suppliedKey)) return res.status(401).send(`Unauthorized`) + } + } + async processPrediction(req: Request, res: Response, isInternal = false) { try { const chatflowid = req.params.id @@ -294,27 +356,19 @@ export class App { if (!chatflow) return res.status(404).send(`Chatflow ${chatflowid} not found`) if (!isInternal) { - const chatFlowApiKeyId = chatflow.apikeyid - const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? '' - - if (chatFlowApiKeyId && !authorizationHeader) return res.status(401).send(`Unauthorized`) - - const suppliedKey = authorizationHeader.split(`Bearer `).pop() - if (chatFlowApiKeyId && suppliedKey) { - const keys = await getAPIKeys() - const apiSecret = keys.find((key) => key.id === chatFlowApiKeyId)?.apiSecret - if (!compareKeys(apiSecret, suppliedKey)) return res.status(401).send(`Unauthorized`) - } + await this.validateKey(req, res, chatflow) } - /* Check if: + /* Don't rebuild the flow (to avoid duplicated upsert, recomputation) when all these conditions met: * - Node Data already exists in pool * - Still in sync (i.e the flow has not been modified since) + * - Existing overrideConfig and new overrideConfig are the same * - Flow doesn't start with nodes that depend on incomingInput.question ***/ if ( Object.prototype.hasOwnProperty.call(this.chatflowPool.activeChatflows, chatflowid) && this.chatflowPool.activeChatflows[chatflowid].inSync && + isSameOverrideConfig(this.chatflowPool.activeChatflows[chatflowid].overrideConfig, incomingInput.overrideConfig) && !isStartNodeDependOnInput(this.chatflowPool.activeChatflows[chatflowid].startingNodes) ) { nodeToExecuteData = this.chatflowPool.activeChatflows[chatflowid].endingNodeData @@ -359,7 +413,8 @@ export class App { graph, depthQueue, this.nodesPool.componentNodes, - incomingInput.question + incomingInput.question, + incomingInput?.overrideConfig ) const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId) @@ -369,7 +424,7 @@ export class App { nodeToExecuteData = reactFlowNodeData const startingNodes = nodes.filter((nd) => startingNodeIds.includes(nd.id)) - this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes) + this.chatflowPool.add(chatflowid, nodeToExecuteData, startingNodes, incomingInput?.overrideConfig) } const nodeInstanceFilePath = this.nodesPool.componentNodes[nodeToExecuteData.name].filePath as string diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 42510f944..80c017e15 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -11,7 +11,8 @@ import { IReactFlowEdge, IReactFlowNode, IVariableDict, - INodeData + INodeData, + IOverrideConfig } from '../Interface' import { cloneDeep, get } from 'lodash' import { ICommonObject, getInputVariables } from 'flowise-components' @@ -180,7 +181,8 @@ export const buildLangchain = async ( graph: INodeDirectedGraph, depthQueue: IDepthQueue, componentNodes: IComponentNodes, - question: string + question: string, + overrideConfig?: ICommonObject ) => { const flowNodes = cloneDeep(reactFlowNodes) @@ -208,7 +210,9 @@ export const buildLangchain = async ( const nodeModule = await import(nodeInstanceFilePath) const newNodeInstance = new nodeModule.nodeClass() - const reactFlowNodeData: INodeData = resolveVariables(reactFlowNode.data, flowNodes, question) + let flowNodeData = cloneDeep(reactFlowNode.data) + if (overrideConfig) flowNodeData = replaceInputsWithConfig(flowNodeData, overrideConfig) + const reactFlowNodeData: INodeData = resolveVariables(flowNodeData, flowNodes, question) flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData, question) } catch (e: any) { @@ -342,7 +346,24 @@ export const resolveVariables = (reactFlowNodeData: INodeData, reactFlowNodes: I } } - const paramsObj = (flowNodeData as any)[types] + const paramsObj = flowNodeData[types] ?? {} + + getParamValues(paramsObj) + + return flowNodeData +} + +export const replaceInputsWithConfig = (flowNodeData: INodeData, overrideConfig: ICommonObject) => { + const types = 'inputs' + + const getParamValues = (paramsObj: ICommonObject) => { + for (const key in paramsObj) { + const paramValue: string = paramsObj[key] + paramsObj[key] = overrideConfig[key] ?? paramValue + } + } + + const paramsObj = flowNodeData[types] ?? {} getParamValues(paramsObj) @@ -365,6 +386,24 @@ export const isStartNodeDependOnInput = (startingNodes: IReactFlowNode[]): boole return false } +/** + * Rebuild flow if new override config is provided + * @param {ICommonObject} existingOverrideConfig + * @param {ICommonObject} newOverrideConfig + * @returns {boolean} + */ +export const isSameOverrideConfig = (existingOverrideConfig?: ICommonObject, newOverrideConfig?: ICommonObject): boolean => { + if ( + existingOverrideConfig && + Object.keys(existingOverrideConfig).length && + newOverrideConfig && + Object.keys(newOverrideConfig).length && + JSON.stringify(existingOverrideConfig) === JSON.stringify(newOverrideConfig) + ) + return true + return false +} + /** * Returns the api key path * @returns {string} @@ -480,3 +519,67 @@ export const deleteAPIKey = async (keyIdToDelete: string): Promise} + */ +export const mapMimeTypeToInputField = (mimeType: string) => { + switch (mimeType) { + case 'text/plain': + return 'txtFile' + case 'application/pdf': + return 'pdfFile' + case 'application/json': + return 'jsonFile' + case 'text/csv': + return 'csvFile' + case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': + return 'docxFile' + default: + return '' + } +} + +/** + * Find all available inpur params config + * @param {IReactFlowNode[]} reactFlowNodes + * @returns {Promise} + */ +export const findAvailableConfigs = (reactFlowNodes: IReactFlowNode[]) => { + const configs: IOverrideConfig[] = [] + + for (const flowNode of reactFlowNodes) { + for (const inputParam of flowNode.data.inputParams) { + let obj: IOverrideConfig + if (inputParam.type === 'password' || inputParam.type === 'options') { + obj = { + node: flowNode.data.label, + label: inputParam.label, + name: inputParam.name, + type: 'string' + } + } else if (inputParam.type === 'file') { + obj = { + node: flowNode.data.label, + label: inputParam.label, + name: 'files', + type: inputParam.fileType ?? inputParam.type + } + } else { + obj = { + node: flowNode.data.label, + label: inputParam.label, + name: inputParam.name, + type: inputParam.type + } + } + if (!configs.some((config) => JSON.stringify(config) === JSON.stringify(obj))) { + configs.push(obj) + } + } + } + + return configs +} diff --git a/packages/ui/src/api/config.js b/packages/ui/src/api/config.js new file mode 100644 index 000000000..0fb8297df --- /dev/null +++ b/packages/ui/src/api/config.js @@ -0,0 +1,7 @@ +import client from './client' + +const getConfig = (id) => client.get(`/flow-config/${id}`) + +export default { + getConfig +} diff --git a/packages/ui/src/ui-component/checkbox/Checkbox.js b/packages/ui/src/ui-component/checkbox/Checkbox.js new file mode 100644 index 000000000..9c16a43a9 --- /dev/null +++ b/packages/ui/src/ui-component/checkbox/Checkbox.js @@ -0,0 +1,34 @@ +import { useState } from 'react' +import PropTypes from 'prop-types' +import { FormControlLabel, Checkbox } from '@mui/material' + +export const CheckboxInput = ({ value, label, onChange, disabled = false }) => { + const [myValue, setMyValue] = useState(value) + + return ( + <> + { + setMyValue(event.target.checked) + onChange(event.target.checked) + }} + /> + } + label={label} + /> + + ) +} + +CheckboxInput.propTypes = { + value: PropTypes.bool, + label: PropTypes.string, + onChange: PropTypes.func, + disabled: PropTypes.bool +} diff --git a/packages/ui/src/ui-component/dialog/APICodeDialog.js b/packages/ui/src/ui-component/dialog/APICodeDialog.js index c72148ecc..f160c44dc 100644 --- a/packages/ui/src/ui-component/dialog/APICodeDialog.js +++ b/packages/ui/src/ui-component/dialog/APICodeDialog.js @@ -22,9 +22,12 @@ import cURLSVG from 'assets/images/cURL.svg' // API import apiKeyApi from 'api/apikey' import chatflowsApi from 'api/chatflows' +import configApi from 'api/config' // Hooks import useApi from 'hooks/useApi' +import { CheckboxInput } from 'ui-component/checkbox/Checkbox' +import { TableViewOnly } from 'ui-component/table/Table' function TabPanel(props) { const { children, value, index, ...other } = props @@ -54,6 +57,66 @@ function a11yProps(index) { } } +const unshiftFiles = (configData) => { + const filesConfig = configData.find((config) => config.name === 'files') + if (filesConfig) { + configData = configData.filter((config) => config.name !== 'files') + configData.unshift(filesConfig) + } + return configData +} + +const getFormDataExamplesForJS = (configData) => { + let finalStr = '' + configData = unshiftFiles(configData) + const loop = Math.min(configData.length, 4) + for (let i = 0; i < loop; i += 1) { + const config = configData[i] + let exampleVal = `"example"` + if (config.type === 'string') exampleVal = `"example"` + else if (config.type === 'boolean') exampleVal = `true` + else if (config.type === 'number') exampleVal = `1` + else if (config.name === 'files') exampleVal = `input.files[0]` + finalStr += `formData.append("${config.name}", ${exampleVal})\n` + } + return finalStr +} + +const getFormDataExamplesForPython = (configData) => { + let finalStr = '' + configData = unshiftFiles(configData) + const loop = Math.min(configData.length, 4) + for (let i = 0; i < loop; i += 1) { + const config = configData[i] + let exampleVal = `"example"` + if (config.type === 'string') exampleVal = `"example"` + else if (config.type === 'boolean') exampleVal = `true` + else if (config.type === 'number') exampleVal = `1` + else if (config.name === 'files') exampleVal = `('example${config.type}', open('example${config.type}', 'rb'))` + finalStr += `\n "${config.name}": ${exampleVal}` + if (i === loop - 1) finalStr += `\n` + } + return finalStr +} + +const getFormDataExamplesForCurl = (configData) => { + let finalStr = '' + configData = unshiftFiles(configData) + const loop = Math.min(configData.length, 4) + for (let i = 0; i < loop; i += 1) { + const config = configData[i] + let exampleVal = `example` + if (config.type === 'string') exampleVal = `example` + else if (config.type === 'boolean') exampleVal = `true` + else if (config.type === 'number') exampleVal = `1` + else if (config.name === 'files') exampleVal = `@/home/user1/Desktop/example${config.type}` + finalStr += `\n -F "${config.name}=${exampleVal}"` + if (i === loop - 1) finalStr += `)\n` + else finalStr += ` \\` + } + return finalStr +} + const APICodeDialog = ({ show, dialogProps, onCancel }) => { const portalElement = document.getElementById('portal') const navigate = useNavigate() @@ -64,9 +127,18 @@ const APICodeDialog = ({ show, dialogProps, onCancel }) => { const [apiKeys, setAPIKeys] = useState([]) const [chatflowApiKeyId, setChatflowApiKeyId] = useState('') const [selectedApiKey, setSelectedApiKey] = useState({}) + const [checkboxVal, setCheckbox] = useState(false) const getAllAPIKeysApi = useApi(apiKeyApi.getAllAPIKeys) const updateChatflowApi = useApi(chatflowsApi.updateChatflow) + const getConfigApi = useApi(configApi.getConfig) + + const onCheckBoxChanged = (newVal) => { + setCheckbox(newVal) + if (newVal) { + getConfigApi.request(dialogProps.chatflowid) + } + } const onApiKeySelected = (keyValue) => { if (keyValue === 'addnewkey') { @@ -106,7 +178,7 @@ output = query({ }) ` } else if (codeLang === 'JavaScript') { - return `async function query(data) { + return `async function query() { const response = await fetch( "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", { @@ -190,6 +262,147 @@ output = query({ return pythonSVG } + const getConfigCode = (codeLang, configData) => { + if (codeLang === 'Python') { + return `import requests +form_data = {${getFormDataExamplesForPython(configData)}} + +def setConfig(): + response = requests.post("${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}", files=form_data) + return response.json() + +def query(payload): + response = requests.post("${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", json=payload) + return response.json() + +# Set initial config +config = setConfig() + +# Run prediction with config +output = query({ + "question": "Hey, how are you?", + "overrideConfig": config +}) +` + } else if (codeLang === 'JavaScript') { + return `let formData = new FormData(); +${getFormDataExamplesForJS(configData)} +async function setConfig() { + const response = await fetch( + "${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}", + { + method: "POST", + body: formData + } + ); + const config = await response.json(); + return config; //Returns a config object +} + +async function query(config) { + const response = await fetch( + "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", + { + method: "POST", + body: { + "question": "Hey, how are you?", + "overrideConfig": config + }, + } + ); + const result = await response.json(); + return result; +} + +// Set initial config +const config = await setConfig() + +// Run prediction with config +const res = await query(config) +` + } else if (codeLang === 'cURL') { + return `CONFIG=$(curl ${baseURL}/api/v1/flow-config/${dialogProps.chatflowid} \\ + -X POST \\${getFormDataExamplesForCurl(configData)} +curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ + -X POST \\ + -d '{"question": "Hey, how are you?", "overrideConfig": $CONFIG}'` + } + return '' + } + + const getConfigCodeWithAuthorization = (codeLang, configData) => { + if (codeLang === 'Python') { + return `import requests +form_data = {${getFormDataExamplesForPython(configData)}} +headers = {"Authorization": "Bearer ${selectedApiKey?.apiKey}"} + +def setConfig(): + response = requests.post("${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}", headers=headers, files=form_data) + return response.json() + +def query(payload): + response = requests.post("${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", headers=headers, json=payload) + return response.json() + +# Set initial config +config = setConfig() + +# Run prediction with config +output = query({ + "question": "Hey, how are you?", + "overrideConfig": config +}) +` + } else if (codeLang === 'JavaScript') { + return `let formData = new FormData(); +${getFormDataExamplesForJS(configData)} +async function setConfig() { + const response = await fetch( + "${baseURL}/api/v1/flow-config/${dialogProps.chatflowid}", + { + headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" }, + method: "POST", + body: formData + } + ); + const config = await response.json(); + return config; //Returns a config object +} + +async function query(config) { + const response = await fetch( + "${baseURL}/api/v1/prediction/${dialogProps.chatflowid}", + { + headers: { Authorization: "Bearer ${selectedApiKey?.apiKey}" }, + method: "POST", + body: { + "question": "Hey, how are you?", + "overrideConfig": config + }, + } + ); + const result = await response.json(); + return result; +} + +// Set initial config +const config = await setConfig() + +// Run prediction with config +const res = await query(config) +` + } else if (codeLang === 'cURL') { + return `CONFIG=$(curl ${baseURL}/api/v1/flow-config/${dialogProps.chatflowid} \\ + -X POST \\ + -H "Authorization: Bearer ${selectedApiKey?.apiKey}"\\${getFormDataExamplesForCurl(configData)} +curl ${baseURL}/api/v1/prediction/${dialogProps.chatflowid} \\ + -X POST \\ + -H "Authorization: Bearer ${selectedApiKey?.apiKey}" + -d '{"question": "Hey, how are you?", "overrideConfig": $CONFIG}'` + } + return '' + } + useEffect(() => { if (getAllAPIKeysApi.data) { const options = [ @@ -219,7 +432,9 @@ output = query({ }, [dialogProps, getAllAPIKeysApi.data]) useEffect(() => { - if (show) getAllAPIKeysApi.request() + if (show) { + getAllAPIKeysApi.request() + } // eslint-disable-next-line react-hooks/exhaustive-deps }, [show]) @@ -277,6 +492,23 @@ output = query({ showLineNumbers={false} wrapLines /> + + {checkboxVal && getConfigApi.data && getConfigApi.data.length > 0 && ( + <> + + + + )} ))} diff --git a/packages/ui/src/ui-component/file/File.js b/packages/ui/src/ui-component/file/File.js index 180b7f03a..93e1bb91f 100644 --- a/packages/ui/src/ui-component/file/File.js +++ b/packages/ui/src/ui-component/file/File.js @@ -10,25 +10,48 @@ export const File = ({ value, fileType, onChange, disabled = false }) => { const [myValue, setMyValue] = useState(value ?? '') - const handleFileUpload = (e) => { + const handleFileUpload = async (e) => { if (!e.target.files) return - const file = e.target.files[0] - const { name } = file + if (e.target.files.length === 1) { + const file = e.target.files[0] + const { name } = file - const reader = new FileReader() - reader.onload = (evt) => { - if (!evt?.target?.result) { - return + const reader = new FileReader() + reader.onload = (evt) => { + if (!evt?.target?.result) { + return + } + const { result } = evt.target + + const value = result + `,filename:${name}` + + setMyValue(value) + onChange(value) } - const { result } = evt.target + reader.readAsDataURL(file) + } else if (e.target.files.length > 0) { + let files = Array.from(e.target.files).map((file) => { + const reader = new FileReader() + const { name } = file - const value = result + `,filename:${name}` + return new Promise((resolve) => { + reader.onload = (evt) => { + if (!evt?.target?.result) { + return + } + const { result } = evt.target + const value = result + `,filename:${name}` + resolve(value) + } + reader.readAsDataURL(file) + }) + }) - setMyValue(value) - onChange(value) + const res = await Promise.all(files) + setMyValue(JSON.stringify(res)) + onChange(JSON.stringify(res)) } - reader.readAsDataURL(file) } return ( @@ -51,7 +74,7 @@ export const File = ({ value, fileType, onChange, disabled = false }) => { sx={{ marginRight: '1rem' }} > {'Upload File'} - handleFileUpload(e)} /> + handleFileUpload(e)} /> ) diff --git a/packages/ui/src/ui-component/table/Table.js b/packages/ui/src/ui-component/table/Table.js new file mode 100644 index 000000000..a6ab312e1 --- /dev/null +++ b/packages/ui/src/ui-component/table/Table.js @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types' +import { TableContainer, Table, TableHead, TableCell, TableRow, TableBody, Paper } from '@mui/material' + +export const TableViewOnly = ({ columns, rows }) => { + return ( + <> + + + + + {columns.map((col, index) => ( + {col.charAt(0).toUpperCase() + col.slice(1)} + ))} + + + + {rows.map((row, index) => ( + + {Object.keys(row).map((key, index) => ( + {row[key]} + ))} + + ))} + +
+
+ + ) +} + +TableViewOnly.propTypes = { + rows: PropTypes.array, + columns: PropTypes.array +} diff --git a/packages/ui/src/utils/genericHelper.js b/packages/ui/src/utils/genericHelper.js index 199373754..c1dcb1086 100644 --- a/packages/ui/src/utils/genericHelper.js +++ b/packages/ui/src/utils/genericHelper.js @@ -206,9 +206,20 @@ export const convertDateStringToDateObject = (dateString) => { } export const getFileName = (fileBase64) => { - const splitDataURI = fileBase64.split(',') - const filename = splitDataURI[splitDataURI.length - 1].split(':')[1] - return filename + let fileNames = [] + if (fileBase64.startsWith('[') && fileBase64.endsWith(']')) { + const files = JSON.parse(fileBase64) + for (const file of files) { + const splitDataURI = file.split(',') + const filename = splitDataURI[splitDataURI.length - 1].split(':')[1] + fileNames.push(filename) + } + return fileNames.join(', ') + } else { + const splitDataURI = fileBase64.split(',') + const filename = splitDataURI[splitDataURI.length - 1].split(':')[1] + return filename + } } export const getFolderName = (base64ArrayStr) => {