diff --git a/packages/components/nodes/tools/CustomTool/core.ts b/packages/components/nodes/tools/CustomTool/core.ts index 12dd72f19..2aa06b547 100644 --- a/packages/components/nodes/tools/CustomTool/core.ts +++ b/packages/components/nodes/tools/CustomTool/core.ts @@ -2,37 +2,7 @@ import { z } from 'zod' import { CallbackManagerForToolRun } from 'langchain/callbacks' import { StructuredTool, ToolParams } from 'langchain/tools' import { NodeVM } from 'vm2' - -/* - * List of dependencies allowed to be import in vm2 - */ -const availableDependencies = [ - '@dqbd/tiktoken', - '@getzep/zep-js', - '@huggingface/inference', - '@pinecone-database/pinecone', - '@supabase/supabase-js', - 'axios', - 'cheerio', - 'chromadb', - 'cohere-ai', - 'd3-dsv', - 'form-data', - 'graphql', - 'html-to-text', - 'langchain', - 'linkifyjs', - 'mammoth', - 'moment', - 'node-fetch', - 'pdf-parse', - 'pdfjs-dist', - 'playwright', - 'puppeteer', - 'srt-parser-2', - 'typeorm', - 'weaviate-ts-client' -] +import { availableDependencies } from '../../../src/utils' export interface BaseDynamicToolInput extends ToolParams { name: string diff --git a/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts new file mode 100644 index 000000000..b358b24b3 --- /dev/null +++ b/packages/components/nodes/utilities/CustomFunction/CustomFunction.ts @@ -0,0 +1,124 @@ +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' +import { NodeVM } from 'vm2' +import { availableDependencies, handleEscapeCharacters } from '../../../src/utils' + +class CustomFunction_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Custom JS Function' + this.name = 'customFunction' + this.version = 1.0 + this.type = 'CustomFunction' + this.icon = 'customfunction.svg' + this.category = 'Utilities' + this.description = `Execute custom javascript function` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Input Variables', + name: 'functionInputVariables', + description: 'Input variables can be used in the function with prefix $. For example: $var', + type: 'json', + optional: true, + acceptVariable: true, + list: true + }, + { + label: 'Function Name', + name: 'functionName', + type: 'string', + optional: true, + placeholder: 'My Function' + }, + { + label: 'Javascript Function', + name: 'javascriptFunction', + type: 'code' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData, input: string): Promise { + const javascriptFunction = nodeData.inputs?.javascriptFunction as string + const functionInputVariablesRaw = nodeData.inputs?.functionInputVariables + + let inputVars: ICommonObject = {} + if (functionInputVariablesRaw) { + try { + inputVars = + typeof functionInputVariablesRaw === 'object' ? functionInputVariablesRaw : JSON.parse(functionInputVariablesRaw) + } catch (exception) { + throw new Error("Invalid JSON in the PromptTemplate's promptValues: " + exception) + } + } + + let sandbox: any = { $input: input } + + if (Object.keys(inputVars).length) { + for (const item in inputVars) { + sandbox[`$${item}`] = inputVars[item] + } + } + + const defaultAllowBuiltInDep = [ + 'assert', + 'buffer', + 'crypto', + 'events', + 'http', + 'https', + 'net', + 'path', + 'querystring', + 'timers', + 'tls', + 'url', + 'zlib' + ] + + const builtinDeps = process.env.TOOL_FUNCTION_BUILTIN_DEP + ? defaultAllowBuiltInDep.concat(process.env.TOOL_FUNCTION_BUILTIN_DEP.split(',')) + : defaultAllowBuiltInDep + const externalDeps = process.env.TOOL_FUNCTION_EXTERNAL_DEP ? process.env.TOOL_FUNCTION_EXTERNAL_DEP.split(',') : [] + const deps = availableDependencies.concat(externalDeps) + + const nodeVMOptions = { + console: 'inherit', + sandbox, + require: { + external: { modules: deps }, + builtin: builtinDeps + } + } as any + + const vm = new NodeVM(nodeVMOptions) + try { + const response = await vm.run(`module.exports = async function() {${javascriptFunction}}()`, __dirname) + if (typeof response === 'string') { + return handleEscapeCharacters(response, false) + } + return response + } catch (e) { + throw new Error(e) + } + } +} + +module.exports = { nodeClass: CustomFunction_Utilities } diff --git a/packages/components/nodes/utilities/CustomFunction/customfunction.svg b/packages/components/nodes/utilities/CustomFunction/customfunction.svg new file mode 100644 index 000000000..bf60fcae7 --- /dev/null +++ b/packages/components/nodes/utilities/CustomFunction/customfunction.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/utilities/GetVariable/GetVariable.ts b/packages/components/nodes/utilities/GetVariable/GetVariable.ts new file mode 100644 index 000000000..dde5a2d96 --- /dev/null +++ b/packages/components/nodes/utilities/GetVariable/GetVariable.ts @@ -0,0 +1,52 @@ +import { ICommonObject, INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class GetVariable_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Get Variable' + this.name = 'getVariable' + this.version = 1.0 + this.type = 'GetVariable' + this.icon = 'getvar.svg' + this.category = 'Utilities' + this.description = `Get variable that was saved using Set Variable node` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Variable Name', + name: 'variableName', + type: 'string', + placeholder: 'var1' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { + const variableName = nodeData.inputs?.variableName as string + const dynamicVars = options.dynamicVariables as Record + + if (Object.prototype.hasOwnProperty.call(dynamicVars, variableName)) { + return dynamicVars[variableName] + } + return undefined + } +} + +module.exports = { nodeClass: GetVariable_Utilities } diff --git a/packages/components/nodes/utilities/GetVariable/getvar.svg b/packages/components/nodes/utilities/GetVariable/getvar.svg new file mode 100644 index 000000000..49e27ab13 --- /dev/null +++ b/packages/components/nodes/utilities/GetVariable/getvar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/nodes/utilities/SetVariable/SetVariable.ts b/packages/components/nodes/utilities/SetVariable/SetVariable.ts new file mode 100644 index 000000000..8542668ca --- /dev/null +++ b/packages/components/nodes/utilities/SetVariable/SetVariable.ts @@ -0,0 +1,56 @@ +import { INode, INodeData, INodeOutputsValue, INodeParams } from '../../../src/Interface' + +class SetVariable_Utilities implements INode { + label: string + name: string + version: number + description: string + type: string + icon: string + category: string + baseClasses: string[] + inputs: INodeParams[] + outputs: INodeOutputsValue[] + + constructor() { + this.label = 'Set Variable' + this.name = 'setVariable' + this.version = 1.0 + this.type = 'SetVariable' + this.icon = 'setvar.svg' + this.category = 'Utilities' + this.description = `Set variable which can be retrieved at a later stage. Variable is only available during runtime.` + this.baseClasses = [this.type, 'Utilities'] + this.inputs = [ + { + label: 'Input', + name: 'input', + type: 'string | number | boolean | json | array', + optional: true, + list: true + }, + { + label: 'Variable Name', + name: 'variableName', + type: 'string', + placeholder: 'var1' + } + ] + this.outputs = [ + { + label: 'Output', + name: 'output', + baseClasses: ['string', 'number', 'boolean', 'json', 'array'] + } + ] + } + + async init(nodeData: INodeData): Promise { + const inputRaw = nodeData.inputs?.input + const variableName = nodeData.inputs?.variableName as string + + return { output: inputRaw, dynamicVariables: { [variableName]: inputRaw } } + } +} + +module.exports = { nodeClass: SetVariable_Utilities } diff --git a/packages/components/nodes/utilities/SetVariable/setvar.svg b/packages/components/nodes/utilities/SetVariable/setvar.svg new file mode 100644 index 000000000..c8d643c9b --- /dev/null +++ b/packages/components/nodes/utilities/SetVariable/setvar.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/components/src/utils.ts b/packages/components/src/utils.ts index 404f7c75d..239b13ca8 100644 --- a/packages/components/src/utils.ts +++ b/packages/components/src/utils.ts @@ -12,6 +12,63 @@ import { AIMessage, HumanMessage } from 'langchain/schema' export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}} export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank +/* + * List of dependencies allowed to be import in vm2 + */ +export const availableDependencies = [ + '@aws-sdk/client-bedrock-runtime', + '@aws-sdk/client-dynamodb', + '@aws-sdk/client-s3', + '@elastic/elasticsearch', + '@dqbd/tiktoken', + '@getzep/zep-js', + '@gomomento/sdk', + '@gomomento/sdk-core', + '@google-ai/generativelanguage', + '@huggingface/inference', + '@notionhq/client', + '@opensearch-project/opensearch', + '@pinecone-database/pinecone', + '@qdrant/js-client-rest', + '@supabase/supabase-js', + '@upstash/redis', + '@zilliz/milvus2-sdk-node', + 'apify-client', + 'axios', + 'cheerio', + 'chromadb', + 'cohere-ai', + 'd3-dsv', + 'faiss-node', + 'form-data', + 'google-auth-library', + 'graphql', + 'html-to-text', + 'ioredis', + 'langchain', + 'langfuse', + 'langsmith', + 'linkifyjs', + 'llmonitor', + 'mammoth', + 'moment', + 'mongodb', + 'mysql2', + 'node-fetch', + 'node-html-markdown', + 'notion-to-md', + 'openai', + 'pdf-parse', + 'pdfjs-dist', + 'pg', + 'playwright', + 'puppeteer', + 'redis', + 'replicate', + 'srt-parser-2', + 'typeorm', + 'weaviate-ts-client' +] /** * Get base classes of components diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index fb4a5f5a0..85e9eac45 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -281,6 +281,28 @@ export class App { } }) + // execute custom function node + this.app.post('/api/v1/node-custom-function', async (req: Request, res: Response) => { + const body = req.body + const nodeData = { inputs: body } + if (Object.prototype.hasOwnProperty.call(this.nodesPool.componentNodes, 'customFunction')) { + try { + const nodeInstanceFilePath = this.nodesPool.componentNodes['customFunction'].filePath as string + const nodeModule = await import(nodeInstanceFilePath) + const newNodeInstance = new nodeModule.nodeClass() + + const returnOptions: INodeOptionsValue[] = await newNodeInstance.init(nodeData) + + return res.json(returnOptions) + } catch (error) { + return res.status(500).send(`Error running custom function: ${error}`) + } + } else { + res.status(404).send(`Node customFunction not found`) + return + } + }) + // ---------------------------------------- // Chatflows // ---------------------------------------- diff --git a/packages/server/src/utils/index.ts b/packages/server/src/utils/index.ts index 2bf1c04a4..d411050c5 100644 --- a/packages/server/src/utils/index.ts +++ b/packages/server/src/utils/index.ts @@ -231,6 +231,7 @@ export const buildLangchain = async ( // Create a Queue and add our initial node in it const nodeQueue = [] as INodeQueue[] const exploredNode = {} as IExploredNode + const dynamicVariables = {} as Record // In the case of infinite loop, only max 3 loops will be executed const maxLoop = 3 @@ -267,20 +268,36 @@ export const buildLangchain = async ( appDataSource, databaseEntities, logger, - cachePool + cachePool, + dynamicVariables }) logger.debug(`[server]: Finished upserting ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) break } else { logger.debug(`[server]: Initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) - flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData, question, { + let outputResult = await newNodeInstance.init(reactFlowNodeData, question, { chatId, chatflowid, appDataSource, databaseEntities, logger, - cachePool + cachePool, + dynamicVariables }) + + // Save dynamic variables + if (reactFlowNode.data.name === 'setVariable') { + const dynamicVars = outputResult?.dynamicVariables ?? {} + + for (const variableKey in dynamicVars) { + dynamicVariables[variableKey] = dynamicVars[variableKey] + } + + outputResult = outputResult?.output + } + + flowNodes[nodeIndex].data.instance = outputResult + logger.debug(`[server]: Finished initializing ${reactFlowNode.data.label} (${reactFlowNode.data.id})`) } } catch (e: any) { diff --git a/packages/ui/package.json b/packages/ui/package.json index 7a739978e..2aed7d977 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -8,13 +8,20 @@ "email": "henryheng@flowiseai.com" }, "dependencies": { + "@codemirror/lang-javascript": "^6.2.1", + "@codemirror/lang-json": "^6.0.1", + "@codemirror/view": "^6.22.3", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.10.6", "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.0.3", - "@mui/material": "^5.11.12", + "@mui/lab": "^5.0.0-alpha.156", + "@mui/material": "^5.15.0", "@mui/x-data-grid": "^6.8.0", "@tabler/icons": "^1.39.1", + "@uiw/codemirror-theme-sublime": "^4.21.21", + "@uiw/codemirror-theme-vscode": "^4.21.21", + "@uiw/react-codemirror": "^4.21.21", "clsx": "^1.1.1", "flowise-embed": "*", "flowise-embed-react": "*", @@ -26,7 +33,6 @@ "lodash": "^4.17.21", "moment": "^2.29.3", "notistack": "^2.0.4", - "prismjs": "^1.28.0", "prop-types": "^15.7.2", "react": "^18.2.0", "react-code-blocks": "^0.0.9-0", @@ -39,7 +45,6 @@ "react-redux": "^8.0.5", "react-router": "~6.3.0", "react-router-dom": "~6.3.0", - "react-simple-code-editor": "^0.11.2", "react-syntax-highlighter": "^15.5.0", "reactflow": "^11.5.6", "redux": "^4.0.5", diff --git a/packages/ui/src/api/nodes.js b/packages/ui/src/api/nodes.js index 7eb4c3518..3b7eacc5e 100644 --- a/packages/ui/src/api/nodes.js +++ b/packages/ui/src/api/nodes.js @@ -4,7 +4,10 @@ const getAllNodes = () => client.get('/nodes') const getSpecificNode = (name) => client.get(`/nodes/${name}`) +const executeCustomFunctionNode = (body) => client.post(`/node-custom-function`, body) + export default { getAllNodes, - getSpecificNode + getSpecificNode, + executeCustomFunctionNode } diff --git a/packages/ui/src/ui-component/dialog/ExpandTextDialog.js b/packages/ui/src/ui-component/dialog/ExpandTextDialog.js index 2a4ec4f5a..0ef70e29e 100644 --- a/packages/ui/src/ui-component/dialog/ExpandTextDialog.js +++ b/packages/ui/src/ui-component/dialog/ExpandTextDialog.js @@ -2,14 +2,24 @@ import { createPortal } from 'react-dom' import { useState, useEffect } from 'react' import { useSelector, useDispatch } from 'react-redux' import PropTypes from 'prop-types' +import PerfectScrollbar from 'react-perfect-scrollbar' + +// MUI import { Button, Dialog, DialogActions, DialogContent, Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' -import PerfectScrollbar from 'react-perfect-scrollbar' +import { LoadingButton } from '@mui/lab' + +// Project Import import { StyledButton } from 'ui-component/button/StyledButton' -import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor' -import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor' +import { CodeEditor } from 'ui-component/editor/CodeEditor' + +// Store import { HIDE_CANVAS_DIALOG, SHOW_CANVAS_DIALOG } from 'store/actions' +// API +import nodesApi from 'api/nodes' +import useApi from 'hooks/useApi' + import './ExpandTextDialog.css' const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { @@ -18,18 +28,30 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { const theme = useTheme() const dispatch = useDispatch() const customization = useSelector((state) => state.customization) - const languageType = 'json' const [inputValue, setInputValue] = useState('') const [inputParam, setInputParam] = useState(null) + const [languageType, setLanguageType] = useState('json') + const [loading, setLoading] = useState(false) + const [codeExecutedResult, setCodeExecutedResult] = useState('') + + const executeCustomFunctionNodeApi = useApi(nodesApi.executeCustomFunctionNode) useEffect(() => { if (dialogProps.value) setInputValue(dialogProps.value) - if (dialogProps.inputParam) setInputParam(dialogProps.inputParam) + if (dialogProps.inputParam) { + setInputParam(dialogProps.inputParam) + if (dialogProps.inputParam.type === 'code') { + setLanguageType('js') + } + } return () => { setInputValue('') + setLoading(false) setInputParam(null) + setLanguageType('json') + setCodeExecutedResult('') } }, [dialogProps]) @@ -39,11 +61,31 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { return () => dispatch({ type: HIDE_CANVAS_DIALOG }) }, [show, dispatch]) + useEffect(() => { + setLoading(executeCustomFunctionNodeApi.loading) + }, [executeCustomFunctionNodeApi.loading]) + + useEffect(() => { + if (executeCustomFunctionNodeApi.data) { + setCodeExecutedResult(executeCustomFunctionNodeApi.data) + } + }, [executeCustomFunctionNodeApi.data]) + + useEffect(() => { + if (executeCustomFunctionNodeApi.error) { + if (typeof executeCustomFunctionNodeApi.error === 'object' && executeCustomFunctionNodeApi.error?.response?.data) { + setCodeExecutedResult(executeCustomFunctionNodeApi.error?.response?.data) + } else if (typeof executeCustomFunctionNodeApi.error === 'string') { + setCodeExecutedResult(executeCustomFunctionNodeApi.error) + } + } + }, [executeCustomFunctionNodeApi.error]) + const component = show ? (
- {inputParam && inputParam.type === 'string' && ( + {inputParam && (inputParam.type === 'string' || inputParam.type === 'code') && (
{inputParam.label} @@ -54,42 +96,66 @@ const ExpandTextDialog = ({ show, dialogProps, onCancel, onConfirm }) => { borderColor: theme.palette.grey['500'], borderRadius: '12px', height: '100%', - maxHeight: 'calc(100vh - 220px)', + maxHeight: languageType === 'js' ? 'calc(100vh - 250px)' : 'calc(100vh - 220px)', overflowX: 'hidden', backgroundColor: 'white' }} > - {customization.isDarkMode ? ( - setInputValue(code)} - placeholder={inputParam.placeholder} - type={languageType} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%' - }} - /> - ) : ( - setInputValue(code)} - placeholder={inputParam.placeholder} - type={languageType} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%' - }} - /> - )} + setInputValue(code)} + />
)}
+ {languageType === 'js' && ( + { + setLoading(true) + executeCustomFunctionNodeApi.request({ javascriptFunction: inputValue }) + }} + > + Execute + + )} + {codeExecutedResult && ( +
+ +
+ )}
diff --git a/packages/ui/src/ui-component/editor/CodeEditor.js b/packages/ui/src/ui-component/editor/CodeEditor.js new file mode 100644 index 000000000..120e19a01 --- /dev/null +++ b/packages/ui/src/ui-component/editor/CodeEditor.js @@ -0,0 +1,48 @@ +import PropTypes from 'prop-types' +import CodeMirror from '@uiw/react-codemirror' +import { javascript } from '@codemirror/lang-javascript' +import { json } from '@codemirror/lang-json' +import { vscodeDark } from '@uiw/codemirror-theme-vscode' +import { sublime } from '@uiw/codemirror-theme-sublime' +import { EditorView } from '@codemirror/view' + +export const CodeEditor = ({ value, height, theme, lang, placeholder, disabled = false, basicSetup = {}, onValueChange }) => { + const customStyle = EditorView.baseTheme({ + '&': { + color: '#191b1f', + padding: '10px' + }, + '.cm-placeholder': { + color: 'rgba(120, 120, 120, 0.5)' + } + }) + + return ( + + ) +} + +CodeEditor.propTypes = { + value: PropTypes.string, + height: PropTypes.string, + theme: PropTypes.string, + lang: PropTypes.string, + placeholder: PropTypes.string, + disabled: PropTypes.bool, + basicSetup: PropTypes.object, + onValueChange: PropTypes.func +} diff --git a/packages/ui/src/ui-component/editor/DarkCodeEditor.js b/packages/ui/src/ui-component/editor/DarkCodeEditor.js deleted file mode 100644 index bf0719dd9..000000000 --- a/packages/ui/src/ui-component/editor/DarkCodeEditor.js +++ /dev/null @@ -1,43 +0,0 @@ -import Editor from 'react-simple-code-editor' -import { highlight, languages } from 'prismjs/components/prism-core' -import 'prismjs/components/prism-clike' -import 'prismjs/components/prism-javascript' -import 'prismjs/components/prism-json' -import 'prismjs/components/prism-markup' -import './prism-dark.css' -import PropTypes from 'prop-types' -import { useTheme } from '@mui/material/styles' - -export const DarkCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => { - const theme = useTheme() - - return ( - highlight(code, type === 'json' ? languages.json : languages.js)} - padding={10} - onValueChange={onValueChange} - onMouseUp={onMouseUp} - onBlur={onBlur} - tabSize={4} - style={{ - ...style, - background: theme.palette.codeEditor.main - }} - textareaClassName='editor__textarea' - /> - ) -} - -DarkCodeEditor.propTypes = { - value: PropTypes.string, - placeholder: PropTypes.string, - disabled: PropTypes.bool, - type: PropTypes.string, - style: PropTypes.object, - onValueChange: PropTypes.func, - onMouseUp: PropTypes.func, - onBlur: PropTypes.func -} diff --git a/packages/ui/src/ui-component/editor/LightCodeEditor.js b/packages/ui/src/ui-component/editor/LightCodeEditor.js deleted file mode 100644 index 14dcbf29a..000000000 --- a/packages/ui/src/ui-component/editor/LightCodeEditor.js +++ /dev/null @@ -1,43 +0,0 @@ -import Editor from 'react-simple-code-editor' -import { highlight, languages } from 'prismjs/components/prism-core' -import 'prismjs/components/prism-clike' -import 'prismjs/components/prism-javascript' -import 'prismjs/components/prism-json' -import 'prismjs/components/prism-markup' -import './prism-light.css' -import PropTypes from 'prop-types' -import { useTheme } from '@mui/material/styles' - -export const LightCodeEditor = ({ value, placeholder, disabled = false, type, style, onValueChange, onMouseUp, onBlur }) => { - const theme = useTheme() - - return ( - highlight(code, type === 'json' ? languages.json : languages.js)} - padding={10} - onValueChange={onValueChange} - onMouseUp={onMouseUp} - onBlur={onBlur} - tabSize={4} - style={{ - ...style, - background: theme.palette.card.main - }} - textareaClassName='editor__textarea' - /> - ) -} - -LightCodeEditor.propTypes = { - value: PropTypes.string, - placeholder: PropTypes.string, - disabled: PropTypes.bool, - type: PropTypes.string, - style: PropTypes.object, - onValueChange: PropTypes.func, - onMouseUp: PropTypes.func, - onBlur: PropTypes.func -} diff --git a/packages/ui/src/ui-component/editor/prism-dark.css b/packages/ui/src/ui-component/editor/prism-dark.css deleted file mode 100644 index c4bfb4132..000000000 --- a/packages/ui/src/ui-component/editor/prism-dark.css +++ /dev/null @@ -1,275 +0,0 @@ -pre[class*='language-'], -code[class*='language-'] { - color: #d4d4d4; - font-size: 13px; - text-shadow: none; - font-family: Menlo, Monaco, Consolas, 'Andale Mono', 'Ubuntu Mono', 'Courier New', monospace; - direction: ltr; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - line-height: 1.5; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -pre[class*='language-']::selection, -code[class*='language-']::selection, -pre[class*='language-'] *::selection, -code[class*='language-'] *::selection { - text-shadow: none; - background: #264f78; -} - -@media print { - pre[class*='language-'], - code[class*='language-'] { - text-shadow: none; - } -} - -pre[class*='language-'] { - padding: 1em; - margin: 0.5em 0; - overflow: auto; - background: #1e1e1e; -} - -:not(pre) > code[class*='language-'] { - padding: 0.1em 0.3em; - border-radius: 0.3em; - color: #db4c69; - background: #1e1e1e; -} -/********************************************************* -* Tokens -*/ -.namespace { - opacity: 0.7; -} - -.token.doctype .token.doctype-tag { - color: #569cd6; -} - -.token.doctype .token.name { - color: #9cdcfe; -} - -.token.comment, -.token.prolog { - color: #6a9955; -} - -.token.punctuation, -.language-html .language-css .token.punctuation, -.language-html .language-javascript .token.punctuation { - color: #d4d4d4; -} - -.token.property, -.token.tag, -.token.boolean, -.token.number, -.token.constant, -.token.symbol, -.token.inserted, -.token.unit { - color: #b5cea8; -} - -.token.selector, -.token.attr-name, -.token.string, -.token.char, -.token.builtin, -.token.deleted { - color: #ce9178; -} - -.language-css .token.string.url { - text-decoration: underline; -} - -.token.operator, -.token.entity { - color: #d4d4d4; -} - -.token.operator.arrow { - color: #569cd6; -} - -.token.atrule { - color: #ce9178; -} - -.token.atrule .token.rule { - color: #c586c0; -} - -.token.atrule .token.url { - color: #9cdcfe; -} - -.token.atrule .token.url .token.function { - color: #dcdcaa; -} - -.token.atrule .token.url .token.punctuation { - color: #d4d4d4; -} - -.token.keyword { - color: #569cd6; -} - -.token.keyword.module, -.token.keyword.control-flow { - color: #c586c0; -} - -.token.function, -.token.function .token.maybe-class-name { - color: #dcdcaa; -} - -.token.regex { - color: #d16969; -} - -.token.important { - color: #569cd6; -} - -.token.italic { - font-style: italic; -} - -.token.constant { - color: #9cdcfe; -} - -.token.class-name, -.token.maybe-class-name { - color: #4ec9b0; -} - -.token.console { - color: #9cdcfe; -} - -.token.parameter { - color: #9cdcfe; -} - -.token.interpolation { - color: #9cdcfe; -} - -.token.punctuation.interpolation-punctuation { - color: #569cd6; -} - -.token.boolean { - color: #569cd6; -} - -.token.property, -.token.variable, -.token.imports .token.maybe-class-name, -.token.exports .token.maybe-class-name { - color: #9cdcfe; -} - -.token.selector { - color: #d7ba7d; -} - -.token.escape { - color: #d7ba7d; -} - -.token.tag { - color: #569cd6; -} - -.token.tag .token.punctuation { - color: #808080; -} - -.token.cdata { - color: #808080; -} - -.token.attr-name { - color: #9cdcfe; -} - -.token.attr-value, -.token.attr-value .token.punctuation { - color: #ce9178; -} - -.token.attr-value .token.punctuation.attr-equals { - color: #d4d4d4; -} - -.token.entity { - color: #569cd6; -} - -.token.namespace { - color: #4ec9b0; -} -/********************************************************* -* Language Specific -*/ - -pre[class*='language-javascript'], -code[class*='language-javascript'], -pre[class*='language-jsx'], -code[class*='language-jsx'], -pre[class*='language-typescript'], -code[class*='language-typescript'], -pre[class*='language-tsx'], -code[class*='language-tsx'] { - color: #9cdcfe; -} - -pre[class*='language-css'], -code[class*='language-css'] { - color: #ce9178; -} - -pre[class*='language-html'], -code[class*='language-html'] { - color: #d4d4d4; -} - -.language-regex .token.anchor { - color: #dcdcaa; -} - -.language-html .token.punctuation { - color: #808080; -} -/********************************************************* -* Line highlighting -*/ -pre[class*='language-'] > code[class*='language-'] { - position: relative; - z-index: 1; -} - -.line-highlight.line-highlight { - background: #f7ebc6; - box-shadow: inset 5px 0 0 #f7d87c; - z-index: 0; -} diff --git a/packages/ui/src/ui-component/editor/prism-light.css b/packages/ui/src/ui-component/editor/prism-light.css deleted file mode 100644 index 95d6d6eba..000000000 --- a/packages/ui/src/ui-component/editor/prism-light.css +++ /dev/null @@ -1,207 +0,0 @@ -code[class*='language-'], -pre[class*='language-'] { - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - color: #90a4ae; - background: #fafafa; - font-family: Roboto Mono, monospace; - font-size: 1em; - line-height: 1.5em; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} - -code[class*='language-']::-moz-selection, -pre[class*='language-']::-moz-selection, -code[class*='language-'] ::-moz-selection, -pre[class*='language-'] ::-moz-selection { - background: #cceae7; - color: #263238; -} - -code[class*='language-']::selection, -pre[class*='language-']::selection, -code[class*='language-'] ::selection, -pre[class*='language-'] ::selection { - background: #cceae7; - color: #263238; -} - -:not(pre) > code[class*='language-'] { - white-space: normal; - border-radius: 0.2em; - padding: 0.1em; -} - -pre[class*='language-'] { - overflow: auto; - position: relative; - margin: 0.5em 0; - padding: 1.25em 1em; -} - -.language-css > code, -.language-sass > code, -.language-scss > code { - color: #f76d47; -} - -[class*='language-'] .namespace { - opacity: 0.7; -} - -.token.atrule { - color: #7c4dff; -} - -.token.attr-name { - color: #39adb5; -} - -.token.attr-value { - color: #f6a434; -} - -.token.attribute { - color: #f6a434; -} - -.token.boolean { - color: #7c4dff; -} - -.token.builtin { - color: #39adb5; -} - -.token.cdata { - color: #39adb5; -} - -.token.char { - color: #39adb5; -} - -.token.class { - color: #39adb5; -} - -.token.class-name { - color: #6182b8; -} - -.token.comment { - color: #aabfc9; -} - -.token.constant { - color: #7c4dff; -} - -.token.deleted { - color: #e53935; -} - -.token.doctype { - color: #aabfc9; -} - -.token.entity { - color: #e53935; -} - -.token.function { - color: #7c4dff; -} - -.token.hexcode { - color: #f76d47; -} - -.token.id { - color: #7c4dff; - font-weight: bold; -} - -.token.important { - color: #7c4dff; - font-weight: bold; -} - -.token.inserted { - color: #39adb5; -} - -.token.keyword { - color: #7c4dff; -} - -.token.number { - color: #f76d47; -} - -.token.operator { - color: #39adb5; -} - -.token.prolog { - color: #aabfc9; -} - -.token.property { - color: #39adb5; -} - -.token.pseudo-class { - color: #f6a434; -} - -.token.pseudo-element { - color: #f6a434; -} - -.token.punctuation { - color: #39adb5; -} - -.token.regex { - color: #6182b8; -} - -.token.selector { - color: #e53935; -} - -.token.string { - color: #f6a434; -} - -.token.symbol { - color: #7c4dff; -} - -.token.tag { - color: #e53935; -} - -.token.unit { - color: #f76d47; -} - -.token.url { - color: #e53935; -} - -.token.variable { - color: #e53935; -} diff --git a/packages/ui/src/ui-component/input/Input.js b/packages/ui/src/ui-component/input/Input.js index 6993847b0..3e5759386 100644 --- a/packages/ui/src/ui-component/input/Input.js +++ b/packages/ui/src/ui-component/input/Input.js @@ -1,23 +1,10 @@ import { useState, useEffect, useRef } from 'react' import PropTypes from 'prop-types' import { FormControl, OutlinedInput, Popover } from '@mui/material' -import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog' import SelectVariable from 'ui-component/json/SelectVariable' import { getAvailableNodesForVariable } from 'utils/genericHelper' -export const Input = ({ - inputParam, - value, - nodes, - edges, - nodeId, - onChange, - disabled = false, - showDialog, - dialogProps, - onDialogCancel, - onDialogConfirm -}) => { +export const Input = ({ inputParam, value, nodes, edges, nodeId, onChange, disabled = false }) => { const [myValue, setMyValue] = useState(value ?? '') const [anchorEl, setAnchorEl] = useState(null) const [availableNodesForVariable, setAvailableNodesForVariable] = useState([]) @@ -86,17 +73,6 @@ export const Input = ({ }} /> - {showDialog && ( - { - setMyValue(newValue) - onDialogConfirm(newValue, inputParamName) - }} - > - )}
{inputParam?.acceptVariable && ( diff --git a/packages/ui/src/views/canvas/NodeInputHandler.js b/packages/ui/src/views/canvas/NodeInputHandler.js index 892a6273d..18e823386 100644 --- a/packages/ui/src/views/canvas/NodeInputHandler.js +++ b/packages/ui/src/views/canvas/NodeInputHandler.js @@ -22,8 +22,11 @@ import { flowContext } from 'store/context/ReactFlowContext' import { isValidConnection } from 'utils/genericHelper' import { JsonEditorInput } from 'ui-component/json/JsonEditor' import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' +import { CodeEditor } from 'ui-component/editor/CodeEditor' + import ToolDialog from 'views/tools/ToolDialog' import AssistantDialog from 'views/assistants/AssistantDialog' +import ExpandTextDialog from 'ui-component/dialog/ExpandTextDialog' import FormatPromptValuesDialog from 'ui-component/dialog/FormatPromptValuesDialog' import CredentialInputHandler from './CredentialInputHandler' @@ -83,7 +86,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA } } } - const onFormatPromptValuesClicked = (value, inputParam) => { + const onEditJSONClicked = (value, inputParam) => { // Preset values if the field is format prompt values let inputValue = value if (inputParam.name === 'promptValues' && !value) { @@ -255,7 +258,7 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA {inputParam.description && }
- {inputParam.type === 'string' && inputParam.rows && ( + {((inputParam.type === 'string' && inputParam.rows) || inputParam.type === 'code') && ( (data.inputs[inputParam.name] = newValue)} /> )} + {inputParam.type === 'code' && ( + <> +
+
+ (data.inputs[inputParam.name] = code)} + basicSetup={{ highlightActiveLine: false, highlightActiveLineGutter: false }} + /> +
+ + )} {(inputParam.type === 'string' || inputParam.type === 'password' || inputParam.type === 'number') && ( setShowExpandDialog(false)} - onDialogConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)} /> )} {inputParam.type === 'json' && ( @@ -353,11 +369,11 @@ const NodeInputHandler = ({ inputAnchor, inputParam, data, disabled = false, isA {inputParam?.acceptVariable && ( <> setAsyncOptionEditDialog('')} onConfirm={onConfirmAsyncOption} > + setShowExpandDialog(false)} + onConfirm={(newValue, inputParamName) => onExpandDialogSave(newValue, inputParamName)} + > ) } diff --git a/packages/ui/src/views/tools/ToolDialog.js b/packages/ui/src/views/tools/ToolDialog.js index 398e9eb8d..6272e05fa 100644 --- a/packages/ui/src/views/tools/ToolDialog.js +++ b/packages/ui/src/views/tools/ToolDialog.js @@ -12,9 +12,7 @@ import { TooltipWithParser } from 'ui-component/tooltip/TooltipWithParser' import { GridActionsCellItem } from '@mui/x-data-grid' import DeleteIcon from '@mui/icons-material/Delete' import ConfirmDialog from 'ui-component/dialog/ConfirmDialog' -import { DarkCodeEditor } from 'ui-component/editor/DarkCodeEditor' -import { LightCodeEditor } from 'ui-component/editor/LightCodeEditor' -import { useTheme } from '@mui/material/styles' +import { CodeEditor } from 'ui-component/editor/CodeEditor' // Icons import { IconX, IconFileExport } from '@tabler/icons' @@ -56,7 +54,6 @@ try { const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) => { const portalElement = document.getElementById('portal') - const theme = useTheme() const customization = useSelector((state) => state.customization) const dispatch = useDispatch() @@ -490,32 +487,14 @@ const ToolDialog = ({ show, dialogProps, onUseTemplate, onCancel, onConfirm }) = See Example )} - {customization.isDarkMode ? ( - setToolFunc(code)} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%', - borderRadius: 5 - }} - /> - ) : ( - setToolFunc(code)} - style={{ - fontSize: '0.875rem', - minHeight: 'calc(100vh - 220px)', - width: '100%', - border: `1px solid ${theme.palette.grey[300]}`, - borderRadius: 5 - }} - /> - )} + setToolFunc(code)} + />