Initial push
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
dist
|
||||
build
|
||||
|
||||
**/node_modules
|
||||
**/build
|
||||
**/dist
|
||||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
**/node_modules
|
||||
**/dist
|
||||
**/build
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = {
|
||||
printWidth: 140,
|
||||
singleQuote: true,
|
||||
jsxSingleQuote: true,
|
||||
trailingComma: 'none',
|
||||
tabWidth: 4,
|
||||
semi: false,
|
||||
endOfLine: 'auto'
|
||||
}
|
||||
|
|
@ -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/
|
||||
|
|
@ -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.
|
||||
|
|
@ -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" ]
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).
|
||||
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
|
@ -0,0 +1,13 @@
|
|||
module.exports = {
|
||||
presets: [
|
||||
'@babel/preset-typescript',
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
node: 'current'
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
|
After Width: | Height: | Size: 4.7 MiB |
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<!-- markdownlint-disable MD030 -->
|
||||
|
||||
# Flowise Components
|
||||
|
||||
Apps integration for Flowise. Contain Nodes and Credentials.
|
||||
|
||||

|
||||
|
||||
Install:
|
||||
|
||||
```bash
|
||||
npm i flowise-components
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import gulp from 'gulp'
|
||||
|
||||
const { src, dest } = gulp
|
||||
|
||||
function copyIcons() {
|
||||
return src(['nodes/**/*.{jpg,png,svg}']).pipe(dest('dist/nodes'))
|
||||
}
|
||||
|
||||
exports.default = copyIcons
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
|
||||
class MRLKAgentLLM implements INode {
|
||||
label: string
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
inputs: INodeParams[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'MRLK Agent for LLMs'
|
||||
this.name = 'mrlkAgentLLM'
|
||||
this.type = 'AgentExecutor'
|
||||
this.category = 'Agents'
|
||||
this.icon = 'agent.svg'
|
||||
this.description = 'Agent that uses the ReAct Framework to decide what action to take, optimized to be used with LLMs'
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Allowed Tools',
|
||||
name: 'tools',
|
||||
type: 'Tool',
|
||||
list: true
|
||||
},
|
||||
{
|
||||
label: 'LLM Model',
|
||||
name: 'model',
|
||||
type: 'BaseLanguageModel'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async getBaseClasses(): Promise<string[]> {
|
||||
return ['AgentExecutor']
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const { initializeAgentExecutor } = await import('langchain/agents')
|
||||
|
||||
const model = nodeData.inputs?.model
|
||||
const tools = nodeData.inputs?.tools
|
||||
|
||||
const executor = await initializeAgentExecutor(tools, model, 'zero-shot-react-description', true)
|
||||
|
||||
return executor
|
||||
}
|
||||
|
||||
async run(nodeData: INodeData, input: string): Promise<string> {
|
||||
const executor = nodeData.instance
|
||||
const result = await executor.call({ input })
|
||||
|
||||
return result?.output
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: MRLKAgentLLM }
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-robot" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M7 7h10a2 2 0 0 1 2 2v1l1 1v3l-1 1v3a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-3l-1 -1v-3l1 -1v-1a2 2 0 0 1 2 -2z"></path>
|
||||
<path d="M10 16h4"></path>
|
||||
<circle cx="8.5" cy="11.5" r=".5" fill="currentColor"></circle>
|
||||
<circle cx="15.5" cy="11.5" r=".5" fill="currentColor"></circle>
|
||||
<path d="M9 7l-1 -4"></path>
|
||||
<path d="M15 7l1 -4"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 650 B |
|
|
@ -0,0 +1,61 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { getBaseClasses } from '../../../src/utils'
|
||||
|
||||
class LLMChain_Chains implements INode {
|
||||
label: string
|
||||
name: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
description: string
|
||||
inputs: INodeParams[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'LLM Chain'
|
||||
this.name = 'llmChain'
|
||||
this.type = 'LLMChain'
|
||||
this.icon = 'chain.svg'
|
||||
this.category = 'Chains'
|
||||
this.description = 'Chain to run queries against LLMs'
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'LLM',
|
||||
name: 'llm',
|
||||
type: 'BaseLanguageModel'
|
||||
},
|
||||
{
|
||||
label: 'Prompt',
|
||||
name: 'prompt',
|
||||
type: 'BasePromptTemplate'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async getBaseClasses(): Promise<string[]> {
|
||||
const { LLMChain } = await import('langchain/chains')
|
||||
return getBaseClasses(LLMChain)
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const { LLMChain } = await import('langchain/chains')
|
||||
|
||||
const llm = nodeData.inputs?.llm
|
||||
const prompt = nodeData.inputs?.prompt
|
||||
|
||||
const chain = new LLMChain({ llm, prompt })
|
||||
return chain
|
||||
}
|
||||
|
||||
async run(nodeData: INodeData, input: string): Promise<string> {
|
||||
const prompt = nodeData.instance.prompt.inputVariables // ["product"]
|
||||
if (prompt.length > 1) throw new Error('Prompt can only contains 1 literal string {}. Multiples are found')
|
||||
|
||||
const chain = nodeData.instance
|
||||
const res = await chain.run(input)
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: LLMChain_Chains }
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-dna" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M14.828 14.828a4 4 0 1 0 -5.656 -5.656a4 4 0 0 0 5.656 5.656z"></path>
|
||||
<path d="M9.172 20.485a4 4 0 1 0 -5.657 -5.657"></path>
|
||||
<path d="M14.828 3.515a4 4 0 0 0 5.657 5.657"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 489 B |
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,83 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { getBaseClasses } from '../../../src/utils'
|
||||
|
||||
class OpenAI_LLMs implements INode {
|
||||
label: string
|
||||
name: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
description: string
|
||||
baseClasses: string[]
|
||||
inputs: INodeParams[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'OpenAI'
|
||||
this.name = 'openAI'
|
||||
this.type = 'OpenAI'
|
||||
this.icon = 'openai.png'
|
||||
this.category = 'LLMs'
|
||||
this.description = 'Wrapper around OpenAI large language models'
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'OpenAI Api Key',
|
||||
name: 'openAIApiKey',
|
||||
type: 'password'
|
||||
},
|
||||
{
|
||||
label: 'Model Name',
|
||||
name: 'modelName',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
label: 'text-davinci-003',
|
||||
name: 'text-davinci-003'
|
||||
},
|
||||
{
|
||||
label: 'text-davinci-002',
|
||||
name: 'text-davinci-002'
|
||||
},
|
||||
{
|
||||
label: 'text-curie-001',
|
||||
name: 'text-curie-001'
|
||||
},
|
||||
{
|
||||
label: 'text-babbage-001',
|
||||
name: 'text-babbage-001'
|
||||
}
|
||||
],
|
||||
default: 'text-davinci-003',
|
||||
optional: true
|
||||
},
|
||||
{
|
||||
label: 'Temperature',
|
||||
name: 'temperature',
|
||||
type: 'number',
|
||||
default: 0.7,
|
||||
optional: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async getBaseClasses(): Promise<string[]> {
|
||||
const { OpenAI } = await import('langchain/llms')
|
||||
return getBaseClasses(OpenAI)
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const { OpenAI } = await import('langchain/llms')
|
||||
|
||||
const temperature = nodeData.inputs?.temperature as string
|
||||
const modelName = nodeData.inputs?.modelName as string
|
||||
const openAIApiKey = nodeData.inputs?.openAIApiKey as string
|
||||
|
||||
const model = new OpenAI({
|
||||
temperature: parseInt(temperature, 10),
|
||||
modelName,
|
||||
openAIApiKey
|
||||
})
|
||||
return model
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: OpenAI_LLMs }
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
|
@ -0,0 +1,56 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { getBaseClasses, getInputVariables } from '../../../src/utils'
|
||||
|
||||
class PromptTemplate_Prompts implements INode {
|
||||
label: string
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
inputs: INodeParams[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'Prompt Template'
|
||||
this.name = 'promptTemplate'
|
||||
this.type = 'PromptTemplate'
|
||||
this.icon = 'prompt.svg'
|
||||
this.category = 'Prompts'
|
||||
this.description = 'Schema to represent a basic prompt for an LLM. Template can only contains 1 literal string {}'
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Template',
|
||||
name: 'template',
|
||||
type: 'string',
|
||||
rows: 5,
|
||||
default: 'What is a good name for a company that makes {product}?',
|
||||
placeholder: 'What is a good name for a company that makes {product}?'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async getBaseClasses(): Promise<string[]> {
|
||||
const { PromptTemplate } = await import('langchain/prompts')
|
||||
return getBaseClasses(PromptTemplate)
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const { PromptTemplate } = await import('langchain/prompts')
|
||||
|
||||
const template = nodeData.inputs?.template as string
|
||||
const inputVariables = getInputVariables(template)
|
||||
|
||||
try {
|
||||
const prompt = new PromptTemplate({
|
||||
template,
|
||||
inputVariables: inputVariables
|
||||
})
|
||||
return prompt
|
||||
} catch (e) {
|
||||
throw new Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: PromptTemplate_Prompts }
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-terminal-2" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M8 9l3 3l-3 3"></path>
|
||||
<path d="M13 15l3 0"></path>
|
||||
<path d="M3 4m0 2a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 465 B |
|
|
@ -0,0 +1,33 @@
|
|||
import { INode } from '../../../src/Interface'
|
||||
import { getBaseClasses } from '../../../src/utils'
|
||||
|
||||
class Calculator implements INode {
|
||||
label: string
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'Calculator'
|
||||
this.name = 'calculator'
|
||||
this.type = 'Calculator'
|
||||
this.icon = 'calculator.svg'
|
||||
this.category = 'Tools'
|
||||
this.description = 'Perform calculations on response'
|
||||
}
|
||||
|
||||
async getBaseClasses(): Promise<string[]> {
|
||||
const { Calculator } = await import('langchain/tools')
|
||||
return getBaseClasses(Calculator)
|
||||
}
|
||||
|
||||
async init(): Promise<any> {
|
||||
const { Calculator } = await import('langchain/tools')
|
||||
return new Calculator()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: Calculator }
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-calculator" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
|
||||
<path d="M4 3m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"></path>
|
||||
<path d="M8 7m0 1a1 1 0 0 1 1 -1h6a1 1 0 0 1 1 1v1a1 1 0 0 1 -1 1h-6a1 1 0 0 1 -1 -1z"></path>
|
||||
<path d="M8 14l0 .01"></path>
|
||||
<path d="M12 14l0 .01"></path>
|
||||
<path d="M16 14l0 .01"></path>
|
||||
<path d="M8 17l0 .01"></path>
|
||||
<path d="M12 17l0 .01"></path>
|
||||
<path d="M16 17l0 .01"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 698 B |
|
|
@ -0,0 +1,42 @@
|
|||
import { INode, INodeData, INodeParams } from '../../../src/Interface'
|
||||
import { getBaseClasses } from '../../../src/utils'
|
||||
|
||||
class SerpAPI implements INode {
|
||||
label: string
|
||||
name: string
|
||||
description: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
inputs: INodeParams[]
|
||||
|
||||
constructor() {
|
||||
this.label = 'Serp API'
|
||||
this.name = 'serpAPI'
|
||||
this.type = 'SerpAPI'
|
||||
this.icon = 'serp.png'
|
||||
this.category = 'Tools'
|
||||
this.description = 'Wrapper around SerpAPI - a real-time API to access Google search results'
|
||||
this.inputs = [
|
||||
{
|
||||
label: 'Serp Api Key',
|
||||
name: 'apiKey',
|
||||
type: 'password'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
async getBaseClasses(): Promise<string[]> {
|
||||
const { SerpAPI } = await import('langchain/tools')
|
||||
return getBaseClasses(SerpAPI)
|
||||
}
|
||||
|
||||
async init(nodeData: INodeData): Promise<any> {
|
||||
const { SerpAPI } = await import('langchain/tools')
|
||||
const apiKey = nodeData.inputs?.apiKey as string
|
||||
return new SerpAPI(apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { nodeClass: SerpAPI }
|
||||
|
After Width: | Height: | Size: 7.3 KiB |
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "flowise-components",
|
||||
"version": "1.0.0",
|
||||
"description": "Flowiseai Components",
|
||||
"main": "dist/src/index",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc && gulp",
|
||||
"dev": "tsc --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"homepage": "https://flowiseai.com",
|
||||
"author": {
|
||||
"name": "Henry Heng",
|
||||
"email": "henryheng@flowiseai.com"
|
||||
},
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.17.3",
|
||||
"form-data": "^4.0.0",
|
||||
"langchain": "^0.0.44",
|
||||
"moment": "^2.29.3",
|
||||
"node-fetch": "2",
|
||||
"ws": "^8.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.9",
|
||||
"@types/ws": "^8.5.3",
|
||||
"gulp": "^4.0.2",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* Types
|
||||
*/
|
||||
|
||||
export type NodeParamsType =
|
||||
| 'asyncOptions'
|
||||
| 'options'
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'password'
|
||||
| 'json'
|
||||
| 'code'
|
||||
| 'date'
|
||||
| 'file'
|
||||
| 'folder'
|
||||
|
||||
export type CommonType = string | number | boolean | undefined | null
|
||||
|
||||
/**
|
||||
* Others
|
||||
*/
|
||||
|
||||
export interface ICommonObject {
|
||||
[key: string]: any | CommonType | ICommonObject | CommonType[] | ICommonObject[]
|
||||
}
|
||||
|
||||
export interface IAttachment {
|
||||
content: string
|
||||
contentType: string
|
||||
size?: number
|
||||
filename?: string
|
||||
}
|
||||
|
||||
export interface INodeOptionsValue {
|
||||
label: string
|
||||
name: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface INodeParams {
|
||||
label: string
|
||||
name: string
|
||||
type: NodeParamsType | string
|
||||
default?: CommonType | ICommonObject | ICommonObject[]
|
||||
description?: string
|
||||
options?: Array<INodeOptionsValue>
|
||||
optional?: boolean | INodeDisplay
|
||||
rows?: number
|
||||
list?: boolean
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export interface INodeExecutionData {
|
||||
[key: string]: CommonType | CommonType[] | ICommonObject | ICommonObject[]
|
||||
}
|
||||
|
||||
export interface INodeDisplay {
|
||||
[key: string]: string[] | string
|
||||
}
|
||||
|
||||
export interface INodeProperties {
|
||||
label: string
|
||||
name: string
|
||||
type: string
|
||||
icon: string
|
||||
category: string
|
||||
baseClasses: string[]
|
||||
description?: string
|
||||
filePath?: string
|
||||
}
|
||||
|
||||
export interface INode extends INodeProperties {
|
||||
inputs?: INodeParams[]
|
||||
getBaseClasses?(): Promise<string[]>
|
||||
getInstance?(nodeData: INodeData): Promise<string>
|
||||
run?(nodeData: INodeData, input: string): Promise<string>
|
||||
}
|
||||
|
||||
export interface INodeData extends INodeProperties {
|
||||
inputs?: ICommonObject
|
||||
instance?: any
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './Interface'
|
||||
export * from './utils'
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
export const numberOrExpressionRegex = '^(\\d+\\.?\\d*|{{.*}})$' //return true if string consists only numbers OR expression {{}}
|
||||
export const notEmptyRegex = '(.|\\s)*\\S(.|\\s)*' //return true if string is not empty or blank
|
||||
|
||||
export const getBaseClasses = (targetClass: any) => {
|
||||
const baseClasses: string[] = []
|
||||
|
||||
if (targetClass instanceof Function) {
|
||||
let baseClass = targetClass
|
||||
|
||||
while (baseClass) {
|
||||
const newBaseClass = Object.getPrototypeOf(baseClass)
|
||||
if (newBaseClass && newBaseClass !== Object && newBaseClass.name) {
|
||||
baseClass = newBaseClass
|
||||
baseClasses.push(baseClass.name)
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return baseClasses
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize axios query params
|
||||
*
|
||||
* @export
|
||||
* @param {any} params
|
||||
* @param {boolean} skipIndex // Set to true if you want same params to be: param=1¶m=2 instead of: param[0]=1¶m[1]=2
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeQueryParams(params: any, skipIndex?: boolean): string {
|
||||
const parts: any[] = []
|
||||
|
||||
const encode = (val: string) => {
|
||||
return encodeURIComponent(val)
|
||||
.replace(/%3A/gi, ':')
|
||||
.replace(/%24/g, '$')
|
||||
.replace(/%2C/gi, ',')
|
||||
.replace(/%20/g, '+')
|
||||
.replace(/%5B/gi, '[')
|
||||
.replace(/%5D/gi, ']')
|
||||
}
|
||||
|
||||
const convertPart = (key: string, val: any) => {
|
||||
if (val instanceof Date) val = val.toISOString()
|
||||
else if (val instanceof Object) val = JSON.stringify(val)
|
||||
|
||||
parts.push(encode(key) + '=' + encode(val))
|
||||
}
|
||||
|
||||
Object.entries(params).forEach(([key, val]) => {
|
||||
if (val === null || typeof val === 'undefined') return
|
||||
|
||||
if (Array.isArray(val)) val.forEach((v, i) => convertPart(`${key}${skipIndex ? '' : `[${i}]`}`, v))
|
||||
else convertPart(key, val)
|
||||
})
|
||||
|
||||
return parts.join('&')
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle error from try catch
|
||||
*
|
||||
* @export
|
||||
* @param {any} error
|
||||
* @returns {string}
|
||||
*/
|
||||
export function handleErrorMessage(error: any): string {
|
||||
let errorMessage = ''
|
||||
|
||||
if (error.message) {
|
||||
errorMessage += error.message + '. '
|
||||
}
|
||||
|
||||
if (error.response && error.response.data) {
|
||||
if (error.response.data.error) {
|
||||
if (typeof error.response.data.error === 'object') errorMessage += JSON.stringify(error.response.data.error) + '. '
|
||||
else if (typeof error.response.data.error === 'string') errorMessage += error.response.data.error + '. '
|
||||
} else if (error.response.data.msg) errorMessage += error.response.data.msg + '. '
|
||||
else if (error.response.data.Message) errorMessage += error.response.data.Message + '. '
|
||||
else if (typeof error.response.data === 'string') errorMessage += error.response.data + '. '
|
||||
}
|
||||
|
||||
if (!errorMessage) errorMessage = 'Unexpected Error.'
|
||||
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path of node modules package
|
||||
* @param {string} packageName
|
||||
* @returns {string}
|
||||
*/
|
||||
export const getNodeModulesPackagePath = (packageName: string): string => {
|
||||
const checkPaths = [
|
||||
path.join(__dirname, '..', 'node_modules', packageName),
|
||||
path.join(__dirname, '..', '..', 'node_modules', packageName),
|
||||
path.join(__dirname, '..', '..', '..', 'node_modules', packageName),
|
||||
path.join(__dirname, '..', '..', '..', '..', 'node_modules', packageName),
|
||||
path.join(__dirname, '..', '..', '..', '..', '..', 'node_modules', packageName)
|
||||
]
|
||||
for (const checkPath of checkPaths) {
|
||||
if (fs.existsSync(checkPath)) {
|
||||
return checkPath
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Get input variables
|
||||
* @param {string} paramValue
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export const getInputVariables = (paramValue: string): string[] => {
|
||||
let returnVal = paramValue
|
||||
const variableStack = []
|
||||
const inputVariables = []
|
||||
let startIdx = 0
|
||||
const endIdx = returnVal.length - 1
|
||||
|
||||
while (startIdx < endIdx) {
|
||||
const substr = returnVal.substring(startIdx, startIdx + 1)
|
||||
|
||||
// Store the opening double curly bracket
|
||||
if (substr === '{') {
|
||||
variableStack.push({ substr, startIdx: startIdx + 1 })
|
||||
}
|
||||
|
||||
// Found the complete variable
|
||||
if (substr === '}' && variableStack.length > 0 && variableStack[variableStack.length - 1].substr === '{') {
|
||||
const variableStartIdx = variableStack[variableStack.length - 1].startIdx
|
||||
const variableEndIdx = startIdx
|
||||
const variableFullPath = returnVal.substring(variableStartIdx, variableEndIdx)
|
||||
inputVariables.push(variableFullPath)
|
||||
variableStack.pop()
|
||||
}
|
||||
startIdx += 1
|
||||
}
|
||||
return inputVariables
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ES2020"],
|
||||
"experimentalDecorators": true /* Enable experimental support for TC39 stage 2 draft decorators. */,
|
||||
"emitDecoratorMetadata": true /* Emit design-type metadata for decorated declarations in source files. */,
|
||||
"target": "ES2020", // or higher
|
||||
"outDir": "./dist/",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
|
||||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
|
||||
"sourceMap": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"useUnknownInCatchVariables": false,
|
||||
"declaration": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node16"
|
||||
},
|
||||
"include": ["src", "nodes"]
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<!-- markdownlint-disable MD030 -->
|
||||
|
||||
# Flowise - LangchainJS UI
|
||||
|
||||

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

|
||||
|
||||
Install:
|
||||
|
||||
```bash
|
||||
npm i flowise-ui
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Source code in this repository is made available under the [MIT License](https://github.com/FlowiseAI/Flowise/blob/master/LICENSE.md).
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "esnext",
|
||||
"module": "commonjs",
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"name": "flowise-ui",
|
||||
"version": "1.0.1",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"homepage": "https://flowiseai.com",
|
||||
"author": {
|
||||
"name": "HenryHeng",
|
||||
"email": "henryheng@flowiseai.com"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.0.3",
|
||||
"@mui/material": "^5.11.12",
|
||||
"@tabler/icons": "^1.39.1",
|
||||
"clsx": "^1.1.1",
|
||||
"formik": "^2.2.6",
|
||||
"framer-motion": "^4.1.13",
|
||||
"history": "^5.0.0",
|
||||
"html-react-parser": "^3.0.4",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.3",
|
||||
"notistack": "^2.0.4",
|
||||
"prismjs": "^1.28.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^18.2.0",
|
||||
"react-datepicker": "^4.8.0",
|
||||
"react-device-detect": "^1.17.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-markdown": "^8.0.6",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router": "~6.3.0",
|
||||
"react-router-dom": "~6.3.0",
|
||||
"react-simple-code-editor": "^0.11.2",
|
||||
"reactflow": "^11.5.6",
|
||||
"redux": "^4.0.5",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"dev": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"babel": {
|
||||
"presets": [
|
||||
"@babel/preset-react"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.15.8",
|
||||
"@testing-library/jest-dom": "^5.11.10",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"react-scripts": "^5.0.1",
|
||||
"sass": "^1.42.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 287 B |
|
After Width: | Height: | Size: 589 B |
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,62 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Flowise - LangchainJS UI</title>
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<!-- Meta Tags-->
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#2296f3" />
|
||||
<meta name="title" content="Flowise - LangchainJS UI" />
|
||||
<meta name="description" content="Flowise helps you to better integrate Web3 with existing Web2 applications" />
|
||||
<meta name="keywords" content="react, material-ui, reactjs, reactjs, workflow automation, web3, web2, blockchain" />
|
||||
<meta name="author" content="CodedThemes" />
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://flowiseai.com/" />
|
||||
<meta property="og:site_name" content="flowiseai.com" />
|
||||
<meta property="article:publisher" content="https://www.facebook.com/codedthemes" />
|
||||
<meta property="og:title" content="Flowise - LangchainJS UI" />
|
||||
<meta property="og:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
|
||||
<meta property="og:image" content="https://flowiseai.com/og-image/og-facebook.png" />
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:url" content="https://flowiseai.com" />
|
||||
<meta property="twitter:title" content="Flowise - LangchainJS UI" />
|
||||
<meta property="twitter:description" content="Flowise helps you to better build LLM flows using Langchain in simple GUI" />
|
||||
<meta property="twitter:image" content="https://flowiseai.com/og-image/og-twitter.png" />
|
||||
<meta name="twitter:creator" content="@codedthemes" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="portal"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { ThemeProvider } from '@mui/material/styles'
|
||||
import { CssBaseline, StyledEngineProvider } from '@mui/material'
|
||||
|
||||
// routing
|
||||
import Routes from 'routes'
|
||||
|
||||
// defaultTheme
|
||||
import themes from 'themes'
|
||||
|
||||
// project imports
|
||||
import NavigationScroll from 'layout/NavigationScroll'
|
||||
|
||||
// ==============================|| APP ||============================== //
|
||||
|
||||
const App = () => {
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
return (
|
||||
<StyledEngineProvider injectFirst>
|
||||
<ThemeProvider theme={themes(customization)}>
|
||||
<CssBaseline />
|
||||
<NavigationScroll>
|
||||
<Routes />
|
||||
</NavigationScroll>
|
||||
</ThemeProvider>
|
||||
</StyledEngineProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import client from './client'
|
||||
|
||||
const getAllChatflows = () => client.get('/chatflows')
|
||||
|
||||
const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`)
|
||||
|
||||
const createNewChatflow = (body) => client.post(`/chatflows`, body)
|
||||
|
||||
const updateChatflow = (id, body) => client.put(`/chatflows/${id}`, body)
|
||||
|
||||
const deleteChatflow = (id) => client.delete(`/chatflows/${id}`)
|
||||
|
||||
export default {
|
||||
getAllChatflows,
|
||||
getSpecificChatflow,
|
||||
createNewChatflow,
|
||||
updateChatflow,
|
||||
deleteChatflow
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import client from './client'
|
||||
|
||||
const getChatmessageFromChatflow = (id) => client.get(`/chatmessage/${id}`)
|
||||
|
||||
const createNewChatmessage = (id, body) => client.post(`/chatmessage/${id}`, body)
|
||||
|
||||
const deleteChatmessage = (id) => client.delete(`/chatmessage/${id}`)
|
||||
|
||||
export default {
|
||||
getChatmessageFromChatflow,
|
||||
createNewChatmessage,
|
||||
deleteChatmessage
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import axios from 'axios'
|
||||
import { baseURL } from 'store/constant'
|
||||
|
||||
const apiClient = axios.create({
|
||||
baseURL: `${baseURL}/api/v1`,
|
||||
headers: {
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
export default apiClient
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import client from './client'
|
||||
|
||||
const getAllNodes = () => client.get('/nodes')
|
||||
|
||||
const getSpecificNode = (name) => client.get(`/nodes/${name}`)
|
||||
|
||||
export default {
|
||||
getAllNodes,
|
||||
getSpecificNode
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import client from './client'
|
||||
|
||||
const sendMessageAndGetPrediction = (id, input) => client.post(`/prediction/${id}`, input)
|
||||
|
||||
export default {
|
||||
sendMessageAndGetPrediction
|
||||
}
|
||||
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 8.8 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
|
@ -0,0 +1,157 @@
|
|||
// paper & background
|
||||
$paper: #ffffff;
|
||||
|
||||
// primary
|
||||
$primaryLight: #e3f2fd;
|
||||
$primaryMain: #2196f3;
|
||||
$primaryDark: #1e88e5;
|
||||
$primary200: #90caf9;
|
||||
$primary800: #1565c0;
|
||||
|
||||
// secondary
|
||||
$secondaryLight: #ede7f6;
|
||||
$secondaryMain: #673ab7;
|
||||
$secondaryDark: #5e35b1;
|
||||
$secondary200: #b39ddb;
|
||||
$secondary800: #4527a0;
|
||||
|
||||
// success Colors
|
||||
$successLight: #cdf5d8;
|
||||
$success200: #69f0ae;
|
||||
$successMain: #00e676;
|
||||
$successDark: #00c853;
|
||||
|
||||
// error
|
||||
$errorLight: #f3d2d2;
|
||||
$errorMain: #f44336;
|
||||
$errorDark: #c62828;
|
||||
|
||||
// orange
|
||||
$orangeLight: #fbe9e7;
|
||||
$orangeMain: #ffab91;
|
||||
$orangeDark: #d84315;
|
||||
|
||||
// warning
|
||||
$warningLight: #fff8e1;
|
||||
$warningMain: #ffe57f;
|
||||
$warningDark: #ffc107;
|
||||
|
||||
// grey
|
||||
$grey50: #fafafa;
|
||||
$grey100: #f5f5f5;
|
||||
$grey200: #eeeeee;
|
||||
$grey300: #e0e0e0;
|
||||
$grey500: #9e9e9e;
|
||||
$grey600: #757575;
|
||||
$grey700: #616161;
|
||||
$grey900: #212121;
|
||||
|
||||
// ==============================|| DARK THEME VARIANTS ||============================== //
|
||||
|
||||
// paper & background
|
||||
$darkBackground: #191b1f;
|
||||
$darkPaper: #191b1f;
|
||||
|
||||
// dark 800 & 900
|
||||
$darkLevel1: #252525; // level 1
|
||||
$darkLevel2: #242424; // level 2
|
||||
|
||||
// primary dark
|
||||
$darkPrimaryLight: #23262c;
|
||||
$darkPrimaryMain: #23262c;
|
||||
$darkPrimaryDark: #191b1f;
|
||||
$darkPrimary200: #c9d4e9;
|
||||
$darkPrimary800: #32353b;
|
||||
|
||||
// secondary dark
|
||||
$darkSecondaryLight: #454c59;
|
||||
$darkSecondaryMain: #7c4dff;
|
||||
$darkSecondaryDark: #ffffff;
|
||||
$darkSecondary200: #32353b;
|
||||
$darkSecondary800: #6200ea;
|
||||
|
||||
// text variants
|
||||
$darkTextTitle: #d7dcec;
|
||||
$darkTextPrimary: #bdc8f0;
|
||||
$darkTextSecondary: #8492c4;
|
||||
|
||||
// ==============================|| JAVASCRIPT ||============================== //
|
||||
|
||||
:export {
|
||||
// paper & background
|
||||
paper: $paper;
|
||||
|
||||
// primary
|
||||
primaryLight: $primaryLight;
|
||||
primary200: $primary200;
|
||||
primaryMain: $primaryMain;
|
||||
primaryDark: $primaryDark;
|
||||
primary800: $primary800;
|
||||
|
||||
// secondary
|
||||
secondaryLight: $secondaryLight;
|
||||
secondary200: $secondary200;
|
||||
secondaryMain: $secondaryMain;
|
||||
secondaryDark: $secondaryDark;
|
||||
secondary800: $secondary800;
|
||||
|
||||
// success
|
||||
successLight: $successLight;
|
||||
success200: $success200;
|
||||
successMain: $successMain;
|
||||
successDark: $successDark;
|
||||
|
||||
// error
|
||||
errorLight: $errorLight;
|
||||
errorMain: $errorMain;
|
||||
errorDark: $errorDark;
|
||||
|
||||
// orange
|
||||
orangeLight: $orangeLight;
|
||||
orangeMain: $orangeMain;
|
||||
orangeDark: $orangeDark;
|
||||
|
||||
// warning
|
||||
warningLight: $warningLight;
|
||||
warningMain: $warningMain;
|
||||
warningDark: $warningDark;
|
||||
|
||||
// grey
|
||||
grey50: $grey50;
|
||||
grey100: $grey100;
|
||||
grey200: $grey200;
|
||||
grey300: $grey300;
|
||||
grey500: $grey500;
|
||||
grey600: $grey600;
|
||||
grey700: $grey700;
|
||||
grey900: $grey900;
|
||||
|
||||
// ==============================|| DARK THEME VARIANTS ||============================== //
|
||||
|
||||
// paper & background
|
||||
darkPaper: $darkPaper;
|
||||
darkBackground: $darkBackground;
|
||||
|
||||
// dark 800 & 900
|
||||
darkLevel1: $darkLevel1;
|
||||
darkLevel2: $darkLevel2;
|
||||
|
||||
// text variants
|
||||
darkTextTitle: $darkTextTitle;
|
||||
darkTextPrimary: $darkTextPrimary;
|
||||
darkTextSecondary: $darkTextSecondary;
|
||||
|
||||
// primary dark
|
||||
darkPrimaryLight: $darkPrimaryLight;
|
||||
darkPrimaryMain: $darkPrimaryMain;
|
||||
darkPrimaryDark: $darkPrimaryDark;
|
||||
darkPrimary200: $darkPrimary200;
|
||||
darkPrimary800: $darkPrimary800;
|
||||
|
||||
// secondary dark
|
||||
darkSecondaryLight: $darkSecondaryLight;
|
||||
darkSecondaryMain: $darkSecondaryMain;
|
||||
darkSecondaryDark: $darkSecondaryDark;
|
||||
darkSecondary200: $darkSecondary200;
|
||||
darkSecondary800: $darkSecondary800;
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
// color variants
|
||||
@import 'themes-vars.module.scss';
|
||||
|
||||
// third-party
|
||||
@import '~react-perfect-scrollbar/dist/css/styles.css';
|
||||
|
||||
// ==============================|| LIGHT BOX ||============================== //
|
||||
.fullscreen .react-images__blanket {
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
// ==============================|| PERFECT SCROLLBAR ||============================== //
|
||||
|
||||
.scrollbar-container {
|
||||
.ps__rail-y {
|
||||
&:hover > .ps__thumb-y,
|
||||
&:focus > .ps__thumb-y,
|
||||
&.ps--clicking .ps__thumb-y {
|
||||
background-color: $grey500;
|
||||
width: 5px;
|
||||
}
|
||||
}
|
||||
.ps__thumb-y {
|
||||
background-color: $grey500;
|
||||
border-radius: 6px;
|
||||
width: 5px;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar-container.ps,
|
||||
.scrollbar-container > .ps {
|
||||
&.ps--active-y > .ps__rail-y {
|
||||
width: 5px;
|
||||
background-color: transparent !important;
|
||||
z-index: 999;
|
||||
&:hover,
|
||||
&.ps--clicking {
|
||||
width: 5px;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
&.ps--scrolling-y > .ps__rail-y,
|
||||
&.ps--scrolling-x > .ps__rail-x {
|
||||
opacity: 0.4;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================|| ANIMATION KEYFRAMES ||============================== //
|
||||
|
||||
@keyframes wings {
|
||||
50% {
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%,
|
||||
20%,
|
||||
53%,
|
||||
to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
40%,
|
||||
43% {
|
||||
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
||||
transform: translate3d(0, -5px, 0);
|
||||
}
|
||||
70% {
|
||||
animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
||||
transform: translate3d(0, -7px, 0);
|
||||
}
|
||||
80% {
|
||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(0, -2px, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideY {
|
||||
0%,
|
||||
50%,
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideX {
|
||||
0%,
|
||||
50%,
|
||||
100% {
|
||||
transform: translateX(0px);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
const config = {
|
||||
// basename: only at build time to set, and Don't add '/' at end off BASENAME for breadcrumbs, also Don't put only '/' use blank('') instead,
|
||||
basename: '',
|
||||
defaultPath: '/chatflows',
|
||||
fontFamily: `'Roboto', sans-serif`,
|
||||
borderRadius: 12
|
||||
}
|
||||
|
||||
export default config
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import { useState } from 'react'
|
||||
|
||||
export default (apiFunc) => {
|
||||
const [data, setData] = useState(null)
|
||||
const [error, setError] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const request = async (...args) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const result = await apiFunc(...args)
|
||||
setData(result.data)
|
||||
} catch (err) {
|
||||
setError(err || 'Unexpected Error!')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
loading,
|
||||
request
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { useContext } from 'react'
|
||||
import ConfirmContext from 'store/context/ConfirmContext'
|
||||
import { HIDE_CONFIRM, SHOW_CONFIRM } from 'store/actions'
|
||||
|
||||
let resolveCallback
|
||||
const useConfirm = () => {
|
||||
const [confirmState, dispatch] = useContext(ConfirmContext)
|
||||
|
||||
const closeConfirm = () => {
|
||||
dispatch({
|
||||
type: HIDE_CONFIRM
|
||||
})
|
||||
}
|
||||
|
||||
const onConfirm = () => {
|
||||
closeConfirm()
|
||||
resolveCallback(true)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
closeConfirm()
|
||||
resolveCallback(false)
|
||||
}
|
||||
const confirm = (confirmPayload) => {
|
||||
dispatch({
|
||||
type: SHOW_CONFIRM,
|
||||
payload: confirmPayload
|
||||
})
|
||||
return new Promise((res) => {
|
||||
resolveCallback = res
|
||||
})
|
||||
}
|
||||
|
||||
return { confirm, onConfirm, onCancel, confirmState }
|
||||
}
|
||||
|
||||
export default useConfirm
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { useEffect, useRef } from 'react'
|
||||
|
||||
// ==============================|| ELEMENT REFERENCE HOOKS ||============================== //
|
||||
|
||||
const useScriptRef = () => {
|
||||
const scripted = useRef(true)
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
scripted.current = false
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return scripted
|
||||
}
|
||||
|
||||
export default useScriptRef
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import React from 'react'
|
||||
import App from './App'
|
||||
import { store } from 'store'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
// style + assets
|
||||
import 'assets/scss/style.scss'
|
||||
|
||||
// third party
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Provider } from 'react-redux'
|
||||
import { SnackbarProvider } from 'notistack'
|
||||
import ConfirmContextProvider from 'store/context/ConfirmContextProvider'
|
||||
import { ReactFlowContext } from 'store/context/ReactFlowContext'
|
||||
|
||||
const container = document.getElementById('root')
|
||||
const root = createRoot(container)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<SnackbarProvider>
|
||||
<ConfirmContextProvider>
|
||||
<ReactFlowContext>
|
||||
<App />
|
||||
</ReactFlowContext>
|
||||
</ConfirmContextProvider>
|
||||
</SnackbarProvider>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useState } from 'react'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Avatar, Box, ButtonBase, Switch } from '@mui/material'
|
||||
import { styled } from '@mui/material/styles'
|
||||
|
||||
// project imports
|
||||
import LogoSection from '../LogoSection'
|
||||
|
||||
// assets
|
||||
import { IconMenu2 } from '@tabler/icons'
|
||||
|
||||
// store
|
||||
import { SET_DARKMODE } from 'store/actions'
|
||||
|
||||
// ==============================|| MAIN NAVBAR / HEADER ||============================== //
|
||||
|
||||
const MaterialUISwitch = styled(Switch)(({ theme }) => ({
|
||||
width: 62,
|
||||
height: 34,
|
||||
padding: 7,
|
||||
'& .MuiSwitch-switchBase': {
|
||||
margin: 1,
|
||||
padding: 0,
|
||||
transform: 'translateX(6px)',
|
||||
'&.Mui-checked': {
|
||||
color: '#fff',
|
||||
transform: 'translateX(22px)',
|
||||
'& .MuiSwitch-thumb:before': {
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||
'#fff'
|
||||
)}" d="M4.2 2.5l-.7 1.8-1.8.7 1.8.7.7 1.8.6-1.8L6.7 5l-1.9-.7-.6-1.8zm15 8.3a6.7 6.7 0 11-6.6-6.6 5.8 5.8 0 006.6 6.6z"/></svg>')`
|
||||
},
|
||||
'& + .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be'
|
||||
}
|
||||
}
|
||||
},
|
||||
'& .MuiSwitch-thumb': {
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#003892' : '#001e3c',
|
||||
width: 32,
|
||||
height: 32,
|
||||
'&:before': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
left: 0,
|
||||
top: 0,
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundPosition: 'center',
|
||||
backgroundImage: `url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 20 20"><path fill="${encodeURIComponent(
|
||||
'#fff'
|
||||
)}" d="M9.305 1.667V3.75h1.389V1.667h-1.39zm-4.707 1.95l-.982.982L5.09 6.072l.982-.982-1.473-1.473zm10.802 0L13.927 5.09l.982.982 1.473-1.473-.982-.982zM10 5.139a4.872 4.872 0 00-4.862 4.86A4.872 4.872 0 0010 14.862 4.872 4.872 0 0014.86 10 4.872 4.872 0 0010 5.139zm0 1.389A3.462 3.462 0 0113.471 10a3.462 3.462 0 01-3.473 3.472A3.462 3.462 0 016.527 10 3.462 3.462 0 0110 6.528zM1.665 9.305v1.39h2.083v-1.39H1.666zm14.583 0v1.39h2.084v-1.39h-2.084zM5.09 13.928L3.616 15.4l.982.982 1.473-1.473-.982-.982zm9.82 0l-.982.982 1.473 1.473.982-.982-1.473-1.473zM9.305 16.25v2.083h1.389V16.25h-1.39z"/></svg>')`
|
||||
}
|
||||
},
|
||||
'& .MuiSwitch-track': {
|
||||
opacity: 1,
|
||||
backgroundColor: theme.palette.mode === 'dark' ? '#8796A5' : '#aab4be',
|
||||
borderRadius: 20 / 2
|
||||
}
|
||||
}))
|
||||
|
||||
const Header = ({ handleLeftDrawerToggle }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [isDark, setIsDark] = useState(customization.isDarkMode)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
const changeDarkMode = () => {
|
||||
dispatch({ type: SET_DARKMODE, isDarkMode: !isDark })
|
||||
setIsDark((isDark) => !isDark)
|
||||
localStorage.setItem('isDarkMode', !isDark)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* logo & toggler button */}
|
||||
<Box
|
||||
sx={{
|
||||
width: 228,
|
||||
display: 'flex',
|
||||
[theme.breakpoints.down('md')]: {
|
||||
width: 'auto'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box component='span' sx={{ display: { xs: 'none', md: 'block' }, flexGrow: 1 }}>
|
||||
<LogoSection />
|
||||
</Box>
|
||||
<ButtonBase sx={{ borderRadius: '12px', overflow: 'hidden' }}>
|
||||
<Avatar
|
||||
variant='rounded'
|
||||
sx={{
|
||||
...theme.typography.commonAvatar,
|
||||
...theme.typography.mediumAvatar,
|
||||
transition: 'all .2s ease-in-out',
|
||||
background: theme.palette.secondary.light,
|
||||
color: theme.palette.secondary.dark,
|
||||
'&:hover': {
|
||||
background: theme.palette.secondary.dark,
|
||||
color: theme.palette.secondary.light
|
||||
}
|
||||
}}
|
||||
onClick={handleLeftDrawerToggle}
|
||||
color='inherit'
|
||||
>
|
||||
<IconMenu2 stroke={1.5} size='1.3rem' />
|
||||
</Avatar>
|
||||
</ButtonBase>
|
||||
</Box>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
<MaterialUISwitch checked={isDark} onChange={changeDarkMode} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Header.propTypes = {
|
||||
handleLeftDrawerToggle: PropTypes.func
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { Link } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { ButtonBase } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import config from 'config'
|
||||
import Logo from 'ui-component/extended/Logo'
|
||||
|
||||
// ==============================|| MAIN LOGO ||============================== //
|
||||
|
||||
const LogoSection = () => (
|
||||
<ButtonBase disableRipple component={Link} to={config.defaultPath}>
|
||||
<Logo />
|
||||
</ButtonBase>
|
||||
)
|
||||
|
||||
export default LogoSection
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import { useState } from 'react'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Collapse, List, ListItemButton, ListItemIcon, ListItemText, Typography } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import NavItem from '../NavItem'
|
||||
|
||||
// assets
|
||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
|
||||
import { IconChevronDown, IconChevronUp } from '@tabler/icons'
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST COLLAPSE ITEMS ||============================== //
|
||||
|
||||
const NavCollapse = ({ menu, level }) => {
|
||||
const theme = useTheme()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selected, setSelected] = useState(null)
|
||||
|
||||
const handleClick = () => {
|
||||
setOpen(!open)
|
||||
setSelected(!selected ? menu.id : null)
|
||||
}
|
||||
|
||||
// menu collapse & item
|
||||
const menus = menu.children?.map((item) => {
|
||||
switch (item.type) {
|
||||
case 'collapse':
|
||||
return <NavCollapse key={item.id} menu={item} level={level + 1} />
|
||||
case 'item':
|
||||
return <NavItem key={item.id} item={item} level={level + 1} />
|
||||
default:
|
||||
return (
|
||||
<Typography key={item.id} variant='h6' color='error' align='center'>
|
||||
Menu Items Error
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const Icon = menu.icon
|
||||
const menuIcon = menu.icon ? (
|
||||
<Icon strokeWidth={1.5} size='1.3rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
|
||||
) : (
|
||||
<FiberManualRecordIcon
|
||||
sx={{
|
||||
width: selected === menu.id ? 8 : 6,
|
||||
height: selected === menu.id ? 8 : 6
|
||||
}}
|
||||
fontSize={level > 0 ? 'inherit' : 'medium'}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItemButton
|
||||
sx={{
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
mb: 0.5,
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
|
||||
py: level > 1 ? 1 : 1.25,
|
||||
pl: `${level * 24}px`
|
||||
}}
|
||||
selected={selected === menu.id}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ListItemIcon sx={{ my: 'auto', minWidth: !menu.icon ? 18 : 36 }}>{menuIcon}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant={selected === menu.id ? 'h5' : 'body1'} color='inherit' sx={{ my: 'auto' }}>
|
||||
{menu.title}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
menu.caption && (
|
||||
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
|
||||
{menu.caption}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{open ? (
|
||||
<IconChevronUp stroke={1.5} size='1rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
|
||||
) : (
|
||||
<IconChevronDown stroke={1.5} size='1rem' style={{ marginTop: 'auto', marginBottom: 'auto' }} />
|
||||
)}
|
||||
</ListItemButton>
|
||||
<Collapse in={open} timeout='auto' unmountOnExit>
|
||||
<List
|
||||
component='div'
|
||||
disablePadding
|
||||
sx={{
|
||||
position: 'relative',
|
||||
'&:after': {
|
||||
content: "''",
|
||||
position: 'absolute',
|
||||
left: '32px',
|
||||
top: 0,
|
||||
height: '100%',
|
||||
width: '1px',
|
||||
opacity: 1,
|
||||
background: theme.palette.primary.light
|
||||
}
|
||||
}}
|
||||
>
|
||||
{menus}
|
||||
</List>
|
||||
</Collapse>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
NavCollapse.propTypes = {
|
||||
menu: PropTypes.object,
|
||||
level: PropTypes.number
|
||||
}
|
||||
|
||||
export default NavCollapse
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Divider, List, Typography } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import NavItem from '../NavItem'
|
||||
import NavCollapse from '../NavCollapse'
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST GROUP ||============================== //
|
||||
|
||||
const NavGroup = ({ item }) => {
|
||||
const theme = useTheme()
|
||||
|
||||
// menu list collapse & items
|
||||
const items = item.children?.map((menu) => {
|
||||
switch (menu.type) {
|
||||
case 'collapse':
|
||||
return <NavCollapse key={menu.id} menu={menu} level={1} />
|
||||
case 'item':
|
||||
return <NavItem key={menu.id} item={menu} level={1} navType='MENU' />
|
||||
default:
|
||||
return (
|
||||
<Typography key={menu.id} variant='h6' color='error' align='center'>
|
||||
Menu Items Error
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<>
|
||||
<List
|
||||
subheader={
|
||||
item.title && (
|
||||
<Typography variant='caption' sx={{ ...theme.typography.menuCaption }} display='block' gutterBottom>
|
||||
{item.title}
|
||||
{item.caption && (
|
||||
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
|
||||
{item.caption}
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
>
|
||||
{items}
|
||||
</List>
|
||||
|
||||
{/* group divider */}
|
||||
<Divider sx={{ mt: 0.25, mb: 1.25 }} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
NavGroup.propTypes = {
|
||||
item: PropTypes.object
|
||||
}
|
||||
|
||||
export default NavGroup
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import PropTypes from 'prop-types'
|
||||
import { forwardRef, useEffect } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography, useMediaQuery } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import { MENU_OPEN, SET_MENU } from 'store/actions'
|
||||
import config from 'config'
|
||||
|
||||
// assets
|
||||
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord'
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST ITEMS ||============================== //
|
||||
|
||||
const NavItem = ({ item, level, navType, onClick, onUploadFile }) => {
|
||||
const theme = useTheme()
|
||||
const dispatch = useDispatch()
|
||||
const customization = useSelector((state) => state.customization)
|
||||
const matchesSM = useMediaQuery(theme.breakpoints.down('lg'))
|
||||
|
||||
const Icon = item.icon
|
||||
const itemIcon = item?.icon ? (
|
||||
<Icon stroke={1.5} size='1.3rem' />
|
||||
) : (
|
||||
<FiberManualRecordIcon
|
||||
sx={{
|
||||
width: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6,
|
||||
height: customization.isOpen.findIndex((id) => id === item?.id) > -1 ? 8 : 6
|
||||
}}
|
||||
fontSize={level > 0 ? 'inherit' : 'medium'}
|
||||
/>
|
||||
)
|
||||
|
||||
let itemTarget = '_self'
|
||||
if (item.target) {
|
||||
itemTarget = '_blank'
|
||||
}
|
||||
|
||||
let listItemProps = {
|
||||
component: forwardRef(function ListItemPropsComponent(props, ref) {
|
||||
return <Link ref={ref} {...props} to={`${config.basename}${item.url}`} target={itemTarget} />
|
||||
})
|
||||
}
|
||||
if (item?.external) {
|
||||
listItemProps = { component: 'a', href: item.url, target: itemTarget }
|
||||
}
|
||||
if (item?.id === 'loadChatflow') {
|
||||
listItemProps.component = 'label'
|
||||
}
|
||||
|
||||
const handleFileUpload = (e) => {
|
||||
if (!e.target.files) return
|
||||
|
||||
const file = e.target.files[0]
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = (evt) => {
|
||||
if (!evt?.target?.result) {
|
||||
return
|
||||
}
|
||||
const { result } = evt.target
|
||||
onUploadFile(result)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
const itemHandler = (id) => {
|
||||
if (navType === 'SETTINGS' && id !== 'loadChatflow') {
|
||||
onClick(id)
|
||||
} else {
|
||||
dispatch({ type: MENU_OPEN, id })
|
||||
if (matchesSM) dispatch({ type: SET_MENU, opened: false })
|
||||
}
|
||||
}
|
||||
|
||||
// active menu item on page load
|
||||
useEffect(() => {
|
||||
if (navType === 'MENU') {
|
||||
const currentIndex = document.location.pathname
|
||||
.toString()
|
||||
.split('/')
|
||||
.findIndex((id) => id === item.id)
|
||||
if (currentIndex > -1) {
|
||||
dispatch({ type: MENU_OPEN, id: item.id })
|
||||
}
|
||||
if (!document.location.pathname.toString().split('/')[1]) {
|
||||
itemHandler('chatflows')
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [navType])
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
{...listItemProps}
|
||||
disabled={item.disabled}
|
||||
sx={{
|
||||
borderRadius: `${customization.borderRadius}px`,
|
||||
mb: 0.5,
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: level > 1 ? 'transparent !important' : 'inherit',
|
||||
py: level > 1 ? 1 : 1.25,
|
||||
pl: `${level * 24}px`
|
||||
}}
|
||||
selected={customization.isOpen.findIndex((id) => id === item.id) > -1}
|
||||
onClick={() => itemHandler(item.id)}
|
||||
>
|
||||
{item.id === 'loadChatflow' && <input type='file' hidden accept='.json' onChange={(e) => handleFileUpload(e)} />}
|
||||
<ListItemIcon sx={{ my: 'auto', minWidth: !item?.icon ? 18 : 36 }}>{itemIcon}</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={
|
||||
<Typography variant={customization.isOpen.findIndex((id) => id === item.id) > -1 ? 'h5' : 'body1'} color='inherit'>
|
||||
{item.title}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
item.caption && (
|
||||
<Typography variant='caption' sx={{ ...theme.typography.subMenuCaption }} display='block' gutterBottom>
|
||||
{item.caption}
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
/>
|
||||
{item.chip && (
|
||||
<Chip
|
||||
color={item.chip.color}
|
||||
variant={item.chip.variant}
|
||||
size={item.chip.size}
|
||||
label={item.chip.label}
|
||||
avatar={item.chip.avatar && <Avatar>{item.chip.avatar}</Avatar>}
|
||||
/>
|
||||
)}
|
||||
</ListItemButton>
|
||||
)
|
||||
}
|
||||
|
||||
NavItem.propTypes = {
|
||||
item: PropTypes.object,
|
||||
level: PropTypes.number,
|
||||
navType: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
onUploadFile: PropTypes.func
|
||||
}
|
||||
|
||||
export default NavItem
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
// material-ui
|
||||
import { Typography } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import NavGroup from './NavGroup'
|
||||
import menuItem from 'menu-items'
|
||||
|
||||
// ==============================|| SIDEBAR MENU LIST ||============================== //
|
||||
|
||||
const MenuList = () => {
|
||||
const navItems = menuItem.items.map((item) => {
|
||||
switch (item.type) {
|
||||
case 'group':
|
||||
return <NavGroup key={item.id} item={item} />
|
||||
default:
|
||||
return (
|
||||
<Typography key={item.id} variant='h6' color='error' align='center'>
|
||||
Menu Items Error
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return <>{navItems}</>
|
||||
}
|
||||
|
||||
export default MenuList
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
import PropTypes from 'prop-types'
|
||||
|
||||
// material-ui
|
||||
import { useTheme } from '@mui/material/styles'
|
||||
import { Box, Drawer, useMediaQuery } from '@mui/material'
|
||||
|
||||
// third-party
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar'
|
||||
import { BrowserView, MobileView } from 'react-device-detect'
|
||||
|
||||
// project imports
|
||||
import MenuList from './MenuList'
|
||||
import LogoSection from '../LogoSection'
|
||||
import { drawerWidth } from 'store/constant'
|
||||
|
||||
// ==============================|| SIDEBAR DRAWER ||============================== //
|
||||
|
||||
const Sidebar = ({ drawerOpen, drawerToggle, window }) => {
|
||||
const theme = useTheme()
|
||||
const matchUpMd = useMediaQuery(theme.breakpoints.up('md'))
|
||||
|
||||
const drawer = (
|
||||
<>
|
||||
<Box sx={{ display: { xs: 'block', md: 'none' } }}>
|
||||
<Box sx={{ display: 'flex', p: 2, mx: 'auto' }}>
|
||||
<LogoSection />
|
||||
</Box>
|
||||
</Box>
|
||||
<BrowserView>
|
||||
<PerfectScrollbar
|
||||
component='div'
|
||||
style={{
|
||||
height: !matchUpMd ? 'calc(100vh - 56px)' : 'calc(100vh - 88px)',
|
||||
paddingLeft: '16px',
|
||||
paddingRight: '16px'
|
||||
}}
|
||||
>
|
||||
<MenuList />
|
||||
</PerfectScrollbar>
|
||||
</BrowserView>
|
||||
<MobileView>
|
||||
<Box sx={{ px: 2 }}>
|
||||
<MenuList />
|
||||
</Box>
|
||||
</MobileView>
|
||||
</>
|
||||
)
|
||||
|
||||
const container = window !== undefined ? () => window.document.body : undefined
|
||||
|
||||
return (
|
||||
<Box component='nav' sx={{ flexShrink: { md: 0 }, width: matchUpMd ? drawerWidth : 'auto' }} aria-label='mailbox folders'>
|
||||
<Drawer
|
||||
container={container}
|
||||
variant={matchUpMd ? 'persistent' : 'temporary'}
|
||||
anchor='left'
|
||||
open={drawerOpen}
|
||||
onClose={drawerToggle}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': {
|
||||
width: drawerWidth,
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
borderRight: 'none',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
top: '66px'
|
||||
}
|
||||
}
|
||||
}}
|
||||
ModalProps={{ keepMounted: true }}
|
||||
color='inherit'
|
||||
>
|
||||
{drawer}
|
||||
</Drawer>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Sidebar.propTypes = {
|
||||
drawerOpen: PropTypes.bool,
|
||||
drawerToggle: PropTypes.func,
|
||||
window: PropTypes.object
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
|
||||
// material-ui
|
||||
import { styled, useTheme } from '@mui/material/styles'
|
||||
import { AppBar, Box, CssBaseline, Toolbar, useMediaQuery } from '@mui/material'
|
||||
|
||||
// project imports
|
||||
import Header from './Header'
|
||||
import Sidebar from './Sidebar'
|
||||
import { drawerWidth } from 'store/constant'
|
||||
import { SET_MENU } from 'store/actions'
|
||||
|
||||
// styles
|
||||
const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })(({ theme, open }) => ({
|
||||
...theme.typography.mainContent,
|
||||
...(!open && {
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.sharp,
|
||||
duration: theme.transitions.duration.leavingScreen
|
||||
}),
|
||||
[theme.breakpoints.up('md')]: {
|
||||
marginLeft: -(drawerWidth - 20),
|
||||
width: `calc(100% - ${drawerWidth}px)`
|
||||
},
|
||||
[theme.breakpoints.down('md')]: {
|
||||
marginLeft: '20px',
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
padding: '16px'
|
||||
},
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
marginLeft: '10px',
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
padding: '16px',
|
||||
marginRight: '10px'
|
||||
}
|
||||
}),
|
||||
...(open && {
|
||||
transition: theme.transitions.create('margin', {
|
||||
easing: theme.transitions.easing.easeOut,
|
||||
duration: theme.transitions.duration.enteringScreen
|
||||
}),
|
||||
marginLeft: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
width: `calc(100% - ${drawerWidth}px)`,
|
||||
[theme.breakpoints.down('md')]: {
|
||||
marginLeft: '20px'
|
||||
},
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
marginLeft: '10px'
|
||||
}
|
||||
})
|
||||
}))
|
||||
|
||||
// ==============================|| MAIN LAYOUT ||============================== //
|
||||
|
||||
const MainLayout = () => {
|
||||
const theme = useTheme()
|
||||
const matchDownMd = useMediaQuery(theme.breakpoints.down('lg'))
|
||||
|
||||
// Handle left drawer
|
||||
const leftDrawerOpened = useSelector((state) => state.customization.opened)
|
||||
const dispatch = useDispatch()
|
||||
const handleLeftDrawerToggle = () => {
|
||||
dispatch({ type: SET_MENU, opened: !leftDrawerOpened })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ type: SET_MENU, opened: !matchDownMd })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [matchDownMd])
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<CssBaseline />
|
||||
{/* header */}
|
||||
<AppBar
|
||||
enableColorOnDark
|
||||
position='fixed'
|
||||
color='inherit'
|
||||
elevation={0}
|
||||
sx={{
|
||||
bgcolor: theme.palette.background.default,
|
||||
transition: leftDrawerOpened ? theme.transitions.create('width') : 'none'
|
||||
}}
|
||||
>
|
||||
<Toolbar>
|
||||
<Header handleLeftDrawerToggle={handleLeftDrawerToggle} />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
|
||||
{/* drawer */}
|
||||
<Sidebar drawerOpen={leftDrawerOpened} drawerToggle={handleLeftDrawerToggle} />
|
||||
|
||||
{/* main content */}
|
||||
<Main theme={theme} open={leftDrawerOpened}>
|
||||
<Outlet />
|
||||
</Main>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default MainLayout
|
||||