Initial push

This commit is contained in:
Henry 2023-04-06 22:17:34 +01:00
commit 05c86ff9c5
162 changed files with 9112 additions and 0 deletions

7
.dockerignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
dist
build
**/node_modules
**/build
**/dist

28
.eslintrc.js Normal file
View File

@ -0,0 +1,28 @@
module.exports = {
extends: [
'eslint:recommended',
'plugin:markdown/recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
'plugin:jsx-a11y/recommended',
'plugin:prettier/recommended'
],
settings: {
react: {
version: 'detect'
}
},
parser: '@typescript-eslint/parser',
ignorePatterns: ['**/node_modules', '**/dist', '**/build', '**/package-lock.json'],
plugins: ['unused-imports'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-unused-vars': 'off',
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': ['warn', { vars: 'all', varsIgnorePattern: '^_', args: 'after-used', argsIgnorePattern: '^_' }],
'no-undef': 'off',
'no-console': [process.env.CI ? 'error' : 'warn', { allow: ['warn', 'error', 'info'] }],
'prettier/prettier': 'error'
}
}

32
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,32 @@
---
name: Bug report
about: Create a report to help us improve
title: '[BUG]'
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Setup**
- OS: [e.g. iOS, Windows, Linux]
- Browser [e.g. chrome, safari]
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,13 @@
---
name: Feature request
about: Suggest an idea for this project
title: '[FEATURE]'
labels: ''
assignees: ''
---
**Describe the feature you'd like**
A clear and concise description of what you would like Flowise to have.
**Additional context**
Add any other context or screenshots about the feature request here.

36
.github/workflows/main.yml vendored Normal file
View File

@ -0,0 +1,36 @@
name: Node CI
on:
push:
branches:
- master
pull_request:
branches:
- '*'
permissions:
contents: read
jobs:
build:
strategy:
matrix:
platform: [ubuntu-latest]
node-version: [14.x, 16.x]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm i -g yarn
- run: yarn install --ignore-engines
- run: yarn lint
- run: yarn build

42
.gitignore vendored Normal file
View File

@ -0,0 +1,42 @@
# editor
.idea
.vscode
# dependencies
**/node_modules
**/package-lock.json
**/yarn.lock
## logs
**/*.log
## build
**/dist
**/build
## temp
**/tmp
**/temp
## test
**/coverage
# misc
.DS_Store
## env
.env.local
.env.development.local
.env.test.local
.env.production.local
.env
## turbo
.turbo
## secrets
**/*.key
**/api.json
## compressed
**/*.tgz

5
.husky/pre-commit Normal file
View File

@ -0,0 +1,5 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn quick # prettify
yarn lint-staged # eslint lint(also include prettify but prettify support more file extensions than eslint, so run prettify first)

3
.prettierignore Normal file
View File

@ -0,0 +1,3 @@
**/node_modules
**/dist
**/build

9
.prettierrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
printWidth: 140,
singleQuote: true,
jsxSingleQuote: true,
trailingComma: 'none',
tabWidth: 4,
semi: false,
endOfLine: 'auto'
}

74
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at hello@flowiseai.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

