Initial push

This commit is contained in:
Henry
2023-04-06 22:17:34 +01:00
commit 05c86ff9c5
162 changed files with 9112 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
<!-- markdownlint-disable MD030 -->
# Flowise Components
Apps integration for Flowise. Contain Nodes and Credentials.
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
Install:
```bash
npm i flowise-components
```
## License
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).
+9
View File
@@ -0,0 +1,9 @@
import gulp from 'gulp'
const { src, dest } = gulp
function copyIcons() {
return src(['nodes/**/*.{jpg,png,svg}']).pipe(dest('dist/nodes'))
}
exports.default = copyIcons
@@ -0,0 +1,58 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
class MRLKAgentLLM implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'MRLK Agent for LLMs'
this.name = 'mrlkAgentLLM'
this.type = 'AgentExecutor'
this.category = 'Agents'
this.icon = 'agent.svg'
this.description = 'Agent that uses the ReAct Framework to decide what action to take, optimized to be used with LLMs'
this.inputs = [
{
label: 'Allowed Tools',
name: 'tools',
type: 'Tool',
list: true
},
{
label: 'LLM Model',
name: 'model',
type: 'BaseLanguageModel'
}
]
}
async getBaseClasses(): Promise<string[]> {
return ['AgentExecutor']
}
async init(nodeData: INodeData): Promise<any> {
const { initializeAgentExecutor } = await import('langchain/agents')
const model = nodeData.inputs?.model
const tools = nodeData.inputs?.tools
const executor = await initializeAgentExecutor(tools, model, 'zero-shot-react-description', true)
return executor
}
async run(nodeData: INodeData, input: string): Promise<string> {
const executor = nodeData.instance
const result = await executor.call({ input })
return result?.output
}
}
module.exports = { nodeClass: MRLKAgentLLM }
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-robot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M7 7h10a2 2 0 0 1 2 2v1l1 1v3l-1 1v3a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-3l-1 -1v-3l1 -1v-1a2 2 0 0 1 2 -2z"></path>
<path d="M10 16h4"></path>
<circle cx="8.5" cy="11.5" r=".5" fill="currentColor"></circle>
<circle cx="15.5" cy="11.5" r=".5" fill="currentColor"></circle>
<path d="M9 7l-1 -4"></path>
<path d="M15 7l1 -4"></path>
</svg>

After

Width:  |  Height:  |  Size: 650 B

@@ -0,0 +1,61 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class LLMChain_Chains implements INode {
label: string
name: string
type: string
icon: string
category: string
baseClasses: string[]
description: string
inputs: INodeParams[]
constructor() {
this.label = 'LLM Chain'
this.name = 'llmChain'
this.type = 'LLMChain'
this.icon = 'chain.svg'
this.category = 'Chains'
this.description = 'Chain to run queries against LLMs'
this.inputs = [
{
label: 'LLM',
name: 'llm',
type: 'BaseLanguageModel'
},
{
label: 'Prompt',
name: 'prompt',
type: 'BasePromptTemplate'
}
]
}
async getBaseClasses(): Promise<string[]> {
const { LLMChain } = await import('langchain/chains')
return getBaseClasses(LLMChain)
}
async init(nodeData: INodeData): Promise<any> {
const { LLMChain } = await import('langchain/chains')
const llm = nodeData.inputs?.llm
const prompt = nodeData.inputs?.prompt
const chain = new LLMChain({ llm, prompt })
return chain
}
async run(nodeData: INodeData, input: string): Promise<string> {
const prompt = nodeData.instance.prompt.inputVariables // ["product"]
if (prompt.length > 1) throw new Error('Prompt can only contains 1 literal string {}. Multiples are found')
const chain = nodeData.instance
const res = await chain.run(input)
return res
}
}
module.exports = { nodeClass: LLMChain_Chains }
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dna" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z"></path>
<path d="M9.172 20.485a4 4 0 1 0 -5.657 -5.657"></path>
<path d="M14.828 3.515a4 4 0 0 0 5.657 5.657"></path>
</svg>

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,83 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class OpenAI_LLMs implements INode {
label: string
name: string
type: string
icon: string
category: string
description: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'OpenAI'
this.name = 'openAI'
this.type = 'OpenAI'
this.icon = 'openai.png'
this.category = 'LLMs'
this.description = 'Wrapper around OpenAI large language models'
this.inputs = [
{
label: 'OpenAI Api Key',
name: 'openAIApiKey',
type: 'password'
},
{
label: 'Model Name',
name: 'modelName',
type: 'options',
options: [
{
label: 'text-davinci-003',
name: 'text-davinci-003'
},
{
label: 'text-davinci-002',
name: 'text-davinci-002'
},
{
label: 'text-curie-001',
name: 'text-curie-001'
},
{
label: 'text-babbage-001',
name: 'text-babbage-001'
}
],
default: 'text-davinci-003',
optional: true
},
{
label: 'Temperature',
name: 'temperature',
type: 'number',
default: 0.7,
optional: true
}
]
}
async getBaseClasses(): Promise<string[]> {
const { OpenAI } = await import('langchain/llms')
return getBaseClasses(OpenAI)
}
async init(nodeData: INodeData): Promise<any> {
const { OpenAI } = await import('langchain/llms')
const temperature = nodeData.inputs?.temperature as string
const modelName = nodeData.inputs?.modelName as string
const openAIApiKey = nodeData.inputs?.openAIApiKey as string
const model = new OpenAI({
temperature: parseInt(temperature, 10),
modelName,
openAIApiKey
})
return model
}
}
module.exports = { nodeClass: OpenAI_LLMs }
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,56 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getInputVariables } from '../../../src/utils'
class PromptTemplate_Prompts implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Prompt Template'
this.name = 'promptTemplate'
this.type = 'PromptTemplate'
this.icon = 'prompt.svg'
this.category = 'Prompts'
this.description = 'Schema to represent a basic prompt for an LLM. Template can only contains 1 literal string {}'
this.inputs = [
{
label: 'Template',
name: 'template',
type: 'string',
rows: 5,
default: 'What is a good name for a company that makes {product}?',
placeholder: 'What is a good name for a company that makes {product}?'
}
]
}
async getBaseClasses(): Promise<string[]> {
const { PromptTemplate } = await import('langchain/prompts')
return getBaseClasses(PromptTemplate)
}
async init(nodeData: INodeData): Promise<any> {
const { PromptTemplate } = await import('langchain/prompts')
const template = nodeData.inputs?.template as string
const inputVariables = getInputVariables(template)
try {
const prompt = new PromptTemplate({
template,
inputVariables: inputVariables
})
return prompt
} catch (e) {
throw new Error(e)
}
}
}
module.exports = { nodeClass: PromptTemplate_Prompts }
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-terminal-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M8 9l3 3l-3 3"></path>
<path d="M13 15l3 0"></path>
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
</svg>

After

Width:  |  Height:  |  Size: 465 B

@@ -0,0 +1,33 @@
import { INode } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class Calculator implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
constructor() {
this.label = 'Calculator'
this.name = 'calculator'
this.type = 'Calculator'
this.icon = 'calculator.svg'
this.category = 'Tools'
this.description = 'Perform calculations on response'
}
async getBaseClasses(): Promise<string[]> {
const { Calculator } = await import('langchain/tools')
return getBaseClasses(Calculator)
}
async init(): Promise<any> {
const { Calculator } = await import('langchain/tools')
return new Calculator()
}
}
module.exports = { nodeClass: Calculator }
@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calculator" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M4 3m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
<path d="M8 7m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v1a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
<path d="M8 14l0 .01"></path>
<path d="M12 14l0 .01"></path>
<path d="M16 14l0 .01"></path>
<path d="M8 17l0 .01"></path>
<path d="M12 17l0 .01"></path>
<path d="M16 17l0 .01"></path>
</svg>

After

Width:  |  Height:  |  Size: 698 B

@@ -0,0 +1,42 @@
import { INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses } from '../../../src/utils'
class SerpAPI implements INode {
label: string
name: string
description: string
type: string
icon: string
category: string
baseClasses: string[]
inputs: INodeParams[]
constructor() {
this.label = 'Serp API'
this.name = 'serpAPI'
this.type = 'SerpAPI'
this.icon = 'serp.png'
this.category = 'Tools'
this.description = 'Wrapper around SerpAPI - a real-time API to access Google search results'
this.inputs = [
{
label: 'Serp Api Key',
name: 'apiKey',
type: 'password'
}
]
}
async getBaseClasses(): Promise<string[]> {
const { SerpAPI } = await import('langchain/tools')
return getBaseClasses(SerpAPI)
}
async init(nodeData: INodeData): Promise<any> {
const { SerpAPI } = await import('langchain/tools')
const apiKey = nodeData.inputs?.apiKey as string
return new SerpAPI(apiKey)
}
}
module.exports = { nodeClass: SerpAPI }
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

