Initial push
@@ -0,0 +1,17 @@
|
||||
<!-- markdownlint-disable MD030 -->
|
||||
|
||||
# Flowise Components
|
||||
|
||||
Apps integration for Flowise. Contain Nodes and Credentials.
|
||||
|
||||

|
||||
|
||||
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).
|
||||
@@ -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 |
|
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 }
|
||||
|
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 }
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './Interface'
|
||||
export * from './utils'
|
||||
@@ -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¶m=2 instead of: param[0]=1¶m[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
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- markdownlint-disable MD030 -->
|
||||
|
||||
# Flowise - LangchainJS UI
|
||||
|
||||

|
||||
|
||||
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).
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: '../../babel.config.js'
|
||||
}
|
||||
@@ -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)
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\dev" %*
|
||||
@@ -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'))
|
||||
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
|
||||
node "%~dp0\run" %*
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ignore": ["**/*.spec.ts", ".git", "node_modules"],
|
||||
"watch": ["commands", "index.ts", "src"],
|
||||
"exec": "yarn oclif-dev",
|
||||
"ext": "ts"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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[]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
})()
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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: ''
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/tests
|
||||
/src
|
||||
/public
|
||||
!build
|
||||
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.eslintrc
|
||||
.prettierignore
|
||||
.prettierrc
|
||||
jsconfig.json
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<!-- markdownlint-disable MD030 -->
|
||||
|
||||
# Flowise UI
|
||||
|
||||
React frontend ui for Flowise.
|
||||
|
||||

|
||||
|
||||
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).
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 287 B |
|
After Width: | Height: | Size: 589 B |
|
After Width: | Height: | Size: 15 KiB |
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,10 @@
|
||||
import client from './client'
|
||||
|
||||
const getAllNodes = () => client.get('/nodes')
|
||||
|
||||
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
|
||||
|
||||
export default {
|
||||
getAllNodes,
|
||||
getSpecificNode
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import client from './client'
|
||||
|
||||
const sendMessageAndGetPrediction = (id, input) => client.post(`/prediction/${id}`, input)
|
||||
|
||||
export default {
|
||||
sendMessageAndGetPrediction
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
import dashboard from './dashboard'
|
||||
|
||||
// ==============================|| MENU ITEMS ||============================== //
|
||||
|
||||
const menuItems = {
|
||||
items: [dashboard]
|
||||
}
|
||||
|
||||
export default menuItems
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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' : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||