127
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,127 @@
<!-- markdownlint-disable MD030 -->
# Contributing to Flowise
We appreciate any form of contributions.
## ⭐ Star
Star and share the [Github Repo](https://github.com/FlowiseAI/Flowise).
## 🙋 Q&A
Search up for any questions in [Q&A section](https://github.com/FlowiseAI/Flowise/discussions/categories/q-a), if you can't find one, don't hesitate to create one. It might helps others that have similar question.
## 🙌 Share Chatflow
Yes! Sharing how you use Flowise is a way of contribution. Export your chatflow as JSON, attach a screenshot and share it in [Show and Tell section](https://github.com/FlowiseAI/Flowise/discussions/categories/show-and-tell).
## 💡 Ideas
Ideas are welcome such as new feature, apps integration, and blockchain networks. Submit in [Ideas section](https://github.com/FlowiseAI/Flowise/discussions/categories/ideas).
## 🐞 Report Bugs
Found an issue? [Report it](https://github.com/FlowiseAI/Flowise/issues/new/choose).
## 👨‍💻 Contribute to Code
Not sure what to contribute? Some ideas:
- Create new components from Langchain
- Update existing components such as extending functionality, fixing bugs
- Add new chatflow ideas
### Developers
Flowise has 3 different modules in a single mono repository.
- `server`: Node backend to serve API logics
- `ui`: React frontend
- `components`: Langchain components
#### Prerequisite
- Install Yarn
```bash
npm i -g yarn
```
#### Step by step
1. Fork the official [Flowise Github Repository](https://github.com/FlowiseAI/Flowise).
2. Clone your forked repository.
3. Create a new branch, see [guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-and-deleting-branches-within-your-repository). Naming conventions:
- For feature branch: `feature/<Your New Feature>`
- For bug fix branch: `bugfix/<Your New Bugfix>`.
4. Switch to the newly created branch.
5. Go into repository folder
```bash
cd Flowise
```
6. Install all dependencies of all modules:
```bash
yarn install
```
7. Build all the code:
```bash
yarn build
```
8. Start the app on [http://localhost:3000](http://localhost:3000)
```bash
yarn start
```
9. For development, run
```bash
yarn dev
```
Any changes made in `packages/ui` or `packages/server` will be reflected on [http://localhost:8080](http://localhost:8080)
For changes made in `packages/components`, run `yarn build` again to pickup the changes.
10. After making all the changes, run
```bash
yarn build
```
and
```bash
yarn start
```
to make sure everything works fine in production.
11. Commit code and submit Pull Request from forked branch pointing to [Flowise master](https://github.com/FlowiseAI/Flowise/tree/master).
## 📖 Contribute to Docs
In-Progress
## 🏷️ Pull Request process
A member of the FlowiseAI team will automatically be notified/assigned when you open a pull request. You can also reach out to us on [Discord](https://discord.gg/GWcGczPk).
## 📃 Contributor License Agreement
Before we can merge your contribution you have to sign our [Contributor License Agreement (CLA)](https://cla-assistant.io/FlowiseAI/Flowise). The CLA contains the terms and conditions under which the contribution is submitted. You need to do this only once for your first pull request. Keep in mind that without a signed CLA we cannot merge your contribution.
## 📜 Code of Conduct
This project and everyone participating in it are governed by the Code of Conduct which can be found in the [file](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to hello@flowiseai.com.

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
# Build local monorepo image
# docker build --no-cache -t flowise .
# Run image
# docker run -d -p 3000:3000 flowise
FROM node:16
WORKDIR /usr/src/packages
# Copy root package.json and lockfile
COPY package.json ./
COPY yarn.lock ./
# Copy components package.json
COPY packages/components/package.json ./packages/components/package.json
# Copy ui package.json
COPY packages/ui/package.json ./packages/ui/package.json
# Copy server package.json
COPY packages/server/package.json ./packages/server/package.json
RUN yarn install
# Copy app source
COPY . .
RUN yarn build
EXPOSE 3000
CMD [ "yarn", "start" ]

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
The MIT License
Copyright (c) 2023 FlowiseAI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

111
README.md Normal file
View File

@ -0,0 +1,111 @@
<!-- markdownlint-disable MD030 -->
# Flowise - LangchainJS UI
<a href="https://github.com/FlowiseAI/Flowise">
<img width="100%" src="https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true"></a>
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 FlowiseAI
```bash
npx flowise start
```
3. Open [http://localhost:3000](http://localhost:3000)
## 🐳 Docker
1. Go to `docker` folder at the root of the project
2. `docker-compose up -d`
3. This will automatically spins up mongodb and flowise containers
4. Open [http://localhost:3000](http://localhost:3000)
5. You can bring the containers down by `docker-compose stop`
## 👨‍💻 Developers
Flowise has 3 different modules in a single mono repository.
- `server`: Node backend to serve API logics
- `ui`: React frontend
- `components`: Langchain components
### Prerequisite
- Install Yarn
```bash
npm i -g yarn
```
### Setup
1. Clone the repository
```bash
git clone https://github.com/FlowiseAI/Flowise.git
```
2. Go into repository folder
```bash
cd Flowise
```
3. Install all dependencies of all modules:
```bash
yarn install
```
4. Build all the code:
```bash
yarn build
```
5. Start the app:
```bash
yarn start
```
You can now access the app on [http://localhost:3000](http://localhost:3000)
6. For development build:
```bash
yarn dev
```
Any code changes will reload the app automatically on [http://localhost:8080](http://localhost:8080)
## 📖 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](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](LICENSE.md).

BIN
assets/Demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

BIN
assets/FloWiseAI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/FloWiseAI_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
assets/FloWiseAI_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

13
babel.config.js Normal file
View File

@ -0,0 +1,13 @@
module.exports = {
presets: [
'@babel/preset-typescript',
[
'@babel/preset-env',
{
targets: {
node: 'current'
}
}
]
]
}

13
docker/Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM node:14.20.0-alpine
USER root
RUN apk add --no-cache git
RUN apk add --no-cache python3 py3-pip
# You can install a specific version like: flowise@1.0.0
RUN npm install -g flowise
WORKDIR /data
CMD "flowise"

23
docker/docker-compose.yml Normal file
View File

@ -0,0 +1,23 @@
version: '3.1'
services:
mongo:
image: mongo
ports:
- '27017:27017'
restart: always
environment:
- MONGO_INITDB_DATABASE=flowiseai
flowise:
image: flowiseai/flowise
restart: always
environment:
- PORT=${PORT}
ports:
- '${PORT}:${PORT}'
links:
- mongo
volumes:
- ~/.flowise:/root/.flowise
command: /bin/sh -c "sleep 3; flowise start"

BIN
images/flowise.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

55
package.json Normal file
View File

@ -0,0 +1,55 @@
{
"name": "flowise",
"version": "1.0.0",
"private": true,
"homepage": "https://flowiseai.com",
"workspaces": [
"packages/*",
"flowise",
"ui",
"components"
],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"start": "run-script-os",
"start:windows": "cd packages/server/bin && run start",
"start:default": "cd packages/server/bin && ./run start",
"clean": "npm exec -ws -- rimraf dist build",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"test": "turbo run test",
"lint": "eslint \"**/*.{js,jsx,ts,tsx,json,md}\"",
"lint-fix": "yarn lint --fix",
"quick": "pretty-quick --staged",
"postinstall": "husky install"
},
"lint-staged": {
"*.{js,jsx,ts,tsx,json,md}": "eslint --fix"
},
"devDependencies": {
"turbo": "1.7.4",
"@babel/preset-env": "^7.19.4",
"@babel/preset-typescript": "7.18.6",
"@types/express": "^4.17.13",
"@typescript-eslint/typescript-estree": "^5.39.0",
"eslint": "^8.24.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-markdown": "^3.0.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unused-imports": "^2.0.0",
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"pretty-quick": "^3.1.3",
"rimraf": "^3.0.2",
"run-script-os": "^1.1.6",
"typescript": "^4.8.4"
},
"engines": {
"node": ">=18.15.0"
}
}

View File

@ -0,0 +1,17 @@
<!-- markdownlint-disable MD030 -->
# Flowise Components
Apps integration for Flowise. Contain Nodes and Credentials.
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
Install:
```bash
npm i flowise-components
```
## License
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).

View File

@ -0,0 +1,9 @@
import gulp from 'gulp'
const { src, dest } = gulp
function copyIcons() {
return src(['nodes/**/*.{jpg,png,svg}']).pipe(dest('dist/nodes'))
}
exports.default = copyIcons

View File

@ -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 }

View File

@ -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

View File

@ -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 }

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dna" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z"></path>
<path d="M9.172 20.485a4 4 0 1 0 -5.657 -5.657"></path>
<path d="M14.828 3.515a4 4 0 0 0 5.657 5.657"></path>
</svg>

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@ -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 }

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,34 @@
{
"name": "flowise-components",
"version": "1.0.0",
"description": "Flowiseai Components",
"main": "dist/src/index",
"types": "dist/src/index.d.ts",
"scripts": {
"build": "tsc && gulp",
"dev": "tsc --watch"
},
"keywords": [],
"homepage": "https://flowiseai.com",
"author": {
"name": "Henry Heng",
"email": "henryheng@flowiseai.com"
},
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"axios": "^0.27.2",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"form-data": "^4.0.0",
"langchain": "^0.0.44",
"moment": "^2.29.3",
"node-fetch": "2",
"ws": "^8.9.0"
},
"devDependencies": {
"@types/gulp": "4.0.9",
"@types/ws": "^8.5.3",
"gulp": "^4.0.2",
"typescript": "^4.8.4"
}
}

View File

@ -0,0 +1,83 @@
/**
* Types
*/
export type NodeParamsType =
| 'asyncOptions'
| 'options'
| 'string'
| 'number'
| 'boolean'
| 'password'
| 'json'
| 'code'
| 'date'
| 'file'
| 'folder'
export type CommonType = string | number | boolean | undefined | null
/**
* Others
*/
export interface ICommonObject {
[key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[]
}
export interface IAttachment {
content: string
contentType: string
size?: number
filename?: string
}
export interface INodeOptionsValue {
label: string
name: string
description?: string
}
export interface INodeParams {
label: string
name: string
type: NodeParamsType | string
default?: CommonType | ICommonObject | ICommonObject[]
description?: string
options?: Array<INodeOptionsValue>
optional?: boolean | INodeDisplay
rows?: number
list?: boolean
placeholder?: string
}
export interface INodeExecutionData {
[key: string]: CommonType | CommonType[] | ICommonObject | ICommonObject[]
}
export interface INodeDisplay {
[key: string]: string[] | string
}
export interface INodeProperties {
label: string
name: string
type: string
icon: string
category: string
baseClasses: string[]
description?: string
filePath?: string
}
export interface INode extends INodeProperties {
inputs?: INodeParams[]
getBaseClasses?(): Promise<string[]>
getInstance?(nodeData: INodeData): Promise<string>
run?(nodeData: INodeData, input: string): Promise<string>
}
export interface INodeData extends INodeProperties {
inputs?: ICommonObject
instance?: any
}

View File

@ -0,0 +1,2 @@
export * from './Interface'
export * from './utils'

View File

@ -0,0 +1,144 @@
import * as fs from 'fs'
import * as path from 'path'
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
export const getBaseClasses = (targetClass: any) => {
const baseClasses: string[] = []
if (targetClass instanceof Function) {
let baseClass = targetClass
while (baseClass) {
const newBaseClass = Object.getPrototypeOf(baseClass)
if (newBaseClass && newBaseClass !== Object && newBaseClass.name) {
baseClass = newBaseClass
baseClasses.push(baseClass.name)
} else {
break
}
}
}
return baseClasses
}
/**
* Serialize axios query params
*
* @export
* @param {any} params
* @param {boolean} skipIndex // Set to true if you want same params to be: param=1&param=2 instead of: param[0]=1&param[1]=2
* @returns {string}
*/
export function serializeQueryParams(params: any, skipIndex?: boolean): string {
const parts: any[] = []
const encode = (val: string) => {
return encodeURIComponent(val)
.replace(/%3A/gi, ':')
.replace(/%24/g, '$')
.replace(/%2C/gi, ',')
.replace(/%20/g, '+')
.replace(/%5B/gi, '[')
.replace(/%5D/gi, ']')
}
const convertPart = (key: string, val: any) => {
if (val instanceof Date) val = val.toISOString()
else if (val instanceof Object) val = JSON.stringify(val)
parts.push(encode(key) + '=' + encode(val))
}
Object.entries(params).forEach(([key, val]) => {
if (val === null || typeof val === 'undefined') return
if (Array.isArray(val)) val.forEach((v, i) => convertPart(`${key}${skipIndex ? '' : `[${i}]`}`, v))
else convertPart(key, val)
})
return parts.join('&')
}
/**
* Handle error from try catch
*
* @export
* @param {any} error
* @returns {string}
*/
export function handleErrorMessage(error: any): string {
let errorMessage = ''
if (error.message) {
errorMessage += error.message + '. '
}
if (error.response && error.response.data) {
if (error.response.data.error) {
if (typeof error.response.data.error === 'object') errorMessage += JSON.stringify(error.response.data.error) + '. '
else if (typeof error.response.data.error === 'string') errorMessage += error.response.data.error + '. '
} else if (error.response.data.msg) errorMessage += error.response.data.msg + '. '
else if (error.response.data.Message) errorMessage += error.response.data.Message + '. '
else if (typeof error.response.data === 'string') errorMessage += error.response.data + '. '
}
if (!errorMessage) errorMessage = 'Unexpected Error.'
return errorMessage
}
/**
* Returns the path of node modules package
* @param {string} packageName
* @returns {string}
*/
export const getNodeModulesPackagePath = (packageName: string): string => {
const checkPaths = [
path.join(__dirname, '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', '..', '..', 'node_modules', packageName)
]
for (const checkPath of checkPaths) {
if (fs.existsSync(checkPath)) {
return checkPath
}
}
return ''
}
/**
* Get input variables
* @param {string} paramValue
* @returns {boolean}
*/
export const getInputVariables = (paramValue: string): string[] => {
let returnVal = paramValue
const variableStack = []
const inputVariables = []
let startIdx = 0
const endIdx = returnVal.length - 1
while (startIdx < endIdx) {
const substr = returnVal.substring(startIdx, startIdx + 1)
// Store the opening double curly bracket
if (substr === '{') {
variableStack.push({ substr, startIdx: startIdx + 1 })
}
// Found the complete variable
if (substr === '}' && variableStack.length > 0 && variableStack[variableStack.length - 1].substr === '{') {
const variableStartIdx = variableStack[variableStack.length - 1].startIdx
const variableEndIdx = startIdx
const variableFullPath = returnVal.substring(variableStartIdx, variableEndIdx)
inputVariables.push(variableFullPath)
variableStack.pop()
}
startIdx += 1
}
return inputVariables
}

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"lib": ["ES2020"],
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
"target": "ES2020", // or higher
"outDir": "./dist/",
"resolveJsonModule": true,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"sourceMap": true,
"strictPropertyInitialization": false,
"useUnknownInCatchVariables": false,
"declaration": true,
"module": "commonjs",
"moduleResolution": "node16"
},
"include": ["src", "nodes"]
}

45
packages/server/README.md Normal file
View File

@ -0,0 +1,45 @@
<!-- markdownlint-disable MD030 -->
# Flowise - LangchainJS UI
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
Drag & drop UI to build your customized LLM flow using [LangchainJS](https://github.com/hwchase17/langchainjs)
## ⚡Quick Start
1. Install Flowise
```bash
npm install -g flowise
```
2. Start Flowise
```bash
npx flowise start
```
3. Open [http://localhost:3000](http://localhost:3000)
## 📖 Documentation
Coming Soon
## 💻 Cloud Hosted
Coming Soon
## 🌐 Self Host
Coming Soon
## 🙋 Support
Feel free to ask any questions, raise problems, and request new features in [discussion](https://github.com/FlowiseAI/Flowise/discussions)
## 🙌 Contributing
See [contributing guide](https://github.com/FlowiseAI/Flowise/blob/master/CONTRIBUTING.md). Reach out to us at [Discord](https://discord.gg/GWcGczPk) if you have any questions or issues.
## 📄 License
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).

View File

@ -0,0 +1,3 @@
module.exports = {
extends: '../../babel.config.js'
}

17
packages/server/bin/dev Normal file
View File

@ -0,0 +1,17 @@
#!/usr/bin/env node
const oclif = require('@oclif/core')
const path = require('path')
const project = path.join(__dirname, '..', 'tsconfig.json')
// In dev mode -> use ts-node and dev plugins
process.env.NODE_ENV = 'development'
require('ts-node').register({ project })
// In dev mode, always show stack traces
oclif.settings.debug = true
// Start the CLI
oclif.run().then(oclif.flush).catch(oclif.Errors.handle)

View File

@ -0,0 +1,3 @@
@echo off
node "%~dp0\dev" %*

5
packages/server/bin/run Normal file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env node
const oclif = require('@oclif/core')
oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle'))

View File

@ -0,0 +1,3 @@
@echo off
node "%~dp0\run" %*

View File

@ -0,0 +1,6 @@
{
"ignore": ["**/*.spec.ts", ".git", "node_modules"],
"watch": ["commands", "index.ts", "src"],
"exec": "yarn oclif-dev",
"ext": "ts"
}

View File

@ -0,0 +1,70 @@
{
"name": "flowise",
"version": "1.0.0",
"description": "Flowiseai Server",
"main": "dist/index",
"types": "dist/index.d.ts",
"bin": {
"flowise": "./bin/run"
},
"files": [
"bin",
"dist",
"npm-shrinkwrap.json",
"oclif.manifest.json",
"oauth2.html",
".env"
],
"oclif": {
"bin": "flowise",
"commands": "./dist/commands"
},
"scripts": {
"build": "tsc",
"start": "run-script-os",
"start:windows": "cd bin && run start",
"start:default": "cd bin && ./run start",
"dev": "concurrently \"yarn watch\" \"nodemon\"",
"oclif-dev": "run-script-os",
"oclif-dev:windows": "cd bin && dev start",
"oclif-dev:default": "cd bin && ./dev start",
"postpack": "shx rm -f oclif.manifest.json",
"prepack": "yarn build && oclif manifest && oclif readme",
"typeorm": "typeorm-ts-node-commonjs",
"watch": "tsc --watch",
"version": "oclif readme && git add README.md"
},
"keywords": [],
"homepage": "https://flowiseai.com",
"author": {
"name": "Henry Heng",
"email": "henryheng@flowiseai.com"
},
"engines": {
"node": ">=18.15.0"
},
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
"@oclif/core": "^1.13.10",
"axios": "^0.27.2",
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.17.3",
"flowise-components": "*",
"flowise-ui": "*",
"moment-timezone": "^0.5.34",
"reflect-metadata": "^0.1.13",
"sqlite3": "^5.1.6",
"typeorm": "^0.3.6"
},
"devDependencies": {
"@types/cors": "^2.8.12",
"concurrently": "^7.1.0",
"nodemon": "^2.0.15",
"oclif": "^3",
"run-script-os": "^1.1.6",
"shx": "^0.3.3",
"ts-node": "^10.7.0",
"typescript": "^4.8.4"
}
}

View File

@ -0,0 +1,27 @@
import 'reflect-metadata'
import path from 'path'
import { DataSource } from 'typeorm'
import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage'
import { getUserHome } from './utils'
let appDataSource: DataSource
export const init = async (): Promise<void> => {
const homePath = path.join(getUserHome(), '.flowise')
appDataSource = new DataSource({
type: 'sqlite',
database: path.resolve(homePath, 'database.sqlite'),
synchronize: true,
entities: [ChatFlow, ChatMessage],
migrations: []
})
}
export function getDataSource(): DataSource {
if (appDataSource === undefined) {
init()
}
return appDataSource
}

View File

@ -0,0 +1,101 @@
import { INode, INodeData } from 'flowise-components'
export type MessageType = 'apiMessage' | 'userMessage'
/**
* Databases
*/
export interface IChatFlow {
id: string
name: string
flowData: string
deployed: boolean
updatedDate: Date
createdDate: Date
}
export interface IChatMessage {
id: string
role: MessageType
content: string
chatflowid: string
createdDate: Date
}
export interface IComponentNodesPool {
[key: string]: INode
}
export interface IVariableDict {
[key: string]: string
}
export interface INodeDependencies {
[key: string]: number
}
export interface INodeDirectedGraph {
[key: string]: string[]
}
export interface IReactFlowNode {
id: string
position: {
x: number
y: number
}
type: string
data: INodeData
positionAbsolute: {
x: number
y: number
}
z: number
handleBounds: {
source: any
target: any
}
width: number
height: number
selected: boolean
dragging: boolean
}
export interface IReactFlowEdge {
source: string
sourceHandle: string
target: string
targetHandle: string
type: string
id: string
data: {
label: string
}
}
export interface IReactFlowObject {
nodes: IReactFlowNode[]
edges: IReactFlowEdge[]
viewport: {
x: number
y: number
zoom: number
}
}
export interface IExploredNode {
[key: string]: {
remainingLoop: number
lastSeenDepth: number
}
}
export interface INodeQueue {
nodeId: string
depth: number
}
export interface IncomingInput {
question: string
history: string[]
}

View File

@ -0,0 +1,66 @@
import { IComponentNodesPool } from './Interface'
import path from 'path'
import { Dirent } from 'fs'
import { getNodeModulesPackagePath } from './utils'
import { promises } from 'fs'
export class NodesPool {
componentNodes: IComponentNodesPool = {}
/**
* Initialize to get all nodes
*/
async initialize() {
const packagePath = getNodeModulesPackagePath('flowise-components')
const nodesPath = path.join(packagePath, 'dist', 'nodes')
const nodeFiles = await this.getFiles(nodesPath)
return Promise.all(
nodeFiles.map(async (file) => {
if (file.endsWith('.js')) {
const nodeModule = await require(file)
try {
const newNodeInstance = new nodeModule.nodeClass()
newNodeInstance.filePath = file
const baseClasses = await newNodeInstance.getBaseClasses!.call(newNodeInstance)
newNodeInstance.baseClasses = baseClasses
this.componentNodes[newNodeInstance.name] = newNodeInstance
// Replace file icon with absolute path
if (
newNodeInstance.icon &&
(newNodeInstance.icon.endsWith('.svg') ||
newNodeInstance.icon.endsWith('.png') ||
newNodeInstance.icon.endsWith('.jpg'))
) {
const filePath = file.replace(/\\/g, '/').split('/')
filePath.pop()
const nodeIconAbsolutePath = `${filePath.join('/')}/${newNodeInstance.icon}`
this.componentNodes[newNodeInstance.name].icon = nodeIconAbsolutePath
}
} catch (e) {
// console.error(e);
}
}
})
)
}
/**
* Recursive function to get node files
* @param {string} dir
* @returns {string[]}
*/
async getFiles(dir: string): Promise<string[]> {
const dirents = await promises.readdir(dir, { withFileTypes: true })
const files = await Promise.all(
dirents.map((dirent: Dirent) => {
const res = path.resolve(dir, dirent.name)
return dirent.isDirectory() ? this.getFiles(res) : res
})
)
return Array.prototype.concat(...files)
}
}

View File

@ -0,0 +1,66 @@
import { Command, Flags } from '@oclif/core'
import path from 'path'
import * as Server from '../index'
import * as DataSource from '../DataSource'
import dotenv from 'dotenv'
dotenv.config({ path: path.join(__dirname, '..', '..', '.env') })
enum EXIT_CODE {
SUCCESS = 0,
FAILED = 1
}
let processExitCode = EXIT_CODE.SUCCESS
export default class Start extends Command {
static flags = {
mongourl: Flags.string()
}
static args = []
async stopProcess() {
console.info('Shutting down Flowise...')
try {
// Shut down the app after timeout if it ever stuck removing pools
setTimeout(() => {
console.info('Flowise was forced to shut down after 30 secs')
process.exit(processExitCode)
}, 30000)
// Removing pools
const serverApp = Server.getInstance()
if (serverApp) await serverApp.stopApp()
} catch (error) {
console.error('There was an error shutting down Flowise...', error)
}
process.exit(processExitCode)
}
async run(): Promise<void> {
process.on('SIGTERM', this.stopProcess)
process.on('SIGINT', this.stopProcess)
// Prevent throw new Error from crashing the app
// TODO: Get rid of this and send proper error message to ui
process.on('uncaughtException', (err) => {
console.error('uncaughtException: ', err)
})
const { flags } = await this.parse(Start)
if (flags.mongourl) process.env.MONGO_URL = flags.mongourl
await (async () => {
try {
this.log('Starting Flowise...')
await DataSource.init()
await Server.start()
} catch (error) {
console.error('There was an error starting Flowise...', error)
processExitCode = EXIT_CODE.FAILED
// @ts-ignore
process.emit('SIGINT')
}
})()
}
}

View File

@ -0,0 +1,24 @@
/* eslint-disable */
import { Entity, Column, CreateDateColumn, UpdateDateColumn, PrimaryGeneratedColumn } from 'typeorm'
import { IChatFlow } from '../Interface'
@Entity()
export class ChatFlow implements IChatFlow {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
name: string
@Column()
flowData: string
@Column()
deployed: boolean
@CreateDateColumn()
createdDate: Date
@UpdateDateColumn()
updatedDate: Date
}

View File

@ -0,0 +1,22 @@
/* eslint-disable */
import { Entity, Column, CreateDateColumn, PrimaryGeneratedColumn, Index } from 'typeorm'
import { IChatMessage, MessageType } from '../Interface'
@Entity()
export class ChatMessage implements IChatMessage {
@PrimaryGeneratedColumn('uuid')
id: string
@Column()
role: MessageType
@Index()
@Column()
chatflowid: string
@Column()
content: string
@CreateDateColumn()
createdDate: Date
}

View File

@ -0,0 +1,146 @@
export const workflow1 = {
nodes: [
{
width: 200,
height: 66,
id: 'promptTemplate_0',
position: {
x: 295.0571878493141,
y: 108.66221078850214
},
type: 'customNode',
data: {
label: 'Prompt Template',
name: 'promptTemplate',
type: 'PromptTemplate',
inputAnchors: [],
outputAnchors: [
{
id: 'promptTemplate_0-output-0'
}
],
selected: false,
inputs: {
template: 'What is a good name for a company that makes {product}?',
inputVariables: '["product"]'
}
},
selected: false,
positionAbsolute: {
x: 295.0571878493141,
y: 108.66221078850214
},
dragging: false
},
{
width: 200,
height: 66,
id: 'openAI_0',
position: {
x: 774,
y: 97.75
},
type: 'customNode',
data: {
label: 'OpenAI',
name: 'openAI',
type: 'OpenAI',
inputAnchors: [],
outputAnchors: [
{
id: 'openAI_0-output-0'
}
],
selected: false,
inputs: {
modelName: 'text-davinci-003',
temperature: '0.7',
openAIApiKey: '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'
},
calls: {
prompt: 'Hi, how are you?'
}
},
selected: false,
positionAbsolute: {
x: 774,
y: 97.75
},
dragging: false
},
{
width: 200,
height: 66,
id: 'llmChain_0',
position: {
x: 1034.233162523021,
y: 97.59868104260748
},
type: 'customNode',
data: {
label: 'LLM Chain',
name: 'llmChain',
type: 'LLMChain',
inputAnchors: [
{
id: 'llmChain_0-input-0'
}
],
outputAnchors: [
{
id: 'llmChain_0-output-0'
}
],
selected: false,
inputs: {
llm: '{{openAI_0.data.instance}}',
prompt: '{{promptTemplate_0.data.instance}}'
},
calls: {
variable: '{"product":"colorful socks"}'
}
},
selected: false,
positionAbsolute: {
x: 1034.233162523021,
y: 97.59868104260748
},
dragging: false
}
],
edges: [
{
source: 'nodeJS_0',
sourceHandle: 'nodeJS_0-output-0',
target: 'nodeJS_1',
targetHandle: 'nodeJS_1-input-0',
type: 'buttonedge',
id: 'nodeJS_0-nodeJS_0-output-0-nodeJS_1-nodeJS_1-input-0',
data: {
label: ''
}
},
{
source: 'webhook_0',
sourceHandle: 'webhook_0-output-0',
target: 'wait_0',
targetHandle: 'wait_0-input-0',
type: 'buttonedge',
id: 'webhook_0-webhook_0-output-0-wait_0-wait_0-input-0',
data: {
label: ''
}
},
{
source: 'wait_0',
sourceHandle: 'wait_0-output-0',
target: 'nodeJS_0',
targetHandle: 'nodeJS_0-input-0',
type: 'buttonedge',
id: 'wait_0-wait_0-output-0-nodeJS_0-nodeJS_0-input-0',
data: {
label: ''
}
}
]
}

View File

@ -0,0 +1,261 @@
import express, { Request, Response } from 'express'
import path from 'path'
import cors from 'cors'
import http from 'http'
import { IChatFlow, IComponentNodesPool, IncomingInput, IReactFlowNode, IReactFlowObject } from './Interface'
import { getNodeModulesPackagePath, getStartingNode, buildLangchain, getEndingNode, constructGraphs } from './utils'
import { cloneDeep } from 'lodash'
import { getDataSource } from './DataSource'
import { NodesPool } from './NodesPool'
import { ChatFlow } from './entity/ChatFlow'
import { ChatMessage } from './entity/ChatMessage'
export class App {
app: express.Application
componentNodes: IComponentNodesPool = {}
AppDataSource = getDataSource()
constructor() {
this.app = express()
}
async initDatabase() {
// Initialize database
this.AppDataSource.initialize()
.then(async () => {
console.info('📦[server]: Data Source has been initialized!')
// Initialize node instances
const nodesPool = new NodesPool()
await nodesPool.initialize()
this.componentNodes = nodesPool.componentNodes
})
.catch((err) => {
console.error('❌[server]: Error during Data Source initialization:', err)
})
}
async config() {
// Limit is needed to allow sending/receiving base64 encoded string
this.app.use(express.json({ limit: '50mb' }))
this.app.use(express.urlencoded({ limit: '50mb', extended: true }))
// Allow access from ui when yarn run dev
if (process.env.NODE_ENV !== 'production') {
this.app.use(cors({ credentials: true, origin: 'http://localhost:8080' }))
}
// ----------------------------------------
// Nodes
// ----------------------------------------
// Get all component nodes
this.app.get('/api/v1/nodes', (req: Request, res: Response) => {
const returnData = []
for (const nodeName in this.componentNodes) {
const clonedNode = cloneDeep(this.componentNodes[nodeName])
returnData.push(clonedNode)
}
return res.json(returnData)
})
// Get specific component node via name
this.app.get('/api/v1/nodes/:name', (req: Request, res: Response) => {
if (Object.prototype.hasOwnProperty.call(this.componentNodes, req.params.name)) {
return res.json(this.componentNodes[req.params.name])
} else {
throw new Error(`Node ${req.params.name} not found`)
}
})
// Returns specific component node icon via name
this.app.get('/api/v1/node-icon/:name', (req: Request, res: Response) => {
if (Object.prototype.hasOwnProperty.call(this.componentNodes, req.params.name)) {
const nodeInstance = this.componentNodes[req.params.name]
if (nodeInstance.icon === undefined) {
throw new Error(`Node ${req.params.name} icon not found`)
}
if (nodeInstance.icon.endsWith('.svg') || nodeInstance.icon.endsWith('.png') || nodeInstance.icon.endsWith('.jpg')) {
const filepath = nodeInstance.icon
res.sendFile(filepath)
} else {
throw new Error(`Node ${req.params.name} icon is missing icon`)
}
} else {
throw new Error(`Node ${req.params.name} not found`)
}
})
// ----------------------------------------
// Chatflows
// ----------------------------------------
// Get all chatflows
this.app.get('/api/v1/chatflows', async (req: Request, res: Response) => {
const chatflows: IChatFlow[] = await this.AppDataSource.getRepository(ChatFlow).find()
return res.json(chatflows)
})
// Get specific chatflow via id
this.app.get('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: req.params.id
})
if (chatflow) return res.json(chatflow)
return res.status(404).send(`Chatflow ${req.params.id} not found`)
})
// Save chatflow
this.app.post('/api/v1/chatflows', async (req: Request, res: Response) => {
const body = req.body
const newChatFlow = new ChatFlow()
Object.assign(newChatFlow, body)
const chatflow = this.AppDataSource.getRepository(ChatFlow).create(newChatFlow)
const results = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
return res.json(results)
})
// Update chatflow
this.app.put('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: req.params.id
})
if (!chatflow) {
res.status(404).send(`Chatflow ${req.params.id} not found`)
return
}
const body = req.body
const updateChatFlow = new ChatFlow()
Object.assign(updateChatFlow, body)
this.AppDataSource.getRepository(ChatFlow).merge(chatflow, updateChatFlow)
const result = await this.AppDataSource.getRepository(ChatFlow).save(chatflow)
return res.json(result)
})
// Delete chatflow via id
this.app.delete('/api/v1/chatflows/:id', async (req: Request, res: Response) => {
const results = await this.AppDataSource.getRepository(ChatFlow).delete({ id: req.params.id })
return res.json(results)
})
// ----------------------------------------
// ChatMessage
// ----------------------------------------
// Get all chatmessages from chatflowid
this.app.get('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
const chatmessages = await this.AppDataSource.getRepository(ChatMessage).findBy({
chatflowid: req.params.id
})
return res.json(chatmessages)
})
// Add chatmessages for chatflowid
this.app.post('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
const body = req.body
const newChatMessage = new ChatMessage()
Object.assign(newChatMessage, body)
const chatmessage = this.AppDataSource.getRepository(ChatMessage).create(newChatMessage)
const results = await this.AppDataSource.getRepository(ChatMessage).save(chatmessage)
return res.json(results)
})
// Delete all chatmessages from chatflowid
this.app.delete('/api/v1/chatmessage/:id', async (req: Request, res: Response) => {
const results = await this.AppDataSource.getRepository(ChatMessage).delete({ chatflowid: req.params.id })
return res.json(results)
})
// ----------------------------------------
// Prediction
// ----------------------------------------
// Send input message and get prediction result
this.app.post('/api/v1/prediction/:id', async (req: Request, res: Response) => {
try {
const incomingInput: IncomingInput = req.body
const chatflow = await this.AppDataSource.getRepository(ChatFlow).findOneBy({
id: req.params.id
})
if (!chatflow) return res.status(404).send(`Chatflow ${req.params.id} not found`)
const flowData = chatflow.flowData
const parsedFlowData: IReactFlowObject = JSON.parse(flowData)
const { graph, nodeDependencies } = constructGraphs(parsedFlowData.nodes, parsedFlowData.edges)
const startingNodeIds = getStartingNode(nodeDependencies)
const endingNodeId = getEndingNode(nodeDependencies, graph)
if (!endingNodeId) return res.status(500).send(`Ending node must be either Chain or Agent`)
const reactFlowNodes = await buildLangchain(startingNodeIds, parsedFlowData.nodes, graph, this.componentNodes)
const nodeToExecute = reactFlowNodes.find((node: IReactFlowNode) => node.id === endingNodeId)
if (!nodeToExecute) return res.status(404).send(`Node ${endingNodeId} not found`)
const nodeInstanceFilePath = this.componentNodes[nodeToExecute.data.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const nodeInstance = new nodeModule.nodeClass()
const result = await nodeInstance.run(nodeToExecute.data, incomingInput.question)
return res.json(result)
} catch (e: any) {
return res.status(500).send(e.message)
}
})
// ----------------------------------------
// Serve UI static
// ----------------------------------------
const packagePath = getNodeModulesPackagePath('flowise-ui')
const uiBuildPath = path.join(packagePath, 'build')
const uiHtmlPath = path.join(packagePath, 'build', 'index.html')
this.app.use('/', express.static(uiBuildPath))
// All other requests not handled will return React app
this.app.use((req, res) => {
res.sendFile(uiHtmlPath)
})
}
async stopApp() {
try {
const removePromises: any[] = []
await Promise.all(removePromises)
} catch (e) {
console.error(`❌[server]: Flowise Server shut down error: ${e}`)
}
}
}
let serverApp: App | undefined
export async function start(): Promise<void> {
serverApp = new App()
const port = parseInt(process.env.PORT || '', 10) || 3000
const server = http.createServer(serverApp.app)
await serverApp.initDatabase()
await serverApp.config()
server.listen(port, () => {
console.info(`⚡️[server]: Flowise Server is listening at ${port}`)
})
}
export function getInstance(): App | undefined {
return serverApp
}

View File

@ -0,0 +1,264 @@
import path from 'path'
import fs from 'fs'
import {
IComponentNodesPool,
IExploredNode,
INodeDependencies,
INodeDirectedGraph,
INodeQueue,
IReactFlowEdge,
IReactFlowNode
} from '../Interface'
import { cloneDeep, get } from 'lodash'
import { ICommonObject, INodeData } from 'flowise-components'
/**
* Returns the home folder path of the user if
* none can be found it falls back to the current
* working directory
*
*/
export const getUserHome = (): string => {
let variableName = 'HOME'
if (process.platform === 'win32') {
variableName = 'USERPROFILE'
}
if (process.env[variableName] === undefined) {
// If for some reason the variable does not exist
// fall back to current folder
return process.cwd()
}
return process.env[variableName] as string
}
/**
* Returns the path of node modules package
* @param {string} packageName
* @returns {string}
*/
export const getNodeModulesPackagePath = (packageName: string): string => {
const checkPaths = [
path.join(__dirname, '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', '..', 'node_modules', packageName),
path.join(__dirname, '..', '..', '..', '..', '..', 'node_modules', packageName)
]
for (const checkPath of checkPaths) {
if (fs.existsSync(checkPath)) {
return checkPath
}
}
return ''
}
/**
* Construct directed graph and node dependencies score
* @param {IReactFlowNode[]} reactFlowNodes
* @param {IReactFlowEdge[]} reactFlowEdges
*/
export const constructGraphs = (reactFlowNodes: IReactFlowNode[], reactFlowEdges: IReactFlowEdge[]) => {
const nodeDependencies = {} as INodeDependencies
const graph = {} as INodeDirectedGraph
for (let i = 0; i < reactFlowNodes.length; i += 1) {
const nodeId = reactFlowNodes[i].id
nodeDependencies[nodeId] = 0
graph[nodeId] = []
}
for (let i = 0; i < reactFlowEdges.length; i += 1) {
const source = reactFlowEdges[i].source
const target = reactFlowEdges[i].target
if (Object.prototype.hasOwnProperty.call(graph, source)) {
graph[source].push(target)
} else {
graph[source] = [target]
}
nodeDependencies[target] += 1
}
return { graph, nodeDependencies }
}
/**
* Get starting node and check if flow is valid
* @param {INodeDependencies} nodeDependencies
*/
export const getStartingNode = (nodeDependencies: INodeDependencies) => {
// Find starting node
const startingNodeIds = [] as string[]
Object.keys(nodeDependencies).forEach((nodeId) => {
if (nodeDependencies[nodeId] === 0) {
startingNodeIds.push(nodeId)
}
})
return startingNodeIds
}
export const getEndingNode = (nodeDependencies: INodeDependencies, graph: INodeDirectedGraph) => {
// Find starting node
let endingNodeId = ''
Object.keys(graph).forEach((nodeId) => {
if (!graph[nodeId].length && nodeDependencies[nodeId] > 0) {
endingNodeId = nodeId
}
})
return endingNodeId
}
/**
* Build langchain from start to end
* @param {string} startingNodeId
* @param {IReactFlowNode[]} reactFlowNodes
* @param {IReactFlowEdge[]} reactFlowEdges
* @param {INodeDirectedGraph} graph
* @param {IComponentNodesPool} componentNodes
* @param {string} clientId
* @param {any} io
*/
export const buildLangchain = async (
startingNodeIds: string[],
reactFlowNodes: IReactFlowNode[],
graph: INodeDirectedGraph,
componentNodes: IComponentNodesPool
) => {
const flowNodes = cloneDeep(reactFlowNodes)
// Create a Queue and add our initial node in it
const nodeQueue = [] as INodeQueue[]
const exploredNode = {} as IExploredNode
// In the case of infinite loop, only max 3 loops will be executed
const maxLoop = 3
for (let i = 0; i < startingNodeIds.length; i += 1) {
nodeQueue.push({ nodeId: startingNodeIds[i], depth: 0 })
exploredNode[startingNodeIds[i]] = { remainingLoop: maxLoop, lastSeenDepth: 0 }
}
while (nodeQueue.length) {
const { nodeId, depth } = nodeQueue.shift() as INodeQueue
const reactFlowNode = flowNodes.find((nd) => nd.id === nodeId)
const nodeIndex = flowNodes.findIndex((nd) => nd.id === nodeId)
if (!reactFlowNode || reactFlowNode === undefined || nodeIndex < 0) continue
try {
const nodeInstanceFilePath = componentNodes[reactFlowNode.data.name].filePath as string
const nodeModule = await import(nodeInstanceFilePath)
const newNodeInstance = new nodeModule.nodeClass()
const reactFlowNodeData: INodeData = resolveVariables(reactFlowNode.data, flowNodes)
flowNodes[nodeIndex].data.instance = await newNodeInstance.init(reactFlowNodeData)
} catch (e: any) {
console.error(e)
throw new Error(e)
}
const neighbourNodeIds = graph[nodeId]
const nextDepth = depth + 1
for (let i = 0; i < neighbourNodeIds.length; i += 1) {
const neighNodeId = neighbourNodeIds[i]
// If nodeId has been seen, cycle detected
if (Object.prototype.hasOwnProperty.call(exploredNode, neighNodeId)) {
const { remainingLoop, lastSeenDepth } = exploredNode[neighNodeId]
if (lastSeenDepth === nextDepth) continue
if (remainingLoop === 0) {
break
}
const remainingLoopMinusOne = remainingLoop - 1
exploredNode[neighNodeId] = { remainingLoop: remainingLoopMinusOne, lastSeenDepth: nextDepth }
nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth })
} else {
exploredNode[neighNodeId] = { remainingLoop: maxLoop, lastSeenDepth: nextDepth }
nodeQueue.push({ nodeId: neighNodeId, depth: nextDepth })
}
}
}
return flowNodes
}
/**
* Get variable value from outputResponses.output
* @param {string} paramValue
* @param {IReactFlowNode[]} reactFlowNodes
* @param {string} key
* @param {number} loopIndex
* @returns {string}
*/
export const getVariableValue = (paramValue: string, reactFlowNodes: IReactFlowNode[]) => {
let returnVal = paramValue
const variableStack = []
let startIdx = 0
const endIdx = returnVal.length - 1
while (startIdx < endIdx) {
const substr = returnVal.substring(startIdx, startIdx + 2)
// Store the opening double curly bracket
if (substr === '{{') {
variableStack.push({ substr, startIdx: startIdx + 2 })
}
// Found the complete variable
if (substr === '}}' && variableStack.length > 0 && variableStack[variableStack.length - 1].substr === '{{') {
const variableStartIdx = variableStack[variableStack.length - 1].startIdx
const variableEndIdx = startIdx
const variableFullPath = returnVal.substring(variableStartIdx, variableEndIdx)
// Split by first occurence of '.' to get just nodeId
const [variableNodeId, _] = variableFullPath.split('.')
const executedNode = reactFlowNodes.find((nd) => nd.id === variableNodeId)
if (executedNode) {
const variableInstance = get(executedNode.data, 'instance')
returnVal = variableInstance
}
variableStack.pop()
}
startIdx += 1
}
return returnVal
}
/**
* Loop through each inputs and resolve variable if neccessary
* @param {INodeData} reactFlowNodeData
* @param {IReactFlowNode[]} reactFlowNodes
* @returns {INodeData}
*/
export const resolveVariables = (reactFlowNodeData: INodeData, reactFlowNodes: IReactFlowNode[]): INodeData => {
const flowNodeData = cloneDeep(reactFlowNodeData)
const types = 'inputs'
const getParamValues = (paramsObj: ICommonObject) => {
for (const key in paramsObj) {
const paramValue: string = paramsObj[key]
if (Array.isArray(paramValue)) {
const resolvedInstances = []
for (const param of paramValue) {
const resolvedInstance = getVariableValue(param, reactFlowNodes)
resolvedInstances.push(resolvedInstance)
}
paramsObj[key] = resolvedInstances
} else {
const resolvedInstance = getVariableValue(paramValue, reactFlowNodes)
paramsObj[key] = resolvedInstance
}
}
}
const paramsObj = (flowNodeData as any)[types]
getParamValues(paramsObj)
return flowNodeData
}

View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": ["es2017"],
"target": "es2017" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
"module": "commonjs" /* Specify what module code is generated. */,
"outDir": "dist",
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"sourceMap": true,
"strictPropertyInitialization": false,
"declaration": true
},
"include": ["src"]
}