+34
View File
@@ -0,0 +1,34 @@
{
"name": "flowise-components",
"version": "1.0.0",
"description": "Flowiseai Components",
"main": "dist/src/index",
"types": "dist/src/index.d.ts",
"scripts": {
"build": "tsc && gulp",
"dev": "tsc --watch"
},
"keywords": [],
"homepage": "https://flowiseai.com",
"author": {
"name": "Henry Heng",
"email": "henryheng@flowiseai.com"
},
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"axios": "^0.27.2",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"form-data": "^4.0.0",
"langchain": "^0.0.44",
"moment": "^2.29.3",
"node-fetch": "2",
"ws": "^8.9.0"
},
"devDependencies": {
"@types/gulp": "4.0.9",
"@types/ws": "^8.5.3",
"gulp": "^4.0.2",
"typescript": "^4.8.4"
}
}
+83
View File
@@ -0,0 +1,83 @@
/**
* Types
*/
export type NodeParamsType =
| 'asyncOptions'
| 'options'
| 'string'
| 'number'
| 'boolean'
| 'password'
| 'json'
| 'code'
| 'date'
| 'file'
| 'folder'
export type CommonType = string | number | boolean | undefined | null
/**
* Others
*/
export interface ICommonObject {
[key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[]
}
export interface IAttachment {
content: string
contentType: string
size?: number
filename?: string
}
export interface INodeOptionsValue {
label: string
name: string
description?: string
}
export interface INodeParams {
label: string
name: string
type: NodeParamsType | string
default?: CommonType | ICommonObject | ICommonObject[]
description?: string
options?: Array<INodeOptionsValue>
optional?: boolean | INodeDisplay
rows?: number
list?: boolean
placeholder?: string
}
export interface INodeExecutionData {
[key: string]: CommonType | CommonType[] | ICommonObject | ICommonObject[]
}
export interface INodeDisplay {
[key: string]: string[] | string
}
export interface INodeProperties {
label: string
name: string
type: string
icon: string
category: string
baseClasses: string[]
description?: string
filePath?: string
}
export interface INode extends INodeProperties {
inputs?: INodeParams[]
getBaseClasses?(): Promise<string[]>
getInstance?(nodeData: INodeData): Promise<string>
run?(nodeData: INodeData, input: string): Promise<string>
}
export interface INodeData extends INodeProperties {
inputs?: ICommonObject
instance?: any
}
+2
View File
@@ -0,0 +1,2 @@
export * from './Interface'
export * from './utils'
+144
View File
@@ -0,0 +1,144 @@
import * as fs from 'fs'
import * as path from 'path'
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
export const getBaseClasses = (targetClass: any) => {
const baseClasses: string[] = []
if (targetClass instanceof Function) {
let baseClass = targetClass
while (baseClass) {
const newBaseClass = Object.getPrototypeOf(baseClass)
if (newBaseClass && newBaseClass !== Object && newBaseClass.name) {
baseClass = newBaseClass
baseClasses.push(baseClass.name)
} else {
break
}
}
}
return baseClasses
}
/**
* Serialize axios query params
*
* @export
* @param {any} params
* @param {boolean} skipIndex // Set to true if you want same params to be: param=1&param=2 instead of: param[0]=1&param[1]=2
* @returns {string}
*/
export function serializeQueryParams(params: any, skipIndex?: boolean): string {
const parts: any[] = []
const encode = (val: string) => {
return encodeURIComponent(val)
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, '+')
.replace(/%5B/gi, '[')
.replace(/%5D/gi, ']')
}
const convertPart = (key: string, val: any) => {
if (val instanceof Date) val = val.toISOString()
else if (val instanceof Object) val = JSON.stringify(val)
parts.push(encode(key) + '=' + encode(val))
}
Object.entries(params).forEach(([key, val]) => {
if (val === null || typeof val === 'undefined') return
if (Array.isArray(val)) val.forEach((v, i) => convertPart(`${key}${skipIndex ? '' : `[${i}]`}`, v))
else convertPart(key, val)
})
return parts.join('&')
}
/**
* Handle error from try catch
*
* @export
* @param {any} error
* @returns {string}
*/
export function handleErrorMessage(error: any): string {
let errorMessage = ''
if (error.message) {
errorMessage += error.message + '. '
}
if (error.response && error.response.data) {
if (error.response.data.error) {
if (typeof error.response.data.error === 'object') errorMessage += JSON.stringify(error.response.data.error) + '. '
else if (typeof error.response.data.error === 'string') errorMessage += error.response.data.error + '. '
} else if (error.response.data.msg) errorMessage += error.response.data.msg + '. '
else if (error.response.data.Message) errorMessage += error.response.data.Message + '. '
else if (typeof error.response.data === 'string') errorMessage += error.response.data + '. '
}
if (!errorMessage) errorMessage = 'Unexpected Error.'
return errorMessage
}
/**
* Returns the path of node modules package
* @param {string} packageName
* @returns {string}
*/
export const getNodeModulesPackagePath = (packageName: string): string => {
const checkPaths = [
path.join(__dirname, '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', '..', '..', 'node_modules', packageName)
]
for (const checkPath of checkPaths) {
if (fs.existsSync(checkPath)) {
return checkPath
}
}
return ''
}
/**
* Get input variables
* @param {string} paramValue
* @returns {boolean}
*/
export const getInputVariables = (paramValue: string): string[] => {
let returnVal = paramValue
const variableStack = []
const inputVariables = []
let startIdx = 0
const endIdx = returnVal.length - 1
while (startIdx < endIdx) {
const substr = returnVal.substring(startIdx, startIdx + 1)
// Store the opening double curly bracket
if (substr === '{') {
variableStack.push({ substr, startIdx: startIdx + 1 })
}
// Found the complete variable
if (substr === '}' && variableStack.length > 0 && variableStack[variableStack.length - 1].substr === '{') {
const variableStartIdx = variableStack[variableStack.length - 1].startIdx
const variableEndIdx = startIdx
const variableFullPath = returnVal.substring(variableStartIdx, variableEndIdx)
inputVariables.push(variableFullPath)
variableStack.pop()
}
startIdx += 1
}
return inputVariables
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"lib": ["ES2020"],
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
"target": "ES2020", // or higher
"outDir": "./dist/",
"resolveJsonModule": true,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"sourceMap": true,
"strictPropertyInitialization": false,
"useUnknownInCatchVariables": false,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node16"
},
"include": ["src", "nodes"]
}
+45
View File
@@ -0,0 +1,45 @@
<!-- markdownlint-disable MD030 -->
# Flowise - LangchainJS UI
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
Drag & drop UI to build your customized LLM flow using [LangchainJS](https://github.com/hwchase17/langchainjs)
## ⚡Quick Start
1. Install Flowise
```bash
npm install -g flowise
```
2. Start Flowise
```bash
npx flowise start
```
3. Open [http://localhost:3000](http://localhost:3000)
## 📖 Documentation
Coming Soon
## 💻 Cloud Hosted
Coming Soon
## 🌐 Self Host
Coming Soon
## 🙋 Support
Feel free to ask any questions, raise problems, and request new features in [discussion](https://github.com/FlowiseAI/Flowise/discussions)
## 🙌 Contributing
See [contributing guide](https://github.com/FlowiseAI/Flowise/blob/master/CONTRIBUTING.md). Reach out to us at [Discord](https://discord.gg/GWcGczPk) if you have any questions or issues.
## 📄 License
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: '../../babel.config.js'
}
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env node
const oclif = require('@oclif/core')
const path = require('path')
const project = path.join(__dirname, '..', 'tsconfig.json')
// In dev mode -> use ts-node and dev plugins
process.env.NODE_ENV = 'development'
require('ts-node').register({ project })
// In dev mode, always show stack traces
oclif.settings.debug = true
// Start the CLI
oclif.run().then(oclif.flush).catch(oclif.Errors.handle)
+3
View File
@@ -0,0 +1,3 @@
@echo off
node "%~dp0\dev" %*
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env node
const oclif = require('@oclif/core')
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))
+3
View File
@@ -0,0 +1,3 @@
@echo off
node "%~dp0\run" %*
+6
View File
@@ -0,0 +1,6 @@
{
"ignore": ["**/*.spec.ts", ".git", "node_modules"],
"watch": ["commands", "index.ts", "src"],
"exec": "yarn oclif-dev",
"ext": "ts"
}
+70
View File
@@ -0,0 +1,70 @@
{
"name": "flowise",
"version": "1.0.0",
"description": "Flowiseai Server",
"main": "dist/index",
"types": "dist/index.d.ts",
"bin": {
"flowise": "./bin/run"
},
"files": [
"bin",
"dist",
"npm-shrinkwrap.json",
"oclif.manifest.json",
"oauth2.html",
".env"
],
"oclif": {
"bin": "flowise",
"commands": "./dist/commands"
},
"scripts": {
"build": "tsc",
"start": "run-script-os",
"start:windows": "cd bin && run start",
"start:default": "cd bin && ./run start",
"dev": "concurrently \"yarn watch\" \"nodemon\"",
"oclif-dev": "run-script-os",
"oclif-dev:windows": "cd bin && dev start",
"oclif-dev:default": "cd bin && ./dev start",
"postpack": "shx rm -f oclif.manifest.json",
"prepack": "yarn build && oclif manifest && oclif readme",
"typeorm": "typeorm-ts-node-commonjs",
"watch": "tsc --watch",
"version": "oclif readme && git add README.md"
},
"keywords": [],
"homepage": "https://flowiseai.com",
"author": {
"name": "Henry Heng",
"email": "henryheng@flowiseai.com"
},
"engines": {
"node": ">=18.15.0"
},
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@oclif/core": "^1.13.10",
"axios": "^0.27.2",
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"flowise-components": "*",
"flowise-ui": "*",
"moment-timezone": "^0.5.34",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.6"
},
"devDependencies": {
"@types/cors": "^2.8.12",
"concurrently": "^7.1.0",
"nodemon": "^2.0.15",
"oclif": "^3",
"run-script-os": "^1.1.6",
"shx": "^0.3.3",
"ts-node": "^10.7.0",
"typescript": "^4.8.4"
}
}
+27
View File
@@ -0,0 +1,27 @@
import 'reflect-metadata'
import path from 'path'
import { DataSource } from 'typeorm'
import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage'
import { getUserHome } from './utils'
let appDataSource: DataSource
export const init = async (): Promise<void> => {
const homePath = path.join(getUserHome(), '.flowise')
appDataSource = new DataSource({
type: 'sqlite',
database: path.resolve(homePath, 'database.sqlite'),
synchronize: true,
entities: [ChatFlow, ChatMessage],
migrations: []
})
}
export function getDataSource(): DataSource {
if (appDataSource === undefined) {
init()
}
return appDataSource
}
+101
View File
@@ -0,0 +1,101 @@
import { INode, INodeData } from 'flowise-components'
export type MessageType = 'apiMessage' | 'userMessage'
/**
* Databases
*/
export interface IChatFlow {
id: string
name: string
flowData: string
deployed: boolean
updatedDate: Date
createdDate: Date
}
export interface IChatMessage {
id: string
role: MessageType
content: string
chatflowid: string
createdDate: Date
}
export interface IComponentNodesPool {
[key: string]: INode
}
export interface IVariableDict {
[key: string]: string
}
export interface INodeDependencies {
[key: string]: number
}
export interface INodeDirectedGraph {
[key: string]: string[]
}
export interface IReactFlowNode {
id: string
position: {
x: number
y: number
}
type: string
data: INodeData
positionAbsolute: {
x: number
y: number
}
z: number
handleBounds: {
source: any
target: any
}
width: number
height: number
selected: boolean
dragging: boolean
}
export interface IReactFlowEdge {
source: string
sourceHandle: string
target: string
targetHandle: string
type: string
id: string
data: {
label: string
}
}
export interface IReactFlowObject {
nodes: IReactFlowNode[]
edges: IReactFlowEdge[]
viewport: {
x: number
y: number
zoom: number
}
}
export interface IExploredNode {
[key: string]: {
remainingLoop: number
lastSeenDepth: number
}
}
export interface INodeQueue {
nodeId: string
depth: number
}
export interface IncomingInput {
question: string
history: string[]
}
+66
View File
@@ -0,0 +1,66 @@
import { IComponentNodesPool } from './Interface'
import path from 'path'
import { Dirent } from 'fs'
import { getNodeModulesPackagePath } from './utils'
import { promises } from 'fs'
export class NodesPool {
componentNodes: IComponentNodesPool = {}
/**
* Initialize to get all nodes
*/
async initialize() {
const packagePath = getNodeModulesPackagePath('flowise-components')
const nodesPath = path.join(packagePath, 'dist', 'nodes')
const nodeFiles = await this.getFiles(nodesPath)
return Promise.all(
nodeFiles.map(async (file) => {
if (file.endsWith('.js')) {
const nodeModule = await require(file)
try {
const newNodeInstance = new nodeModule.nodeClass()
newNodeInstance.filePath = file
const baseClasses = await newNodeInstance.getBaseClasses!.call(newNodeInstance)
newNodeInstance.baseClasses = baseClasses
this.componentNodes[newNodeInstance.name] = newNodeInstance
// Replace file icon with absolute path
if (
newNodeInstance.icon &&
(newNodeInstance.icon.endsWith('.svg') ||
newNodeInstance.icon.endsWith('.png') ||
newNodeInstance.icon.endsWith('.jpg'))
) {
const filePath = file.replace(/\\/g, '/').split('/')
filePath.pop()
const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}`
this.componentNodes[newNodeInstance.name].icon = nodeIconAbsolutePath
}
} catch (e) {
// console.error(e);
}
}
})
)
}
/**
* Recursive function to get node files
* @param {string} dir
* @returns {string[]}
*/
async getFiles(dir: string): Promise<string[]> {
const dirents = await promises.readdir(dir, { withFileTypes: true })
const files = await Promise.all(
dirents.map((dirent: Dirent) => {
const res = path.resolve(dir, dirent.name)
return dirent.isDirectory() ? this.getFiles(res) : res
})
)
return Array.prototype.concat(...files)
}
}
+66
View File
@@ -0,0 +1,66 @@
import { Command, Flags } from '@oclif/core'
import path from 'path'
import * as Server from '../index'
import * as DataSource from '../DataSource'
import dotenv from 'dotenv'
dotenv.config({ path: path.join(__dirname, '..', '..', '.env') })
enum EXIT_CODE {
SUCCESS = 0,
FAILED = 1
}
let processExitCode = EXIT_CODE.SUCCESS
export default class Start extends Command {
static flags = {
mongourl: Flags.string()
}
static args = []
async stopProcess() {
console.info('Shutting down Flowise...')
try {
// Shut down the app after timeout if it ever stuck removing pools
setTimeout(() => {
console.info('Flowise was forced to shut down after 30 secs')
process.exit(processExitCode)
}, 30000)
// Removing pools
const serverApp = Server.getInstance()
if (serverApp) await serverApp.stopApp()
} catch (error) {
console.error('There was an error shutting down Flowise...', error)
}
process.exit(processExitCode)
}
async run(): Promise<void> {
process.on('SIGTERM', this.stopProcess)
process.on('SIGINT', this.stopProcess)
// Prevent throw new Error from crashing the app
// TODO: Get rid of this and send proper error message to ui
process.on('uncaughtException', (err) => {
console.error('uncaughtException: ', err)
})
const { flags } = await this.parse(Start)
if (flags.mongourl) process.env.MONGO_URL = flags.mongourl
await (async () => {
try {
this.log('Starting Flowise...')
await DataSource.init()
await Server.start()
} catch (error) {
console.error('There was an error starting Flowise...', error)
processExitCode = EXIT_CODE.FAILED
// @ts-ignore
process.emit('SIGINT')
}
})()
}
}
+24
View File
@@ -0,0 +1,24 @@
/* eslint-disable */
import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm'
import { IChatFlow } from '../Interface'
@Entity()
export class ChatFlow implements IChatFlow {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
name: string
@Column()
flowData: string
@Column()
deployed: boolean
@CreateDateColumn()
createdDate: Date
@UpdateDateColumn()
updatedDate: Date
}
+22
View File
@@ -0,0 +1,22 @@
/* eslint-disable */
import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, Index } from 'typeorm'
import { IChatMessage, MessageType } from '../Interface'
@Entity()
export class ChatMessage implements IChatMessage {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
role: MessageType
@Index()
@Column()
chatflowid: string
@Column()
content: string
@CreateDateColumn()
createdDate: Date
}
+146
View File
@@ -0,0 +1,146 @@
export const workflow1 = {
nodes: [
{
width: 200,
height: 66,
id: 'promptTemplate_0',
position: {
x: 295.0571878493141,
y: 108.66221078850214
},
type: 'customNode',
data: {
label: 'Prompt Template',
name: 'promptTemplate',
type: 'PromptTemplate',
inputAnchors: [],
outputAnchors: [
{
id: 'promptTemplate_0-output-0'
}
],
selected: false,
inputs: {
template: 'What is a good name for a company that makes {product}?',
inputVariables: '["product"]'
}
},
selected: false,
positionAbsolute: {
x: 295.0571878493141,
y: 108.66221078850214
},
dragging: false
},
{
width: 200,
height: 66,
id: 'openAI_0',
position: {
x: 774,
y: 97.75
},
type: 'customNode',
data: {
label: 'OpenAI',
name: 'openAI',
type: 'OpenAI',
inputAnchors: [],
outputAnchors: [
{
id: 'openAI_0-output-0'
}
],
selected: false,
inputs: {
modelName: 'text-davinci-003',
temperature: '0.7',
openAIApiKey: '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'
},
calls: {
prompt: 'Hi, how are you?'
}
},
selected: false,
positionAbsolute: {
x: 774,
y: 97.75
},
dragging: false
},
{
width: 200,
height: 66,
id: 'llmChain_0',
position: {
x: 1034.233162523021,
y: 97.59868104260748
},
type: 'customNode',
data: {
label: 'LLM Chain',
name: 'llmChain',
type: 'LLMChain',
inputAnchors: [
{
id: 'llmChain_0-input-0'
}
],
outputAnchors: [
{
id: 'llmChain_0-output-0'
}
],
selected: false,
inputs: {
llm: '{{openAI_0.data.instance}}',
prompt: '{{promptTemplate_0.data.instance}}'
},
calls: {
variable: '{"product":"colorful socks"}'
}
},
selected: false,
positionAbsolute: {
x: 1034.233162523021,
y: 97.59868104260748
},
dragging: false
}
],
edges: [
{
source: 'nodeJS_0',
sourceHandle: 'nodeJS_0-output-0',
target: 'nodeJS_1',
targetHandle: 'nodeJS_1-input-0',
type: 'buttonedge',
id: 'nodeJS_0-nodeJS_0-output-0-nodeJS_1-nodeJS_1-input-0',
data: {
label: ''
}
},
{
source: 'webhook_0',
sourceHandle: 'webhook_0-output-0',
target: 'wait_0',
targetHandle: 'wait_0-input-0',
type: 'buttonedge',
id: 'webhook_0-webhook_0-output-0-wait_0-wait_0-input-0',
data: {
label: ''
}
},
{
source: 'wait_0',
sourceHandle: 'wait_0-output-0',
target: 'nodeJS_0',
targetHandle: 'nodeJS_0-input-0',
type: 'buttonedge',
id: 'wait_0-wait_0-output-0-nodeJS_0-nodeJS_0-input-0',
data: {
label: ''
}
}
]
}
+261
View File
@@ -0,0 +1,261 @@
import express, { Request, Response } from 'express'
import path from 'path'
import cors from 'cors'
import http from 'http'
import { IChatFlow, IComponentNodesPool, IncomingInput, IReactFlowNode, IReactFlowObject } from './Interface'
import { getNodeModulesPackagePath, getStartingNode, buildLangchain, getEndingNode, constructGraphs } from './utils'
import { cloneDeep } from 'lodash'
import { getDataSource } from './DataSource'
import { NodesPool } from './NodesPool'
import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage'
export class App {
app: express.Application
componentNodes: IComponentNodesPool = {}
AppDataSource = getDataSource()
constructor() {
this.app = express()
}
async initDatabase() {
// Initialize database
this.AppDataSource.initialize()
.then(async () => {
console.info('📦[server]: Data Source has been initialized!')
// Initialize node instances
const nodesPool = new NodesPool()
await nodesPool.initialize()
this.componentNodes = nodesPool.componentNodes
})
.catch((err) => {
console.error('❌[server]: Error during Data Source initialization:', err)
})
}
async config() {
// Limit is needed to allow sending/receiving base64 encoded string
this.app.use(express.json({ limit: '50mb' }))
this.app.use(express.urlencoded({ limit: '50mb', extended: true }))
// Allow access from ui when yarn run dev
if (process.env.NODE_ENV !== 'production') {
this.app.use(cors({ credentials: true, origin: 'http://localhost:8080' }))
}
// ----------------------------------------
// Nodes
// ----------------------------------------
// Get all component nodes
this.app.get('/api/v1/nodes', (req: Request, res: Response) => {
const returnData = []
for (const nodeName in this.componentNodes) {
const clonedNode = cloneDeep(this.componentNodes[nodeName])
returnData.push(clonedNode)
}
return res.json(returnData)
})
// Get specific component node via name
this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => {
if (Object.prototype.hasOwnProperty.call(this.componentNodes, req.params.name)) {
return res.json(this.componentNodes[req.params.name])
} else {
throw new Error(`Node ${req.params.name} not found`)
}
})
// Returns specific component node icon via name
this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => {
if (Object.prototype.hasOwnProperty.call(this.componentNodes, req.params.name)) {
const nodeInstance = this.componentNodes[req.params.name]
if (nodeInstance.icon === undefined) {
throw new Error(`Node ${req.params.name} icon not found`)
}
if (nodeInstance.icon.endsWith('.svg') || nodeInstance.icon.endsWith('.png') || nodeInstance.icon.endsWith('.jpg')) {
const filepath = nodeInstance.icon
res.sendFile(filepath)
} else {
throw new Error(`Node ${req.params.name} icon is missing icon`)
}
} else {
throw new Error(`Node ${req.params.name} not found`)
}
})
// ----------------------------------------
// Chatflows
// ----------------------------------------
// Get all chatflows
this.app.get('/api/v1/chatflows', async (req: Request, res: Response) => {
const chatflows: IChatFlow[] = await this.AppDataSource.getRepository(ChatFlow).find()
return res.json(chatflows)
})
// Get specific chatflow via id
this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: req.params.id
})
if (chatflow) return res.json(chatflow)
return res.status(404).send(`Chatflow ${req.params.id} not found`)
})
// Save chatflow
this.app.post('/api/v1/chatflows', async (req: Request, res: Response) => {
const body = req.body
const newChatFlow = new ChatFlow()
Object.assign(newChatFlow, body)
const chatflow = this.AppDataSource.getRepository(ChatFlow).create(newChatFlow)
const results = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
return res.json(results)
})
// Update chatflow
this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: req.params.id
})
if (!chatflow) {
res.status(404).send(`Chatflow ${req.params.id} not found`)
return
}
const body = req.body
const updateChatFlow = new ChatFlow()
Object.assign(updateChatFlow, body)
this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
return res.json(result)
})
// Delete chatflow via id
this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id })
return res.json(results)
})
// ----------------------------------------
// ChatMessage
// ----------------------------------------
// Get all chatmessages from chatflowid
this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
const chatmessages = await this.AppDataSource.getRepository(ChatMessage).findBy({
chatflowid: req.params.id
})
return res.json(chatmessages)
})
// Add chatmessages for chatflowid
this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
const body = req.body
const newChatMessage = new ChatMessage()
Object.assign(newChatMessage, body)
const chatmessage = this.AppDataSource.getRepository(ChatMessage).create(newChatMessage)
const results = await this.AppDataSource.getRepository(ChatMessage).save(chatmessage)
return res.json(results)
})
// Delete all chatmessages from chatflowid
this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
const results = await this.AppDataSource.getRepository(ChatMessage).delete({ chatflowid: req.params.id })
return res.json(results)
})
// ----------------------------------------
// Prediction
// ----------------------------------------
// Send input message and get prediction result
this.app.post('/api/v1/prediction/:id', async (req: Request, res: Response) => {
try {
const incomingInput: IncomingInput = req.body
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 { graph, nodeDependencies } = constructGraphs(parsedFlowData.nodes, parsedFlowData.edges)
const startingNodeIds = getStartingNode(nodeDependencies)
const endingNodeId = getEndingNode(nodeDependencies, graph)
if (!endingNodeId) return res.status(500).send(`Ending node must be either Chain or Agent`)
const reactFlowNodes = await buildLangchain(startingNodeIds, parsedFlowData.nodes, graph, this.componentNodes)
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
const nodeInstanceFilePath = this.componentNodes[nodeToExecute.data.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const nodeInstance = new nodeModule.nodeClass()
const result = await nodeInstance.run(nodeToExecute.data, incomingInput.question)
return res.json(result)
} catch (e: any) {
return res.status(500).send(e.message)
}
})
// ----------------------------------------
// Serve UI static
// ----------------------------------------
const packagePath = getNodeModulesPackagePath('flowise-ui')
const uiBuildPath = path.join(packagePath, 'build')
const uiHtmlPath = path.join(packagePath, 'build', 'index.html')
this.app.use('/', express.static(uiBuildPath))
// All other requests not handled will return React app
this.app.use((req, res) => {
res.sendFile(uiHtmlPath)
})
}
async stopApp() {
try {
const removePromises: any[] = []
await Promise.all(removePromises)
} catch (e) {
console.error(`❌[server]: Flowise Server shut down error: ${e}`)
}
}
}
let serverApp: App | undefined
export async function start(): Promise<void> {
serverApp = new App()
const port = parseInt(process.env.PORT || '', 10) || 3000
const server = http.createServer(serverApp.app)
await serverApp.initDatabase()
await serverApp.config()
server.listen(port, () => {
console.info(`⚡️[server]: Flowise Server is listening at ${port}`)
})
}
export function getInstance(): App | undefined {
return serverApp
}
+264
View File
@@ -0,0 +1,264 @@
import path from 'path'
import fs from 'fs'
import {
IComponentNodesPool,
IExploredNode,
INodeDependencies,
INodeDirectedGraph,
INodeQueue,
IReactFlowEdge,
IReactFlowNode
} from '../Interface'
import { cloneDeep, get } from 'lodash'
import { ICommonObject, INodeData } from 'flowise-components'
/**
* Returns the home folder path of the user if
* none can be found it falls back to the current
* working directory
*
*/
export const getUserHome = (): string => {
let variableName = 'HOME'
if (process.platform === 'win32') {
variableName = 'USERPROFILE'
}
if (process.env[variableName] === undefined) {
// If for some reason the variable does not exist
// fall back to current folder
return process.cwd()
}
return process.env[variableName] as string
}
/**
* Returns the path of node modules package
* @param {string} packageName
* @returns {string}
*/
export const getNodeModulesPackagePath = (packageName: string): string => {
const checkPaths = [
path.join(__dirname, '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', '..', '..', 'node_modules', packageName)
]
for (const checkPath of checkPaths) {
if (fs.existsSync(checkPath)) {
return checkPath
}
}
return ''
}
/**
* Construct directed graph and node dependencies score
* @param {IReactFlowNode[]} reactFlowNodes
* @param {IReactFlowEdge[]} reactFlowEdges
*/
export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges: IReactFlowEdge[]) => {
const nodeDependencies = {} as INodeDependencies
const graph = {} as INodeDirectedGraph
for (let i = 0; i < reactFlowNodes.length; i += 1) {
const nodeId = reactFlowNodes[i].id
nodeDependencies[nodeId] = 0
graph[nodeId] = []
}
for (let i = 0; i < reactFlowEdges.length; i += 1) {
const source = reactFlowEdges[i].source
const target = reactFlowEdges[i].target
if (Object.prototype.hasOwnProperty.call(graph, source)) {
graph[source].push(target)
} else {
graph[source] = [target]
}
nodeDependencies[target] += 1
}
return { graph, nodeDependencies }
}
/**
* Get starting node and check if flow is valid
* @param {INodeDependencies} nodeDependencies
*/
export const getStartingNode = (nodeDependencies: INodeDependencies) => {
// Find starting node
const startingNodeIds = [] as string[]
Object.keys(nodeDependencies).forEach((nodeId) => {
if (nodeDependencies[nodeId] === 0) {
startingNodeIds.push(nodeId)
}
})
return startingNodeIds
}
export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeDirectedGraph) => {
// Find starting node
let endingNodeId = ''
Object.keys(graph).forEach((nodeId) => {
if (!graph[nodeId].length && nodeDependencies[nodeId] > 0) {
endingNodeId = nodeId
}
})
return endingNodeId
}
/**
* Build langchain from start to end
* @param {string} startingNodeId
* @param {IReactFlowNode[]} reactFlowNodes
* @param {IReactFlowEdge[]} reactFlowEdges
* @param {INodeDirectedGraph} graph
* @param {IComponentNodesPool} componentNodes
* @param {string} clientId
* @param {any} io
*/
export const buildLangchain = async (
startingNodeIds: string[],
reactFlowNodes: IReactFlowNode[],
graph: INodeDirectedGraph,
componentNodes: IComponentNodesPool
) => {
const flowNodes = cloneDeep(reactFlowNodes)
// Create a Queue and add our initial node in it
const nodeQueue = [] as INodeQueue[]
const exploredNode = {} as IExploredNode
// In the case of infinite loop, only max 3 loops will be executed
const maxLoop = 3
for (let i = 0; i < startingNodeIds.length; i += 1) {
nodeQueue.push({ nodeId: startingNodeIds[i], depth: 0 })
exploredNode[startingNodeIds[i]] = { remainingLoop: maxLoop, lastSeenDepth: 0 }
}
while (nodeQueue.length) {
const { nodeId, depth } = nodeQueue.shift() as INodeQueue
const reactFlowNode = flowNodes.find((nd) => nd.id === nodeId)
const nodeIndex = flowNodes.findIndex((nd) => nd.id === nodeId)
if (!reactFlowNode || reactFlowNode === undefined || nodeIndex < 0) continue
try {
const nodeInstanceFilePath = componentNodes[reactFlowNode.data.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newNodeInstance = new nodeModule.nodeClass()
const reactFlowNodeData: INodeData = resolveVariables(reactFlowNode.data, flowNodes)
flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData)
} catch (e: any) {
console.error(e)
throw new Error(e)
}
const neighbourNodeIds = graph[nodeId]
const nextDepth = depth + 1
for (let i = 0; i < neighbourNodeIds.length; i += 1) {
const neighNodeId = neighbourNodeIds[i]
// If nodeId has been seen, cycle detected
if (Object.prototype.hasOwnProperty.call(exploredNode, neighNodeId)) {
const { remainingLoop, lastSeenDepth } = exploredNode[neighNodeId]
if (lastSeenDepth === nextDepth) continue
if (remainingLoop === 0) {
break
}
const remainingLoopMinusOne = remainingLoop - 1
exploredNode[neighNodeId] = { remainingLoop: remainingLoopMinusOne, lastSeenDepth: nextDepth }
nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth })
} else {
exploredNode[neighNodeId] = { remainingLoop: maxLoop, lastSeenDepth: nextDepth }
nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth })
}
}
}
return flowNodes
}
/**
* Get variable value from outputResponses.output
* @param {string} paramValue
* @param {IReactFlowNode[]} reactFlowNodes
* @param {string} key
* @param {number} loopIndex
* @returns {string}
*/
export const getVariableValue = (paramValue: string, reactFlowNodes: IReactFlowNode[]) => {
let returnVal = paramValue
const variableStack = []
let startIdx = 0
const endIdx = returnVal.length - 1
while (startIdx < endIdx) {
const substr = returnVal.substring(startIdx, startIdx + 2)
// Store the opening double curly bracket
if (substr === '{{') {
variableStack.push({ substr, startIdx: startIdx + 2 })
}
// Found the complete variable
if (substr === '}}' && variableStack.length > 0 && variableStack[variableStack.length - 1].substr === '{{') {
const variableStartIdx = variableStack[variableStack.length - 1].startIdx
const variableEndIdx = startIdx
const variableFullPath = returnVal.substring(variableStartIdx, variableEndIdx)
// Split by first occurence of '.' to get just nodeId
const [variableNodeId, _] = variableFullPath.split('.')
const executedNode = reactFlowNodes.find((nd) => nd.id === variableNodeId)
if (executedNode) {
const variableInstance = get(executedNode.data, 'instance')
returnVal = variableInstance
}
variableStack.pop()
}
startIdx += 1
}
return returnVal
}
/**
* Loop through each inputs and resolve variable if neccessary
* @param {INodeData} reactFlowNodeData
* @param {IReactFlowNode[]} reactFlowNodes
* @returns {INodeData}
*/
export const resolveVariables = (reactFlowNodeData: INodeData, reactFlowNodes: IReactFlowNode[]): INodeData => {
const flowNodeData = cloneDeep(reactFlowNodeData)
const types = 'inputs'
const getParamValues = (paramsObj: ICommonObject) => {
for (const key in paramsObj) {
const paramValue: string = paramsObj[key]
if (Array.isArray(paramValue)) {
const resolvedInstances = []
for (const param of paramValue) {
const resolvedInstance = getVariableValue(param, reactFlowNodes)
resolvedInstances.push(resolvedInstance)
}
paramsObj[key] = resolvedInstances
} else {
const resolvedInstance = getVariableValue(paramValue, reactFlowNodes)
paramsObj[key] = resolvedInstance
}
}
}
const paramsObj = (flowNodeData as any)[types]
getParamValues(paramsObj)
return flowNodeData
}
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": ["es2017"],
"target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
"module": "commonjs" /* Specify what module code is generated. */,
"outDir": "dist",
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"sourceMap": true,
"strictPropertyInitialization": false,
"declaration": true
},
"include": ["src"]
}
+13
View File
@@ -0,0 +1,13 @@
/tests
/src
/public
!build
yarn-debug.log*
yarn-error.log*
.eslintrc
.prettierignore
.prettierrc
jsconfig.json
+17
View File
@@ -0,0 +1,17 @@
<!-- markdownlint-disable MD030 -->
# Flowise UI
React frontend ui for Flowise.
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
Install:
```bash
npm i flowise-ui
```
## License
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"baseUrl": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
+76
View File
@@ -0,0 +1,76 @@
{
"name": "flowise-ui",
"version": "1.0.1",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://flowiseai.com",
"author": {
"name": "HenryHeng",
"email": "henryheng@flowiseai.com"
},
"dependencies": {
"@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",
"@tabler/icons": "^1.39.1",
"clsx": "^1.1.1",
"formik": "^2.2.6",
"framer-motion": "^4.1.13",
"history": "^5.0.0",
"html-react-parser": "^3.0.4",
"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-datepicker": "^4.8.0",
"react-device-detect": "^1.17.0",
"react-dom": "^18.2.0",
"react-json-view": "^1.21.3",
"react-markdown": "^8.0.6",
"react-perfect-scrollbar": "^1.5.8",
"react-redux": "^8.0.5",
"react-router": "~6.3.0",
"react-router-dom": "~6.3.0",
"react-simple-code-editor": "^0.11.2",
"reactflow": "^11.5.6",
"redux": "^4.0.5",
"yup": "^0.32.9"
},
"scripts": {
"start": "react-scripts start",
"dev": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"babel": {
"presets": [
"@babel/preset-react"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^12.8.3",
"pretty-quick": "^3.1.3",
"react-scripts": "^5.0.1",
"sass": "^1.42.1",
"typescript": "^4.8.4"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

+62
View File
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flowise - LangchainJS UI</title>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- Meta Tags-->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2296f3" />
<meta name="title" content="Flowise - LangchainJS UI" />
<meta name="description" content="Flowise helps you to better integrate Web3 with existing Web2 applications" />
<meta name="keywords" content="react, material-ui, reactjs, reactjs, workflow automation, web3, web2, blockchain" />
<meta name="author" content="CodedThemes" />
<!-- Open Graph / Facebook -->
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://flowiseai.com/" />
<meta property="og:site_name" content="flowiseai.com" />
<meta property="article:publisher" content="https://www.facebook.com/codedthemes" />
<meta property="og:title" content="Flowise - LangchainJS UI" />
<meta property="og:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
<meta property="og:image" content="https://flowiseai.com/og-image/og-facebook.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://flowiseai.com" />
<meta property="twitter:title" content="Flowise - LangchainJS UI" />
<meta property="twitter:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
<meta property="twitter:image" content="https://flowiseai.com/og-image/og-twitter.png" />
<meta name="twitter:creator" content="@codedthemes" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="portal"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
+32
View File
@@ -0,0 +1,32 @@
import { useSelector } from 'react-redux'
import { ThemeProvider } from '@mui/material/styles'
import { CssBaseline, StyledEngineProvider } from '@mui/material'
// routing
import Routes from 'routes'
// defaultTheme
import themes from 'themes'
// project imports
import NavigationScroll from 'layout/NavigationScroll'
// ==============================|| APP ||============================== //
const App = () => {
const customization = useSelector((state) => state.customization)
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={themes(customization)}>
<CssBaseline />
<NavigationScroll>
<Routes />
</NavigationScroll>
</ThemeProvider>
</StyledEngineProvider>
)
}
export default App
+19
View File
@@ -0,0 +1,19 @@
import client from './client'
const getAllChatflows = () => client.get('/chatflows')
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
const createNewChatflow = (body) => client.post(`/chatflows`, body)
const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body)
const deleteChatflow = (id) => client.delete(`/chatflows/${id}`)
export default {
getAllChatflows,
getSpecificChatflow,
createNewChatflow,
updateChatflow,
deleteChatflow
}
+13
View File
@@ -0,0 +1,13 @@
import client from './client'
const getChatmessageFromChatflow = (id) => client.get(`/chatmessage/${id}`)
const createNewChatmessage = (id, body) => client.post(`/chatmessage/${id}`, body)
const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`)
export default {
getChatmessageFromChatflow,
createNewChatmessage,
deleteChatmessage
}
+11
View File
@@ -0,0 +1,11 @@
import axios from 'axios'
import { baseURL } from 'store/constant'
const apiClient = axios.create({
baseURL: `${baseURL}/api/v1`,
headers: {
'Content-type': 'application/json'
}
})
export default apiClient
+10
View File
@@ -0,0 +1,10 @@
import client from './client'
const getAllNodes = () => client.get('/nodes')
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
export default {
getAllNodes,
getSpecificNode
}
+7
View File
@@ -0,0 +1,7 @@
import client from './client'
const sendMessageAndGetPrediction = (id, input) => client.post(`/prediction/${id}`, input)
export default {
sendMessageAndGetPrediction
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,157 @@
// paper & background
$paper: #ffffff;
// primary
$primaryLight: #e3f2fd;
$primaryMain: #2196f3;
$primaryDark: #1e88e5;
$primary200: #90caf9;
$primary800: #1565c0;
// secondary
$secondaryLight: #ede7f6;
$secondaryMain: #673ab7;
$secondaryDark: #5e35b1;
$secondary200: #b39ddb;
$secondary800: #4527a0;
// success Colors
$successLight: #cdf5d8;
$success200: #69f0ae;
$successMain: #00e676;
$successDark: #00c853;
// error
$errorLight: #f3d2d2;
$errorMain: #f44336;
$errorDark: #c62828;
// orange
$orangeLight: #fbe9e7;
$orangeMain: #ffab91;
$orangeDark: #d84315;
// warning
$warningLight: #fff8e1;
$warningMain: #ffe57f;
$warningDark: #ffc107;
// grey
$grey50: #fafafa;
$grey100: #f5f5f5;
$grey200: #eeeeee;
$grey300: #e0e0e0;
$grey500: #9e9e9e;
$grey600: #757575;
$grey700: #616161;
$grey900: #212121;
// ==============================|| DARK THEME VARIANTS ||============================== //
// paper & background
$darkBackground: #191b1f;
$darkPaper: #191b1f;
// dark 800 & 900
$darkLevel1: #252525; // level 1
$darkLevel2: #242424; // level 2
// primary dark
$darkPrimaryLight: #23262c;
$darkPrimaryMain: #23262c;
$darkPrimaryDark: #191b1f;
$darkPrimary200: #c9d4e9;
$darkPrimary800: #32353b;
// secondary dark
$darkSecondaryLight: #454c59;
$darkSecondaryMain: #7c4dff;
$darkSecondaryDark: #ffffff;
$darkSecondary200: #32353b;
$darkSecondary800: #6200ea;
// text variants
$darkTextTitle: #d7dcec;
$darkTextPrimary: #bdc8f0;
$darkTextSecondary: #8492c4;
// ==============================|| JAVASCRIPT ||============================== //
:export {
// paper & background
paper: $paper;
// primary
primaryLight: $primaryLight;
primary200: $primary200;
primaryMain: $primaryMain;
primaryDark: $primaryDark;
primary800: $primary800;
// secondary
secondaryLight: $secondaryLight;
secondary200: $secondary200;
secondaryMain: $secondaryMain;
secondaryDark: $secondaryDark;
secondary800: $secondary800;
// success
successLight: $successLight;
success200: $success200;
successMain: $successMain;
successDark: $successDark;
// error
errorLight: $errorLight;
errorMain: $errorMain;
errorDark: $errorDark;
// orange
orangeLight: $orangeLight;
orangeMain: $orangeMain;
orangeDark: $orangeDark;
// warning
warningLight: $warningLight;
warningMain: $warningMain;
warningDark: $warningDark;
// grey
grey50: $grey50;
grey100: $grey100;
grey200: $grey200;
grey300: $grey300;
grey500: $grey500;
grey600: $grey600;
grey700: $grey700;
grey900: $grey900;
// ==============================|| DARK THEME VARIANTS ||============================== //
// paper & background
darkPaper: $darkPaper;
darkBackground: $darkBackground;
// dark 800 & 900
darkLevel1: $darkLevel1;
darkLevel2: $darkLevel2;
// text variants
darkTextTitle: $darkTextTitle;
darkTextPrimary: $darkTextPrimary;
darkTextSecondary: $darkTextSecondary;
// primary dark
darkPrimaryLight: $darkPrimaryLight;
darkPrimaryMain: $darkPrimaryMain;
darkPrimaryDark: $darkPrimaryDark;
darkPrimary200: $darkPrimary200;
darkPrimary800: $darkPrimary800;
// secondary dark
darkSecondaryLight: $darkSecondaryLight;
darkSecondaryMain: $darkSecondaryMain;
darkSecondaryDark: $darkSecondaryDark;
darkSecondary200: $darkSecondary200;
darkSecondary800: $darkSecondary800;
}
+122
View File
@@ -0,0 +1,122 @@
// color variants
@import 'themes-vars.module.scss';
// third-party
@import '~react-perfect-scrollbar/dist/css/styles.css';
// ==============================|| LIGHT BOX ||============================== //
.fullscreen .react-images__blanket {
z-index: 1200;
}
// ==============================|| PERFECT SCROLLBAR ||============================== //
.scrollbar-container {
.ps__rail-y {
&:hover > .ps__thumb-y,
&:focus > .ps__thumb-y,
&.ps--clicking .ps__thumb-y {
background-color: $grey500;
width: 5px;
}
}
.ps__thumb-y {
background-color: $grey500;
border-radius: 6px;
width: 5px;
right: 0;
}
}
.scrollbar-container.ps,
.scrollbar-container > .ps {
&.ps--active-y > .ps__rail-y {
width: 5px;
background-color: transparent !important;
z-index: 999;
&:hover,
&.ps--clicking {
width: 5px;
background-color: transparent;
}
}
&.ps--scrolling-y > .ps__rail-y,
&.ps--scrolling-x > .ps__rail-x {
opacity: 0.4;
background-color: transparent;
}
}
// ==============================|| ANIMATION KEYFRAMES ||============================== //
@keyframes wings {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0px);
}
}
@keyframes blink {
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes bounce {
0%,
20%,
53%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translateZ(0);
}
40%,
43% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -5px, 0);
}
70% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -7px, 0);
}
80% {
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translateZ(0);
}
90% {
transform: translate3d(0, -2px, 0);
}
}
@keyframes slideY {
0%,
50%,
100% {
transform: translateY(0px);
}
25% {
transform: translateY(-10px);
}
75% {
transform: translateY(10px);
}
}
@keyframes slideX {
0%,
50%,
100% {
transform: translateX(0px);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}
+9
View File
@@ -0,0 +1,9 @@
const config = {
// basename: only at build time to set, and Don't add '/' at end off BASENAME for breadcrumbs, also Don't put only '/' use blank('') instead,
basename: '',
defaultPath: '/chatflows',
fontFamily: `'Roboto', sans-serif`,
borderRadius: 12
}
export default config
+26
View File
@@ -0,0 +1,26 @@
import { useState } from 'react'
export default (apiFunc) => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
const request = async (...args) => {
setLoading(true)
try {
const result = await apiFunc(...args)
setData(result.data)
} catch (err) {
setError(err || 'Unexpected Error!')
} finally {
setLoading(false)
}
}
return {
data,
error,
loading,
request
}
}
+37
View File
@@ -0,0 +1,37 @@
import { useContext } from 'react'
import ConfirmContext from 'store/context/ConfirmContext'
import { HIDE_CONFIRM, SHOW_CONFIRM } from 'store/actions'
let resolveCallback
const useConfirm = () => {
const [confirmState, dispatch] = useContext(ConfirmContext)
const closeConfirm = () => {
dispatch({
type: HIDE_CONFIRM
})
}
const onConfirm = () => {
closeConfirm()
resolveCallback(true)
}
const onCancel = () => {
closeConfirm()
resolveCallback(false)
}
const confirm = (confirmPayload) => {
dispatch({
type: SHOW_CONFIRM,
payload: confirmPayload
})
return new Promise((res) => {
resolveCallback = res
})
}
return { confirm, onConfirm, onCancel, confirmState }
}
export default useConfirm
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useRef } from 'react'
// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
const useScriptRef = () => {
const scripted = useRef(true)
useEffect(
() => () => {
scripted.current = false
},
[]
)
return scripted
}
export default useScriptRef
+33
View File
@@ -0,0 +1,33 @@
import React from 'react'
import App from './App'
import { store } from 'store'
import { createRoot } from 'react-dom/client'
// style + assets
import 'assets/scss/style.scss'
// third party
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { SnackbarProvider } from 'notistack'
import ConfirmContextProvider from 'store/context/ConfirmContextProvider'
import { ReactFlowContext } from 'store/context/ReactFlowContext'
const container = document.getElementById('root')
const root = createRoot(container)
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<SnackbarProvider>
<ConfirmContextProvider>
<ReactFlowContext>
<App />
</ReactFlowContext>
</ConfirmContextProvider>
</SnackbarProvider>
</BrowserRouter>
</Provider>
</React.StrictMode>
)
@@ -0,0 +1,127 @@
import PropTypes from 'prop-types'
import { useSelector, useDispatch } from 'react-redux'
import { useState } from 'react'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Box, ButtonBase, Switch } from '@mui/material'
import { styled } from '@mui/material/styles'
// project imports
import LogoSection from '../LogoSection'
// assets
import { IconMenu2 } from '@tabler/icons'
// store
import { SET_DARKMODE } from 'store/actions'
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
const MaterialUISwitch = styled(Switch)(({ theme }) => ({
width: 62,
height: 34,
padding: 7,
'& .MuiSwitch-switchBase': {
margin: 1,
padding: 0,
transform: 'translateX(6px)',
'&.Mui-checked': {
color: '#fff',
transform: 'translateX(22px)',
'& .MuiSwitch-thumb:before': {
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff'
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`
},
'& + .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be'
}
}
},
'& .MuiSwitch-thumb': {
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
width: 32,
height: 32,
'&:before': {
content: "''",
position: 'absolute',
width: '100%',
height: '100%',
left: 0,
top: 0,
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
'#fff'
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`
}
},
'& .MuiSwitch-track': {
opacity: 1,
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
borderRadius: 20 / 2
}
}))
const Header = ({ handleLeftDrawerToggle }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [isDark, setIsDark] = useState(customization.isDarkMode)
const dispatch = useDispatch()
const changeDarkMode = () => {
dispatch({ type: SET_DARKMODE, isDarkMode: !isDark })
setIsDark((isDark) => !isDark)
localStorage.setItem('isDarkMode', !isDark)
}
return (
<>
{/* logo & toggler button */}
<Box
sx={{
width: 228,
display: 'flex',
[theme.breakpoints.down('md')]: {
width: 'auto'
}
}}
>
<Box component='span' sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
<LogoSection />
</Box>
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
<Avatar
variant='rounded'
sx={{
...theme.typography.commonAvatar,
...theme.typography.mediumAvatar,
transition: 'all .2s ease-in-out',
background: theme.palette.secondary.light,
color: theme.palette.secondary.dark,
'&:hover': {
background: theme.palette.secondary.dark,
color: theme.palette.secondary.light
}
}}
onClick={handleLeftDrawerToggle}
color='inherit'
>
<IconMenu2 stroke={1.5} size='1.3rem' />
</Avatar>
</ButtonBase>
</Box>
<Box sx={{ flexGrow: 1 }} />
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
</>
)
}
Header.propTypes = {
handleLeftDrawerToggle: PropTypes.func
}
export default Header
@@ -0,0 +1,18 @@
import { Link } from 'react-router-dom'
// material-ui
import { ButtonBase } from '@mui/material'
// project imports
import config from 'config'
import Logo from 'ui-component/extended/Logo'
// ==============================|| MAIN LOGO ||============================== //
const LogoSection = () => (
<ButtonBase disableRipple component={Link} to={config.defaultPath}>
<Logo />
</ButtonBase>
)
export default LogoSection
@@ -0,0 +1,124 @@
import PropTypes from 'prop-types'
import { useState } from 'react'
import { useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Collapse, List, ListItemButton, ListItemIcon, ListItemText, Typography } from '@mui/material'
// project imports
import NavItem from '../NavItem'
// assets
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
import { IconChevronDown, IconChevronUp } from '@tabler/icons'
// ==============================|| SIDEBAR MENU LIST COLLAPSE ITEMS ||============================== //
const NavCollapse = ({ menu, level }) => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState(null)
const handleClick = () => {
setOpen(!open)
setSelected(!selected ? menu.id : null)
}
// menu collapse & item
const menus = menu.children?.map((item) => {
switch (item.type) {
case 'collapse':
return <NavCollapse key={item.id} menu={item} level={level + 1} />
case 'item':
return <NavItem key={item.id} item={item} level={level + 1} />
default:
return (
<Typography key={item.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
const Icon = menu.icon
const menuIcon = menu.icon ? (
<Icon strokeWidth={1.5} size='1.3rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
) : (
<FiberManualRecordIcon
sx={{
width: selected === menu.id ? 8 : 6,
height: selected === menu.id ? 8 : 6
}}
fontSize={level > 0 ? 'inherit' : 'medium'}
/>
)
return (
<>
<ListItemButton
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
pl: `${level * 24}px`
}}
selected={selected === menu.id}
onClick={handleClick}
>
<ListItemIcon sx={{ my: 'auto', minWidth: !menu.icon ? 18 : 36 }}>{menuIcon}</ListItemIcon>
<ListItemText
primary={
<Typography variant={selected === menu.id ? 'h5' : 'body1'} color='inherit' sx={{ my: 'auto' }}>
{menu.title}
</Typography>
}
secondary={
menu.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{menu.caption}
</Typography>
)
}
/>
{open ? (
<IconChevronUp stroke={1.5} size='1rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
) : (
<IconChevronDown stroke={1.5} size='1rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
)}
</ListItemButton>
<Collapse in={open} timeout='auto' unmountOnExit>
<List
component='div'
disablePadding
sx={{
position: 'relative',
'&:after': {
content: "''",
position: 'absolute',
left: '32px',
top: 0,
height: '100%',
width: '1px',
opacity: 1,
background: theme.palette.primary.light
}
}}
>
{menus}
</List>
</Collapse>
</>
)
}
NavCollapse.propTypes = {
menu: PropTypes.object,
level: PropTypes.number
}
export default NavCollapse
@@ -0,0 +1,61 @@
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Divider, List, Typography } from '@mui/material'
// project imports
import NavItem from '../NavItem'
import NavCollapse from '../NavCollapse'
// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
const NavGroup = ({ item }) => {
const theme = useTheme()
// menu list collapse & items
const items = item.children?.map((menu) => {
switch (menu.type) {
case 'collapse':
return <NavCollapse key={menu.id} menu={menu} level={1} />
case 'item':
return <NavItem key={menu.id} item={menu} level={1} navType='MENU' />
default:
return (
<Typography key={menu.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
return (
<>
<List
subheader={
item.title && (
<Typography variant='caption' sx={{ ...theme.typography.menuCaption }} display='block' gutterBottom>
{item.title}
{item.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{item.caption}
</Typography>
)}
</Typography>
)
}
>
{items}
</List>
{/* group divider */}
<Divider sx={{ mt: 0.25, mb: 1.25 }} />
</>
)
}
NavGroup.propTypes = {
item: PropTypes.object
}
export default NavGroup
@@ -0,0 +1,150 @@
import PropTypes from 'prop-types'
import { forwardRef, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useDispatch, useSelector } from 'react-redux'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, useMediaQuery } from '@mui/material'
// project imports
import { MENU_OPEN, SET_MENU } from 'store/actions'
import config from 'config'
// assets
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
// ==============================|| SIDEBAR MENU LIST ITEMS ||============================== //
const NavItem = ({ item, level, navType, onClick, onUploadFile }) => {
const theme = useTheme()
const dispatch = useDispatch()
const customization = useSelector((state) => state.customization)
const matchesSM = useMediaQuery(theme.breakpoints.down('lg'))
const Icon = item.icon
const itemIcon = item?.icon ? (
<Icon stroke={1.5} size='1.3rem' />
) : (
<FiberManualRecordIcon
sx={{
width: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6,
height: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6
}}
fontSize={level > 0 ? 'inherit' : 'medium'}
/>
)
let itemTarget = '_self'
if (item.target) {
itemTarget = '_blank'
}
let listItemProps = {
component: forwardRef(function ListItemPropsComponent(props, ref) {
return <Link ref={ref} {...props} to={`${config.basename}${item.url}`} target={itemTarget} />
})
}
if (item?.external) {
listItemProps = { component: 'a', href: item.url, target: itemTarget }
}
if (item?.id === 'loadChatflow') {
listItemProps.component = 'label'
}
const handleFileUpload = (e) => {
if (!e.target.files) return
const file = e.target.files[0]
const reader = new FileReader()
reader.onload = (evt) => {
if (!evt?.target?.result) {
return
}
const { result } = evt.target
onUploadFile(result)
}
reader.readAsText(file)
}
const itemHandler = (id) => {
if (navType === 'SETTINGS' && id !== 'loadChatflow') {
onClick(id)
} else {
dispatch({ type: MENU_OPEN, id })
if (matchesSM) dispatch({ type: SET_MENU, opened: false })
}
}
// active menu item on page load
useEffect(() => {
if (navType === 'MENU') {
const currentIndex = document.location.pathname
.toString()
.split('/')
.findIndex((id) => id === item.id)
if (currentIndex > -1) {
dispatch({ type: MENU_OPEN, id: item.id })
}
if (!document.location.pathname.toString().split('/')[1]) {
itemHandler('chatflows')
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [navType])
return (
<ListItemButton
{...listItemProps}
disabled={item.disabled}
sx={{
borderRadius: `${customization.borderRadius}px`,
mb: 0.5,
alignItems: 'flex-start',
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
py: level > 1 ? 1 : 1.25,
pl: `${level * 24}px`
}}
selected={customization.isOpen.findIndex((id) => id === item.id) > -1}
onClick={() => itemHandler(item.id)}
>
{item.id === 'loadChatflow' && <input type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} />}
<ListItemIcon sx={{ my: 'auto', minWidth: !item?.icon ? 18 : 36 }}>{itemIcon}</ListItemIcon>
<ListItemText
primary={
<Typography variant={customization.isOpen.findIndex((id) => id === item.id) > -1 ? 'h5' : 'body1'} color='inherit'>
{item.title}
</Typography>
}
secondary={
item.caption && (
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
{item.caption}
</Typography>
)
}
/>
{item.chip && (
<Chip
color={item.chip.color}
variant={item.chip.variant}
size={item.chip.size}
label={item.chip.label}
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
/>
)}
</ListItemButton>
)
}
NavItem.propTypes = {
item: PropTypes.object,
level: PropTypes.number,
navType: PropTypes.string,
onClick: PropTypes.func,
onUploadFile: PropTypes.func
}
export default NavItem
@@ -0,0 +1,27 @@
// material-ui
import { Typography } from '@mui/material'
// project imports
import NavGroup from './NavGroup'
import menuItem from 'menu-items'
// ==============================|| SIDEBAR MENU LIST ||============================== //
const MenuList = () => {
const navItems = menuItem.items.map((item) => {
switch (item.type) {
case 'group':
return <NavGroup key={item.id} item={item} />
default:
return (
<Typography key={item.id} variant='h6' color='error' align='center'>
Menu Items Error
</Typography>
)
}
})
return <>{navItems}</>
}
export default MenuList
@@ -0,0 +1,85 @@
import PropTypes from 'prop-types'
// material-ui
import { useTheme } from '@mui/material/styles'
import { Box, Drawer, useMediaQuery } from '@mui/material'
// third-party
import PerfectScrollbar from 'react-perfect-scrollbar'
import { BrowserView, MobileView } from 'react-device-detect'
// project imports
import MenuList from './MenuList'
import LogoSection from '../LogoSection'
import { drawerWidth } from 'store/constant'
// ==============================|| SIDEBAR DRAWER ||============================== //
const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
const theme = useTheme()
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'))
const drawer = (
<>
<Box sx={{ display: { xs: 'block', md: 'none' } }}>
<Box sx={{ display: 'flex', p: 2, mx: 'auto' }}>
<LogoSection />
</Box>
</Box>
<BrowserView>
<PerfectScrollbar
component='div'
style={{
height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 88px)',
paddingLeft: '16px',
paddingRight: '16px'
}}
>
<MenuList />
</PerfectScrollbar>
</BrowserView>
<MobileView>
<Box sx={{ px: 2 }}>
<MenuList />
</Box>
</MobileView>
</>
)
const container = window !== undefined ? () => window.document.body : undefined
return (
<Box component='nav' sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : 'auto' }} aria-label='mailbox folders'>
<Drawer
container={container}
variant={matchUpMd ? 'persistent' : 'temporary'}
anchor='left'
open={drawerOpen}
onClose={drawerToggle}
sx={{
'& .MuiDrawer-paper': {
width: drawerWidth,
background: theme.palette.background.default,
color: theme.palette.text.primary,
borderRight: 'none',
[theme.breakpoints.up('md')]: {
top: '66px'
}
}
}}
ModalProps={{ keepMounted: true }}
color='inherit'
>
{drawer}
</Drawer>
</Box>
)
}
Sidebar.propTypes = {
drawerOpen: PropTypes.bool,
drawerToggle: PropTypes.func,
window: PropTypes.object
}
export default Sidebar
+107
View File
@@ -0,0 +1,107 @@
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Outlet } from 'react-router-dom'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material'
// project imports
import Header from './Header'
import Sidebar from './Sidebar'
import { drawerWidth } from 'store/constant'
import { SET_MENU } from 'store/actions'
// styles
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
...theme.typography.mainContent,
...(!open && {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
[theme.breakpoints.up('md')]: {
marginLeft: -(drawerWidth - 20),
width: `calc(100% - ${drawerWidth}px)`
},
[theme.breakpoints.down('md')]: {
marginLeft: '20px',
width: `calc(100% - ${drawerWidth}px)`,
padding: '16px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px',
width: `calc(100% - ${drawerWidth}px)`,
padding: '16px',
marginRight: '10px'
}
}),
...(open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: `calc(100% - ${drawerWidth}px)`,
[theme.breakpoints.down('md')]: {
marginLeft: '20px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px'
}
})
}))
// ==============================|| MAIN LAYOUT ||============================== //
const MainLayout = () => {
const theme = useTheme()
const matchDownMd = useMediaQuery(theme.breakpoints.down('lg'))
// Handle left drawer
const leftDrawerOpened = useSelector((state) => state.customization.opened)
const dispatch = useDispatch()
const handleLeftDrawerToggle = () => {
dispatch({ type: SET_MENU, opened: !leftDrawerOpened })
}
useEffect(() => {
dispatch({ type: SET_MENU, opened: !matchDownMd })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [matchDownMd])
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
{/* header */}
<AppBar
enableColorOnDark
position='fixed'
color='inherit'
elevation={0}
sx={{
bgcolor: theme.palette.background.default,
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
}}
>
<Toolbar>
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
</Toolbar>
</AppBar>
{/* drawer */}
<Sidebar drawerOpen={leftDrawerOpened} drawerToggle={handleLeftDrawerToggle} />
{/* main content */}
<Main theme={theme} open={leftDrawerOpened}>
<Outlet />
</Main>
</Box>
)
}
export default MainLayout
@@ -0,0 +1,11 @@
import { Outlet } from 'react-router-dom'
// ==============================|| MINIMAL LAYOUT ||============================== //
const MinimalLayout = () => (
<>
<Outlet />
</>
)
export default MinimalLayout
+39
View File
@@ -0,0 +1,39 @@
import PropTypes from 'prop-types'
import { motion } from 'framer-motion'
// ==============================|| ANIMATION FOR CONTENT ||============================== //
const NavMotion = ({ children }) => {
const motionVariants = {
initial: {
opacity: 0,
scale: 0.99
},
in: {
opacity: 1,
scale: 1
},
out: {
opacity: 0,
scale: 1.01
}
}
const motionTransition = {
type: 'tween',
ease: 'anticipate',
duration: 0.4
}
return (
<motion.div initial='initial' animate='in' exit='out' variants={motionVariants} transition={motionTransition}>
{children}
</motion.div>
)
}
NavMotion.propTypes = {
children: PropTypes.node
}
export default NavMotion
@@ -0,0 +1,26 @@
import PropTypes from 'prop-types'
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
// ==============================|| NAVIGATION SCROLL TO TOP ||============================== //
const NavigationScroll = ({ children }) => {
const location = useLocation()
const { pathname } = location
useEffect(() => {
window.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
})
}, [pathname])
return children || null
}
NavigationScroll.propTypes = {
children: PropTypes.node
}
export default NavigationScroll
+25
View File
@@ -0,0 +1,25 @@
// assets
import { IconHierarchy, IconKey, IconBook, IconListCheck } from '@tabler/icons'
// constant
const icons = { IconHierarchy, IconKey, IconBook, IconListCheck }
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
const dashboard = {
id: 'dashboard',
title: '',
type: 'group',
children: [
{
id: 'chatflows',
title: 'Chatflows',
type: 'item',
url: '/chatflows',
icon: icons.IconHierarchy,
breadcrumbs: true
}
]
}
export default dashboard
+9
View File
@@ -0,0 +1,9 @@
import dashboard from './dashboard'
// ==============================|| MENU ITEMS ||============================== //
const menuItems = {
items: [dashboard]
}
export default menuItems
+38
View File
@@ -0,0 +1,38 @@
// assets
import { IconTrash, IconFileUpload, IconFileExport } from '@tabler/icons'
// constant
const icons = { IconTrash, IconFileUpload, IconFileExport }
// ==============================|| SETTINGS MENU ITEMS ||============================== //
const settings = {
id: 'settings',
title: '',
type: 'group',
children: [
{
id: 'loadChatflow',
title: 'Load Chatflow',
type: 'item',
url: '',
icon: icons.IconFileUpload
},
{
id: 'exportChatflow',
title: 'Export Chatflow',
type: 'item',
url: '',
icon: icons.IconFileExport
},
{
id: 'deleteChatflow',
title: 'Delete Chatflow',
type: 'item',
url: '',
icon: icons.IconTrash
}
]
}
export default settings
+27
View File
@@ -0,0 +1,27 @@
import { lazy } from 'react'
// project imports
import Loadable from 'ui-component/loading/Loadable'
import MinimalLayout from 'layout/MinimalLayout'
// canvas routing
const Canvas = Loadable(lazy(() => import('views/canvas')))
// ==============================|| CANVAS ROUTING ||============================== //
const CanvasRoutes = {
path: '/',
element: <MinimalLayout />,
children: [
{
path: '/canvas',
element: <Canvas />
},
{
path: '/canvas/:id',
element: <Canvas />
}
]
}
export default CanvasRoutes
+27
View File
@@ -0,0 +1,27 @@
import { lazy } from 'react'
// project imports
import MainLayout from 'layout/MainLayout'
import Loadable from 'ui-component/loading/Loadable'
// chatflows routing
const Chatflows = Loadable(lazy(() => import('views/chatflows')))
// ==============================|| MAIN ROUTING ||============================== //
const MainRoutes = {
path: '/',
element: <MainLayout />,
children: [
{
path: '/',
element: <Chatflows />
},
{
path: '/chatflows',
element: <Chatflows />
}
]
}
export default MainRoutes
+12
View File
@@ -0,0 +1,12 @@
import { useRoutes } from 'react-router-dom'
// routes
import MainRoutes from './MainRoutes'
import CanvasRoutes from './CanvasRoutes'
import config from 'config'
// ==============================|| ROUTING RENDER ||============================== //
export default function ThemeRoutes() {
return useRoutes([MainRoutes, CanvasRoutes], config.basename)
}
+132
View File
@@ -0,0 +1,132 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
)
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
if (installingWorker == null) {
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.info(
'New content is available and will be used when all tabs for this page are closed. See https://bit.ly/CRA-PWA.'
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.info('Content is cached for offline use.')
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration)
}
}
}
}
}
})
.catch((error) => {
console.error('Error during service worker registration:', error)
})
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' }
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type')
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config)
}
})
.catch(() => {
console.info('No internet connection found. App is running in offline mode.')
})
}
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.info(
'This web app is being served cache-first by a service worker. To learn more, visit https://bit.ly/CRA-PWA'
)
})
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config)
}
})
}
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister()
})
.catch((error) => {
console.error(error.message)
})
}
}
+46
View File
@@ -0,0 +1,46 @@
// action - customization reducer
export const SET_MENU = '@customization/SET_MENU'
export const MENU_TOGGLE = '@customization/MENU_TOGGLE'
export const MENU_OPEN = '@customization/MENU_OPEN'
export const SET_FONT_FAMILY = '@customization/SET_FONT_FAMILY'
export const SET_BORDER_RADIUS = '@customization/SET_BORDER_RADIUS'
export const SET_LAYOUT = '@customization/SET_LAYOUT '
export const SET_DARKMODE = '@customization/SET_DARKMODE'
// action - canvas reducer
export const REMOVE_EDGE = '@canvas/REMOVE_EDGE'
export const SET_DIRTY = '@canvas/SET_DIRTY'
export const REMOVE_DIRTY = '@canvas/REMOVE_DIRTY'
export const SET_CHATFLOW = '@canvas/SET_CHATFLOW'
// action - notifier reducer
export const ENQUEUE_SNACKBAR = 'ENQUEUE_SNACKBAR'
export const CLOSE_SNACKBAR = 'CLOSE_SNACKBAR'
export const REMOVE_SNACKBAR = 'REMOVE_SNACKBAR'
// action - dialog reducer
export const SHOW_CONFIRM = 'SHOW_CONFIRM'
export const HIDE_CONFIRM = 'HIDE_CONFIRM'
export const enqueueSnackbar = (notification) => {
const key = notification.options && notification.options.key
return {
type: ENQUEUE_SNACKBAR,
notification: {
...notification,
key: key || new Date().getTime() + Math.random()
}
}
}
export const closeSnackbar = (key) => ({
type: CLOSE_SNACKBAR,
dismissAll: !key, // dismiss all if no key has been defined
key
})
export const removeSnackbar = (key) => ({
type: REMOVE_SNACKBAR,
key
})
+5
View File
@@ -0,0 +1,5 @@
// constant
export const gridSpacing = 3
export const drawerWidth = 260
export const appDrawerWidth = 320
export const baseURL = process.env.NODE_ENV === 'production' ? window.location.origin : window.location.origin.replace(':8080', ':3000')
@@ -0,0 +1,5 @@
import React from 'react'
const ConfirmContext = React.createContext()
export default ConfirmContext
@@ -0,0 +1,16 @@
import { useReducer } from 'react'
import PropTypes from 'prop-types'
import alertReducer, { initialState } from '../reducers/dialogReducer'
import ConfirmContext from './ConfirmContext'
const ConfirmContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(alertReducer, initialState)
return <ConfirmContext.Provider value={[state, dispatch]}>{children}</ConfirmContext.Provider>
}
ConfirmContextProvider.propTypes = {
children: PropTypes.any
}
export default ConfirmContextProvider
@@ -0,0 +1,35 @@
import { createContext, useState } from 'react'
import PropTypes from 'prop-types'
const initialValue = {
reactFlowInstance: null,
setReactFlowInstance: () => {},
deleteNode: () => {}
}
export const flowContext = createContext(initialValue)
export const ReactFlowContext = ({ children }) => {
const [reactFlowInstance, setReactFlowInstance] = useState(null)
const deleteNode = (id) => {
reactFlowInstance.setNodes(reactFlowInstance.getNodes().filter((n) => n.id !== id))
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((ns) => ns.source !== id && ns.target !== id))
}
return (
<flowContext.Provider
value={{
reactFlowInstance,
setReactFlowInstance,
deleteNode
}}
>
{children}
</flowContext.Provider>
)
}
ReactFlowContext.propTypes = {
children: PropTypes.any
}
+9
View File
@@ -0,0 +1,9 @@
import { createStore } from 'redux'
import reducer from './reducer'
// ==============================|| REDUX - MAIN STORE ||============================== //
const store = createStore(reducer)
const persister = 'Free'
export { store, persister }
+18
View File
@@ -0,0 +1,18 @@
import { combineReducers } from 'redux'
// reducer import
import customizationReducer from './reducers/customizationReducer'
import canvasReducer from './reducers/canvasReducer'
import notifierReducer from './reducers/notifierReducer'
import dialogReducer from './reducers/dialogReducer'
// ==============================|| COMBINE REDUCER ||============================== //
const reducer = combineReducers({
customization: customizationReducer,
canvas: canvasReducer,
notifier: notifierReducer,
dialog: dialogReducer
})
export default reducer
@@ -0,0 +1,39 @@
// action - state management
import * as actionTypes from '../actions'
export const initialState = {
removeEdgeId: '',
isDirty: false,
chatflow: null
}
// ==============================|| CANVAS REDUCER ||============================== //
const canvasReducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.REMOVE_EDGE:
return {
...state,
removeEdgeId: action.edgeId
}
case actionTypes.SET_DIRTY:
return {
...state,
isDirty: true
}
case actionTypes.REMOVE_DIRTY:
return {
...state,
isDirty: false
}
case actionTypes.SET_CHATFLOW:
return {
...state,
chatflow: action.chatflow
}
default:
return state
}
}
export default canvasReducer
@@ -0,0 +1,57 @@
// project imports
import config from 'config'
// action - state management
import * as actionTypes from '../actions'
export const initialState = {
isOpen: [], // for active default menu
fontFamily: config.fontFamily,
borderRadius: config.borderRadius,
opened: true,
isHorizontal: localStorage.getItem('isHorizontal') === 'true' ? true : false,
isDarkMode: localStorage.getItem('isDarkMode') === 'true' ? true : false
}
// ==============================|| CUSTOMIZATION REDUCER ||============================== //
const customizationReducer = (state = initialState, action) => {
let id
switch (action.type) {
case actionTypes.MENU_OPEN:
id = action.id
return {
...state,
isOpen: [id]
}
case actionTypes.SET_MENU:
return {
...state,
opened: action.opened
}
case actionTypes.SET_FONT_FAMILY:
return {
...state,
fontFamily: action.fontFamily
}
case actionTypes.SET_BORDER_RADIUS:
return {
...state,
borderRadius: action.borderRadius
}
case actionTypes.SET_LAYOUT:
return {
...state,
isHorizontal: action.isHorizontal
}
case actionTypes.SET_DARKMODE:
return {
...state,
isDarkMode: action.isDarkMode
}
default:
return state
}
}
export default customizationReducer
@@ -0,0 +1,28 @@
import { SHOW_CONFIRM, HIDE_CONFIRM } from '../actions'
export const initialState = {
show: false,
title: '',
description: '',
confirmButtonName: 'OK',
cancelButtonName: 'Cancel'
}
const alertReducer = (state = initialState, action) => {
switch (action.type) {
case SHOW_CONFIRM:
return {
show: true,
title: action.payload.title,
description: action.payload.description,
confirmButtonName: action.payload.confirmButtonName,
cancelButtonName: action.payload.cancelButtonName
}
case HIDE_CONFIRM:
return initialState
default:
return state
}
}
export default alertReducer
@@ -0,0 +1,40 @@
import { ENQUEUE_SNACKBAR, CLOSE_SNACKBAR, REMOVE_SNACKBAR } from '../actions'
export const initialState = {
notifications: []
}
const notifierReducer = (state = initialState, action) => {
switch (action.type) {
case ENQUEUE_SNACKBAR:
return {
...state,
notifications: [
...state.notifications,
{
key: action.key,
...action.notification
}
]
}
case CLOSE_SNACKBAR:
return {
...state,
notifications: state.notifications.map((notification) =>
action.dismissAll || notification.key === action.key ? { ...notification, dismissed: true } : { ...notification }
)
}
case REMOVE_SNACKBAR:
return {
...state,
notifications: state.notifications.filter((notification) => notification.key !== action.key)
}
default:
return state
}
}
export default notifierReducer
+204
View File
@@ -0,0 +1,204 @@
export default function componentStyleOverrides(theme) {
const bgColor = theme.colors?.grey50
return {
MuiButton: {
styleOverrides: {
root: {
fontWeight: 500,
borderRadius: '4px'
}
}
},
MuiSvgIcon: {
styleOverrides: {
root: {
color: theme?.customization?.isDarkMode ? theme.colors?.paper : 'inherit',
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimaryLight : 'inherit'
}
}
},
MuiPaper: {
defaultProps: {
elevation: 0
},
styleOverrides: {
root: {
backgroundImage: 'none'
},
rounded: {
borderRadius: `${theme?.customization?.borderRadius}px`
}
}
},
MuiCardHeader: {
styleOverrides: {
root: {
color: theme.colors?.textDark,
padding: '24px'
},
title: {
fontSize: '1.125rem'
}
}
},
MuiCardContent: {
styleOverrides: {
root: {
padding: '24px'
}
}
},
MuiCardActions: {
styleOverrides: {
root: {
padding: '24px'
}
}
},
MuiListItemButton: {
styleOverrides: {
root: {
color: theme.darkTextPrimary,
paddingTop: '10px',
paddingBottom: '10px',
'&.Mui-selected': {
color: theme.menuSelected,
backgroundColor: theme.menuSelectedBack,
'&:hover': {
backgroundColor: theme.menuSelectedBack
},
'& .MuiListItemIcon-root': {
color: theme.menuSelected
}
},
'&:hover': {
backgroundColor: theme.menuSelectedBack,
color: theme.menuSelected,
'& .MuiListItemIcon-root': {
color: theme.menuSelected
}
}
}
}
},
MuiListItemIcon: {
styleOverrides: {
root: {
color: theme.darkTextPrimary,
minWidth: '36px'
}
}
},
MuiListItemText: {
styleOverrides: {
primary: {
color: theme.textDark
}
}
},
MuiInputBase: {
styleOverrides: {
input: {
color: theme.textDark,
'&::placeholder': {
color: theme.darkTextSecondary,
fontSize: '0.875rem'
}
}
}
},
MuiOutlinedInput: {
styleOverrides: {
root: {
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary800 : bgColor,
borderRadius: `${theme?.customization?.borderRadius}px`,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: theme.colors?.grey400
},
'&:hover $notchedOutline': {
borderColor: theme.colors?.primaryLight
},
'&.MuiInputBase-multiline': {
padding: 1
}
},
input: {
fontWeight: 500,
background: theme?.customization?.isDarkMode ? theme.colors?.darkPrimary800 : bgColor,
padding: '15.5px 14px',
borderRadius: `${theme?.customization?.borderRadius}px`,
'&.MuiInputBase-inputSizeSmall': {
padding: '10px 14px',
'&.MuiInputBase-inputAdornedStart': {
paddingLeft: 0
}
}
},
inputAdornedStart: {
paddingLeft: 4
},
notchedOutline: {
borderRadius: `${theme?.customization?.borderRadius}px`
}
}
},
MuiSlider: {
styleOverrides: {
root: {
'&.Mui-disabled': {
color: theme.colors?.grey300
}
},
mark: {
backgroundColor: theme.paper,
width: '4px'
},
valueLabel: {
color: theme?.colors?.primaryLight
}
}
},
MuiDivider: {
styleOverrides: {
root: {
borderColor: theme.divider,
opacity: 1
}
}
},
MuiAvatar: {
styleOverrides: {
root: {
color: theme.colors?.primaryDark,
background: theme.colors?.primary200
}
}
},
MuiChip: {
styleOverrides: {
root: {
'&.MuiChip-deletable .MuiChip-deleteIcon': {
color: 'inherit'
}
}
}
},
MuiTooltip: {
styleOverrides: {
tooltip: {
color: theme?.customization?.isDarkMode ? theme.colors?.paper : theme.paper,
background: theme.colors?.grey700
}
}
},
MuiAutocomplete: {
styleOverrides: {
option: {
'&:hover': {
background: theme?.customization?.isDarkMode ? '#233345 !important' : ''
}
}
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
import { createTheme } from '@mui/material/styles'
// assets
import colors from 'assets/scss/_themes-vars.module.scss'
// project imports
import componentStyleOverrides from './compStyleOverride'
import themePalette from './palette'
import themeTypography from './typography'
/**
* Represent theme style and structure as per Material-UI
* @param {JsonObject} customization customization parameter object
*/
export const theme = (customization) => {
const color = colors
const themeOption = customization.isDarkMode
? {
colors: color,
heading: color.paper,
paper: color.darkPrimaryLight,
backgroundDefault: color.darkPaper,
background: color.darkPrimaryLight,
darkTextPrimary: color.paper,
darkTextSecondary: color.paper,
textDark: color.paper,
menuSelected: color.darkSecondaryDark,
menuSelectedBack: color.darkSecondaryLight,
divider: color.darkPaper,
customization
}
: {
colors: color,
heading: color.grey900,
paper: color.paper,
backgroundDefault: color.paper,
background: color.primaryLight,
darkTextPrimary: color.grey700,
darkTextSecondary: color.grey500,
textDark: color.grey900,
menuSelected: color.secondaryDark,
menuSelectedBack: color.secondaryLight,
divider: color.grey200,
customization
}
const themeOptions = {
direction: 'ltr',
palette: themePalette(themeOption),
mixins: {
toolbar: {
minHeight: '48px',
padding: '16px',
'@media (min-width: 600px)': {
minHeight: '48px'
}
}
},
typography: themeTypography(themeOption)
}
const themes = createTheme(themeOptions)
themes.components = componentStyleOverrides(themeOption)
return themes
}
export default theme
+96
View File
@@ -0,0 +1,96 @@
/**
* Color intention that you want to used in your theme
* @param {JsonObject} theme Theme customization object
*/
export default function themePalette(theme) {
return {
mode: theme?.customization?.navType,
common: {
black: theme.colors?.darkPaper
},
primary: {
light: theme.customization.isDarkMode ? theme.colors?.darkPrimaryLight : theme.colors?.primaryLight,
main: theme.colors?.primaryMain,
dark: theme.customization.isDarkMode ? theme.colors?.darkPrimaryDark : theme.colors?.primaryDark,
200: theme.customization.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.primary200,
800: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.primary800
},
secondary: {
light: theme.customization.isDarkMode ? theme.colors?.darkSecondaryLight : theme.colors?.secondaryLight,
main: theme.customization.isDarkMode ? theme.colors?.darkSecondaryMain : theme.colors?.secondaryMain,
dark: theme.customization.isDarkMode ? theme.colors?.darkSecondaryDark : theme.colors?.secondaryDark,
200: theme.colors?.secondary200,
800: theme.colors?.secondary800
},
error: {
light: theme.colors?.errorLight,
main: theme.colors?.errorMain,
dark: theme.colors?.errorDark
},
orange: {
light: theme.colors?.orangeLight,
main: theme.colors?.orangeMain,
dark: theme.colors?.orangeDark
},
warning: {
light: theme.colors?.warningLight,
main: theme.colors?.warningMain,
dark: theme.colors?.warningDark
},
success: {
light: theme.colors?.successLight,
200: theme.colors?.success200,
main: theme.colors?.successMain,
dark: theme.colors?.successDark
},
grey: {
50: theme.colors?.grey50,
100: theme.colors?.grey100,
200: theme.colors?.grey200,
300: theme.colors?.grey300,
500: theme.darkTextSecondary,
600: theme.heading,
700: theme.darkTextPrimary,
900: theme.textDark
},
dark: {
light: theme.colors?.darkTextPrimary,
main: theme.colors?.darkLevel1,
dark: theme.colors?.darkLevel2,
800: theme.colors?.darkBackground,
900: theme.colors?.darkPaper
},
text: {
primary: theme.darkTextPrimary,
secondary: theme.darkTextSecondary,
dark: theme.textDark,
hint: theme.colors?.grey100
},
background: {
paper: theme.paper,
default: theme.backgroundDefault
},
card: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimaryMain : theme.colors?.paper,
light: theme.customization.isDarkMode ? theme.colors?.darkPrimary200 : theme.colors?.paper,
hover: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.paper
},
asyncSelect: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.grey50
},
canvasHeader: {
executionLight: theme.colors?.successLight,
executionDark: theme.colors?.successDark,
deployLight: theme.colors?.primaryLight,
deployDark: theme.colors?.primaryDark,
saveLight: theme.colors?.secondaryLight,
saveDark: theme.colors?.secondaryDark,
settingsLight: theme.colors?.grey300,
settingsDark: theme.colors?.grey700
},
codeEditor: {
main: theme.customization.isDarkMode ? theme.colors?.darkPrimary800 : theme.colors?.primaryLight
}
}
}

Some files were not shown because too many files have changed in this diff Show More