13
packages/ui/.npmignore Normal file
View File

@ -0,0 +1,13 @@
/tests
/src
/public
!build
yarn-debug.log*
yarn-error.log*
.eslintrc
.prettierignore
.prettierrc
jsconfig.json

17
packages/ui/README.md Normal file
View File

@ -0,0 +1,17 @@
<!-- markdownlint-disable MD030 -->
# Flowise UI
React frontend ui for Flowise.
![Flowise](https://github.com/FlowiseAI/Flowise/blob/main/images/flowise.gif?raw=true)
Install:
```bash
npm i flowise-ui
```
## License
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).

View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"baseUrl": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

76
packages/ui/package.json Normal file
View File

@ -0,0 +1,76 @@
{
"name": "flowise-ui",
"version": "1.0.1",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://flowiseai.com",
"author": {
"name": "HenryHeng",
"email": "henryheng@flowiseai.com"
},
"dependencies": {
"@emotion/cache": "^11.4.0",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@mui/icons-material": "^5.0.3",
"@mui/material": "^5.11.12",
"@tabler/icons": "^1.39.1",
"clsx": "^1.1.1",
"formik": "^2.2.6",
"framer-motion": "^4.1.13",
"history": "^5.0.0",
"html-react-parser": "^3.0.4",
"lodash": "^4.17.21",
"moment": "^2.29.3",
"notistack": "^2.0.4",
"prismjs": "^1.28.0",
"prop-types": "^15.7.2",
"react": "^18.2.0",
"react-datepicker": "^4.8.0",
"react-device-detect": "^1.17.0",
"react-dom": "^18.2.0",
"react-json-view": "^1.21.3",
"react-markdown": "^8.0.6",
"react-perfect-scrollbar": "^1.5.8",
"react-redux": "^8.0.5",
"react-router": "~6.3.0",
"react-router-dom": "~6.3.0",
"react-simple-code-editor": "^0.11.2",
"reactflow": "^11.5.6",
"redux": "^4.0.5",
"yup": "^0.32.9"
},
"scripts": {
"start": "react-scripts start",
"dev": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"babel": {
"presets": [
"@babel/preset-react"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/eslint-parser": "^7.15.8",
"@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^14.0.0",
"@testing-library/user-event": "^12.8.3",
"pretty-quick": "^3.1.3",
"react-scripts": "^5.0.1",
"sass": "^1.42.1",
"typescript": "^4.8.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flowise - LangchainJS UI</title>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- Meta Tags-->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#2296f3" />
<meta name="title" content="Flowise - LangchainJS UI" />
<meta name="description" content="Flowise helps you to better integrate Web3 with existing Web2 applications" />
<meta name="keywords" content="react, material-ui, reactjs, reactjs, workflow automation, web3, web2, blockchain" />
<meta name="author" content="CodedThemes" />
<!-- Open Graph / Facebook -->
<meta property="og:locale" content="en_US" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://flowiseai.com/" />
<meta property="og:site_name" content="flowiseai.com" />
<meta property="article:publisher" content="https://www.facebook.com/codedthemes" />
<meta property="og:title" content="Flowise - LangchainJS UI" />
<meta property="og:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
<meta property="og:image" content="https://flowiseai.com/og-image/og-facebook.png" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://flowiseai.com" />
<meta property="twitter:title" content="Flowise - LangchainJS UI" />
<meta property="twitter:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
<meta property="twitter:image" content="https://flowiseai.com/og-image/og-twitter.png" />
<meta name="twitter:creator" content="@codedthemes" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet"
/>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<div id="portal"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

32
packages/ui/src/App.js Normal file
View File

@ -0,0 +1,32 @@
import { useSelector } from 'react-redux'
import { ThemeProvider } from '@mui/material/styles'
import { CssBaseline, StyledEngineProvider } from '@mui/material'
// routing
import Routes from 'routes'
// defaultTheme
import themes from 'themes'
// project imports
import NavigationScroll from 'layout/NavigationScroll'
// ==============================|| APP ||============================== //
const App = () => {
const customization = useSelector((state) => state.customization)
return (
<StyledEngineProvider injectFirst>
<ThemeProvider theme={themes(customization)}>
<CssBaseline />
<NavigationScroll>
<Routes />
</NavigationScroll>
</ThemeProvider>
</StyledEngineProvider>
)
}
export default App

View File

@ -0,0 +1,19 @@
import client from './client'
const getAllChatflows = () => client.get('/chatflows')
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
const createNewChatflow = (body) => client.post(`/chatflows`, body)
const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body)
const deleteChatflow = (id) => client.delete(`/chatflows/${id}`)
export default {
getAllChatflows,
getSpecificChatflow,
createNewChatflow,
updateChatflow,
deleteChatflow
}

View File

@ -0,0 +1,13 @@
import client from './client'
const getChatmessageFromChatflow = (id) => client.get(`/chatmessage/${id}`)
const createNewChatmessage = (id, body) => client.post(`/chatmessage/${id}`, body)
const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`)
export default {
getChatmessageFromChatflow,
createNewChatmessage,
deleteChatmessage
}

View File

@ -0,0 +1,11 @@
import axios from 'axios'
import { baseURL } from 'store/constant'
const apiClient = axios.create({
baseURL: `${baseURL}/api/v1`,
headers: {
'Content-type': 'application/json'
}
})
export default apiClient

View File

@ -0,0 +1,10 @@
import client from './client'
const getAllNodes = () => client.get('/nodes')
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
export default {
getAllNodes,
getSpecificNode
}

View File

@ -0,0 +1,7 @@
import client from './client'
const sendMessageAndGetPrediction = (id, input) => client.post(`/prediction/${id}`, input)
export default {
sendMessageAndGetPrediction
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -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;
}

View File

@ -0,0 +1,122 @@
// color variants
@import 'themes-vars.module.scss';
// third-party
@import '~react-perfect-scrollbar/dist/css/styles.css';
// ==============================|| LIGHT BOX ||============================== //
.fullscreen .react-images__blanket {
z-index: 1200;
}
// ==============================|| PERFECT SCROLLBAR ||============================== //
.scrollbar-container {
.ps__rail-y {
&:hover > .ps__thumb-y,
&:focus > .ps__thumb-y,
&.ps--clicking .ps__thumb-y {
background-color: $grey500;
width: 5px;
}
}
.ps__thumb-y {
background-color: $grey500;
border-radius: 6px;
width: 5px;
right: 0;
}
}
.scrollbar-container.ps,
.scrollbar-container > .ps {
&.ps--active-y > .ps__rail-y {
width: 5px;
background-color: transparent !important;
z-index: 999;
&:hover,
&.ps--clicking {
width: 5px;
background-color: transparent;
}
}
&.ps--scrolling-y > .ps__rail-y,
&.ps--scrolling-x > .ps__rail-x {
opacity: 0.4;
background-color: transparent;
}
}
// ==============================|| ANIMATION KEYFRAMES ||============================== //
@keyframes wings {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0px);
}
}
@keyframes blink {
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes bounce {
0%,
20%,
53%,
to {
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translateZ(0);
}
40%,
43% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -5px, 0);
}
70% {
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
transform: translate3d(0, -7px, 0);
}
80% {
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
transform: translateZ(0);
}
90% {
transform: translate3d(0, -2px, 0);
}
}
@keyframes slideY {
0%,
50%,
100% {
transform: translateY(0px);
}
25% {
transform: translateY(-10px);
}
75% {
transform: translateY(10px);
}
}
@keyframes slideX {
0%,
50%,
100% {
transform: translateX(0px);
}
25% {
transform: translateX(-10px);
}
75% {
transform: translateX(10px);
}
}

View File

@ -0,0 +1,9 @@
const config = {
// basename: only at build time to set, and Don't add '/' at end off BASENAME for breadcrumbs, also Don't put only '/' use blank('') instead,
basename: '',
defaultPath: '/chatflows',
fontFamily: `'Roboto', sans-serif`,
borderRadius: 12
}
export default config

View File

@ -0,0 +1,26 @@
import { useState } from 'react'
export default (apiFunc) => {
const [data, setData] = useState(null)
const [error, setError] = useState(null)
const [loading, setLoading] = useState(false)
const request = async (...args) => {
setLoading(true)
try {
const result = await apiFunc(...args)
setData(result.data)
} catch (err) {
setError(err || 'Unexpected Error!')
} finally {
setLoading(false)
}
}
return {
data,
error,
loading,
request
}
}

View File

@ -0,0 +1,37 @@
import { useContext } from 'react'
import ConfirmContext from 'store/context/ConfirmContext'
import { HIDE_CONFIRM, SHOW_CONFIRM } from 'store/actions'
let resolveCallback
const useConfirm = () => {
const [confirmState, dispatch] = useContext(ConfirmContext)
const closeConfirm = () => {
dispatch({
type: HIDE_CONFIRM
})
}
const onConfirm = () => {
closeConfirm()
resolveCallback(true)
}
const onCancel = () => {
closeConfirm()
resolveCallback(false)
}
const confirm = (confirmPayload) => {
dispatch({
type: SHOW_CONFIRM,
payload: confirmPayload
})
return new Promise((res) => {
resolveCallback = res
})
}
return { confirm, onConfirm, onCancel, confirmState }
}
export default useConfirm

View File

@ -0,0 +1,18 @@
import { useEffect, useRef } from 'react'
// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
const useScriptRef = () => {
const scripted = useRef(true)
useEffect(
() => () => {
scripted.current = false
},
[]
)
return scripted
}
export default useScriptRef

33
packages/ui/src/index.js Normal file
View File

@ -0,0 +1,33 @@
import React from 'react'
import App from './App'
import { store } from 'store'
import { createRoot } from 'react-dom/client'
// style + assets
import 'assets/scss/style.scss'
// third party
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { SnackbarProvider } from 'notistack'
import ConfirmContextProvider from 'store/context/ConfirmContextProvider'
import { ReactFlowContext } from 'store/context/ReactFlowContext'
const container = document.getElementById('root')
const root = createRoot(container)
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<SnackbarProvider>
<ConfirmContextProvider>
<ReactFlowContext>
<App />
</ReactFlowContext>
</ConfirmContextProvider>
</SnackbarProvider>
</BrowserRouter>
</Provider>
</React.StrictMode>
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,107 @@
import { useEffect } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Outlet } from 'react-router-dom'
// material-ui
import { styled, useTheme } from '@mui/material/styles'
import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material'
// project imports
import Header from './Header'
import Sidebar from './Sidebar'
import { drawerWidth } from 'store/constant'
import { SET_MENU } from 'store/actions'
// styles
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
...theme.typography.mainContent,
...(!open && {
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
[theme.breakpoints.up('md')]: {
marginLeft: -(drawerWidth - 20),
width: `calc(100% - ${drawerWidth}px)`
},
[theme.breakpoints.down('md')]: {
marginLeft: '20px',
width: `calc(100% - ${drawerWidth}px)`,
padding: '16px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px',
width: `calc(100% - ${drawerWidth}px)`,
padding: '16px',
marginRight: '10px'
}
}),
...(open && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen
}),
marginLeft: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: `calc(100% - ${drawerWidth}px)`,
[theme.breakpoints.down('md')]: {
marginLeft: '20px'
},
[theme.breakpoints.down('sm')]: {
marginLeft: '10px'
}
})
}))
// ==============================|| MAIN LAYOUT ||============================== //
const MainLayout = () => {
const theme = useTheme()
const matchDownMd = useMediaQuery(theme.breakpoints.down('lg'))
// Handle left drawer
const leftDrawerOpened = useSelector((state) => state.customization.opened)
const dispatch = useDispatch()
const handleLeftDrawerToggle = () => {
dispatch({ type: SET_MENU, opened: !leftDrawerOpened })
}
useEffect(() => {
dispatch({ type: SET_MENU, opened: !matchDownMd })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [matchDownMd])
return (
<Box sx={{ display: 'flex' }}>
<CssBaseline />
{/* header */}
<AppBar
enableColorOnDark
position='fixed'
color='inherit'
elevation={0}
sx={{
bgcolor: theme.palette.background.default,
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
}}
>
<Toolbar>
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
</Toolbar>
</AppBar>
{/* drawer */}
<Sidebar drawerOpen={leftDrawerOpened} drawerToggle={handleLeftDrawerToggle} />
{/* main content */}
<Main theme={theme} open={leftDrawerOpened}>
<Outlet />
</Main>
</Box>
)
}
export default MainLayout

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