* Update model kwargs and temperature values

* Update keyboard shortcuts for advanced editing

* make Message field have no handles

* Update OpenAI API Key handling in OpenAIEmbeddingsComponent

* Remove unnecessary field_type key from CustomComponent class

* Update required field behavior in CustomComponent class

* Refactor AzureOpenAIModel.py: Removed unnecessary "required" attribute from input parameters

* Update BaiduQianfanChatModel and OpenAIModel configurations

* Fix range_spec step type validation

* Update RangeSpec step_type default value to "float"

* Fix Save debounce

* Update parameterUtils to use debounce instead of throttle

* Update input type options in schemas and graph base classes

* Refactor run_flow_with_caching endpoint to include simplified and experimental versions

* Add PythonFunctionComponent and test case for it

* Add nest_asyncio to fix event loop issue

* Refactor test_initial_setup.py to use RunOutputs instead of ResultData

* Remove unused code in test_endpoints.py

* Add asyncio loop to uvicorn command

* Refactor load_session method to handle coroutine result

* Fixed saving

* Fixed debouncing

* Add InputType and OutputType literals to schema.py

* Update input type in Graph class

* Add new schema for simplified API request

* Add delete_messages function and update test_successful_run assertions

* Add STREAM_INFO_TEXT constant to model components

* Add session_id to simplified_run_flow_with_caching endpoint

* Add field_typing import to OpenAIModel.py

* update starter projects

* Add constants for Langflow base module

* Update setup.py to include latest component versions

* Update Starter Examples

* sets starter_project fixture to Basic Prompting

* Refactor test_endpoints.py: Update test names and add new tests for different output types

* Update HuggingFace Spaces link and add image for dark mode

* Remove filepath reference

* Update Vertex params in base.py

* Add tests for different input types

* Add type annotations and improve test coverage

* Add duplicate space link to README

* Update HuggingFace Spaces badge in README

* Add Python 3.10 installation requirement to README

* Refactor flow running endpoints

* Refactor SimplifiedAPIRequest and add documentation for Tweaks

* Refactor input_request parameter in simplified_run_flow function

* Add support for retrieving specific component output

* Add custom Uvicorn worker for Langflow application

* Add asyncio loop to LangflowApplication initialization

* Update Makefile with new variables and start command

* Fix indentation in Makefile

* Refactor run_graph function to add support for running a JSON flow

* Refactor getChatInputField function and update API code

* Update HuggingFace Spaces documentation with duplication process

* Add asyncio event loop to uvicorn command

* Add installation of backend in start target

* udpate some starter projects

* Fix formatting in hugging-face-spaces.mdx

* Update installation instructions for Langflow

* set examples order

* Update start command in Makefile

* Add installation and usage instructions for Langflow

* Update Langflow installation and usage instructions

* Fix langflow command in README.md

* Fix broken link to HuggingFace Spaces guide

* Add new SVG assets for blog post, chat bot, and cloud docs

* Refactor example rendering in NewFlowModal

* Add new SVG file for short bio section

* Remove unused import and add new component

* Update title in usage.mdx

* Update HuggingFace Spaces heading in usage.mdx

* Update usage instructions in getting-started/usage.mdx

* Update cache option in usage documentation

* Remove 'advanced' flag from 'n_messages' parameter in MemoryComponent.py

* Refactor code to improve performance and readability

* Update project names and flow examples

* fix document qa example

* Remove commented out code in sidebars.js

* Delete unused documentation files

* Fix bug in login functionality

* Remove global variables from components

* Fix bug in login functionality

* fix modal returning to input

* Update max-width of chat message sender name

* Update styling for chat message component

* Refactor OpenAIEmbeddingsComponent signature

* Update usage.mdx file

* Update path in Makefile

* Add new migration and what's new documentation files

* Add new chapters and migration guides

* Update version to 0.0.13 in pyproject.toml

* new locks

* Update dependencies in pyproject.toml

* general fixes

* Update dependencies in pyproject.toml and poetry.lock files

* add padding to modal

* ✨ (undrawCards/index.tsx): update the SVG used for BasicPrompt component to undraw_short_bio_re_fmx0.svg to match the desired design
♻️ (undrawCards/index.tsx): adjust the width and height of the BasicPrompt SVG to 65% to improve the visual appearance

* Commented out components/data in sidebars.js

* Refactor component names in outputs.mdx

* Update embedded chat script URL

* Add data component and fix formatting in outputs component

* Update dependencies in poetry.lock and pyproject.toml

* Update dependencies in poetry.lock and pyproject.toml

* Refactor code to improve performance and readability

* Update dependencies in poetry.lock and pyproject.toml

* Fixed IO Modal updates

* Remove dead code at API Modal

* Fixed overflow at CodeTabsComponent tweaks page

* ✨ (NewFlowModal/index.tsx): update the name of the example from "Blog Writter" to "Blog Writer" for better consistency and clarity

* Update dependencies versions

* Update langflow-base to version 0.0.15 and fix setup_env script

* Update dependencies in pyproject.toml

* Lock dependencies in parallel

* Add logging statement to setup_app function

* Fix Ace not having type="module" and breaking build

* Update authentication settings for access token cookie

* Update package versions in package-lock.json

* Add scripts directory to Dockerfile

* Add setup_env command to build_and_run target

* Remove unnecessary make command in setup_env

* Remove unnecessary installation step in build_and_run

* Add debug configuration for CLI

* 🔧 chore(Makefile): refactor build_langflow target to use a separate script for updating dependencies and building
✨ feat(update_dependencies.py): add script to update pyproject.toml dependency version based on langflow-base version in src/backend/base/pyproject.toml

* Add number_of_results parameter to AstraDBSearchComponent

* Update HuggingFace Spaces links

* Remove duplicate imports in hugging-face-spaces.mdx

* Add number_of_results parameter to vector search components

* Fixed supabase not commited

* Revert "Fixed supabase not commited"

This reverts commit afb10a6262.

* Update duplicate-space.png image

* Delete unused files and components

* Add/update script to update dependencies

* Add .bak files to .gitignore

* Update version numbers and remove unnecessary dependencies

* Update langflow-base dependency path

* Add Text import to VertexAiModel.py

* Update langflow-base version to 0.0.16 and update dependencies

* Delete start projects and commit session in delete_start_projects function

* Refactor backend startup script to handle autologin option

* Update poetry installation script to include pipx update check

* Update pipx installation script for different operating systems

* Update Makefile to improve setup process

* Add error handling on streaming and fix streaming bug on error

* Added description to Blog Writer

* Sort base classes alphabetically

* Update duplicate-space.png image

* update position on langflow prompt chaining

* Add Langflow CLI and first steps documentation

* Add exception handling for missing 'content' field in search_with_vector_store method

* Remove unused import and update type hinting

* fix bug on egdes after creating group component

* Refactor APIRequest class and update model imports

* Remove unused imports and fix formatting issues

* Refactor reactflowUtils and styleUtils

* Add CLI documentation to getting-started/cli.mdx

* Add CLI usage instructions

* Add ZoomableImage component to first-steps.mdx

* Update CLI and first steps documentation

* Remove duplicate import and add new imports for ThemedImage and useBaseUrl

* Update Langflow CLI documentation link

* Remove first-steps.mdx and update index.mdx and sidebars.js

* Update Docusaurus dependencies

* Add AstraDB RAG Flow guide

* Remove unused imports

* Remove unnecessary import statement

* Refactor guide for better readability

* Add data component documentation

* Update component headings and add prompt template

* Fix logging level and version display

* Add datetime import and buffer for alembic log

* Update flow names in NewFlowModal and documentation

* Add starter projects to sidebars.js

* Fix error handling in DirectoryReader class

* Handle exception when loading components in setup.py

* Update version numbers in pyproject.toml files

* Update build_langflow_base and build_langflow_backup in Makefile

* Added docs

* Update dependencies and build process

* Add Admonition component for API Key documentation

* Update API endpoint in async-api.mdx

* Remove async-api guidelines

* Fix UnicodeDecodeError in DirectoryReader

* Update dependency version and fix encoding issues

* Add conditional build and publish for base and main projects

* Update version to 1.0.0a2 in pyproject.toml

* Remove duplicate imports and unnecessary code in custom-component.mdx

* Fix poetry lock command in Makefile

* Update package versions in pyproject.toml

* Remove unused components and update imports

* 📦 chore(pre-release-base.yml): add pre-release workflow for base project
📦 chore(pre-release-langflow.yml): add pre-release workflow for langflow project

* Add ChatLiteLLMModelComponent to models package

* Add frontend installation and build steps

* Add Dockerfile for building and pushing base image

* Add emoji package and nest-asyncio dependency

* 📝 (components.mdx): update margin style of ZoomableImage to improve spacing
📝 (features.mdx): update margin style of ZoomableImage to improve spacing
📝 (login.mdx): update margin style of ZoomableImage to improve spacing

* Fix module import error in validate.py

* Fix error message in directory_reader.py

* Update version import and handle ImportError

* Add cryptography and langchain-openai dependencies

* Update poetry installation and remove poetry-monorepo-dependency-plugin

* Update workflow and Dockerfile for Langflow base pre-release

* Update display names and descriptions for AstraDB components

* Update installation instructions for Langflow

* Update Astra DB links and remove unnecessary imports

* Rename AstraDB

* Add new components and images

* Update HuggingFace Spaces URLs

* Update Langflow documentation and add new starter projects

* Update flow name to "Basic Prompting (Hello, world!)" in relevant files

* Update Basic Prompting flow name to "Ahoy World!"

* Remove HuggingFace Spaces documentation

* Add new files and update sidebars.js

* Remove async-tasks.mdx and update sidebars.js

* Update starter project URLs

* Enable migration of global variables

* Update OpenAIEmbeddings deployment and model

* 📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment
📝 (inputs.mdx): add margin to image style to improve spacing and center alignment

📝 (rag-with-astradb.mdx): add margin to image styles to improve spacing and readability

* Update welcome message in index.mdx

* Add global variable feature to Langflow documentation

* Reorganized sidebar categories

* Update migration documentation

* Refactor SplitTextComponent class to accept inputs of type Record and Text

* Adjust embeddings docs

* ✨ (cardComponent/index.tsx): add a minimum height to the card component to ensure consistent layout and prevent content from overlapping when the card is empty or has minimal content

* Update flow name from "Ahoy World!" to "Hello, world!"

* Update documentation for embeddings, models, and vector stores

* Update CreateRecordComponent and parameterUtils.ts

* Add documentation for Text and Record types

* Remove commented lines in sidebars.js

* Add run_flow_from_json function to load.py

* Update Langflow package to run flow from JSON file

* Fix type annotations and import errors

* Refactor tests and fix test data

---------

Co-authored-by: Rodrigo Nader <rodrigosilvanader@gmail.com>
Co-authored-by: anovazzi1 <otavio2204@gmail.com>
Co-authored-by: Lucas Oliveira <lucas.edu.oli@hotmail.com>
Co-authored-by: carlosrcoelho <carlosrodrigo.coelho@gmail.com>
Co-authored-by: cristhianzl <cristhian.lousa@gmail.com>
Co-authored-by: Matheus <jacquesmats@gmail.com>
This commit is contained in:
Gabriel Luiz Freitas Almeida 2024-04-04 02:46:44 -03:00 • committed by GitHub
commit 05cd6e4fd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
853 changed files with 59936 additions and 15456 deletions

View file

@ -0,0 +1,29 @@
{
"extends": ["eslint:recommended", "plugin:node/recommended"],
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"no-console": "warn",
"no-self-assign": "warn",
"no-self-compare": "warn",
"complexity": ["error", { "max": 15 }],
"indent": ["error", 2, { "SwitchCase": 1 }],
"no-dupe-keys": "error",
"no-invalid-regexp": "error",
"no-undef": "error",
"no-return-assign": "error",
"no-redeclare": "error",
"no-empty": "error",
"no-await-in-loop": "error",
"node/exports-style": ["error", "module.exports"],
"node/file-extension-in-import": ["error", "always"],
"node/prefer-global/buffer": ["error", "always"],
"node/prefer-global/console": ["error", "always"],
"node/prefer-global/process": ["error", "always"],
"node/prefer-global/url-search-params": ["error", "always"],
"node/prefer-global/url": ["error", "always"],
"node/prefer-promises/dns": "error",
"node/prefer-promises/fs": "error"
}
}

View file

@ -1,27 +0,0 @@
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

View file

@ -24,3 +24,7 @@ yarn-error.log*
/test-results/
/playwright-report/*/
/playwright/.cache/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,16 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.ico" />
<script src="/node_modules/ace-builds/src-min-noconflict/ace.js" type="text/javascript"></script>
<script
src="/node_modules/ace-builds/src-min-noconflict/ace.js"
type="module"
></script>
<title>Langflow</title>
</head>
<body id='body' style="width: 100%; height:100%">
</head>
<body id="body" style="width: 100%; height: 100%">
<noscript>You need to enable JavaScript to run this app.</noscript>
<div style="width: 100vw; height:100vh" id='root'></div>
<div style="width: 100vw; height: 100vh" id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -3,12 +3,8 @@
"version": "0.1.2",
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"@mui/material": "^5.14.7",
"@preact/signals-react": "^2.0.0",
"@million/lint": "^0.0.73",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.4",
@ -19,7 +15,7 @@
"@radix-ui/react-menubar": "^1.0.3",
"@radix-ui/react-popover": "^1.0.6",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
@ -29,31 +25,31 @@
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"accordion": "^3.0.2",
"ace-builds": "^1.24.1",
"add": "^2.0.6",
"ansi-to-html": "^0.7.2",
"axios": "^1.5.0",
"base64-js": "^1.5.1",
"class-variance-authority": "^0.6.1",
"clsx": "^1.2.1",
"cmdk": "^1.0.0",
"dompurify": "^3.0.5",
"esbuild": "^0.17.19",
"framer-motion": "^11.0.6",
"lodash": "^4.17.21",
"lucide-react": "^0.233.0",
"lucide-react": "^0.331.0",
"million": "^3.0.6",
"moment": "^2.29.4",
"playwright": "^1.42.0",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.11",
"react-icons": "^4.10.1",
"react-icons": "^5.0.1",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
"react-router-dom": "^6.15.0",
"react-syntax-highlighter": "^15.5.0",
"react-tabs": "^6.0.2",
"react-tooltip": "^5.21.1",
"react18-json-view": "^0.2.3",
"reactflow": "^11.9.2",
"rehype-mathjax": "^4.0.3",
@ -61,8 +57,6 @@
"remark-math": "^5.1.1",
"shadcn-ui": "^0.2.3",
"short-unique-id": "^4.4.4",
"switch": "^0.0.0",
"table": "^6.8.1",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
"uuid": "^9.0.0",
@ -98,7 +92,7 @@
},
"proxy": "http://127.0.0.1:7860",
"devDependencies": {
"@playwright/test": "^1.38.0",
"@playwright/test": "^1.42.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9",
@ -114,13 +108,17 @@
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"daisyui": "^4.0.4",
"eslint": "^8.57.0",
"eslint-plugin-node": "^11.1.0",
"postcss": "^8.4.29",
"prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.3.0",
"pretty-quick": "^3.1.3",
"tailwindcss": "^3.3.3",
"tailwindcss-dotted-background": "^1.1.0",
"typescript": "^5.2.2",
"ua-parser-js": "^1.0.37",
"vite": "^4.5.2"
}
}

View file

@ -1,18 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<ul>
<li>
<a href="./e2e/index.html">e2e report</a>
</li>
<li>
<a href="./onlyFront/index.html">frontEnd Only report</a>
</li>
</ul>
</body>
</html>

View file

@ -20,18 +20,22 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 2 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
],
timeout: 120 * 1000,
// reporter: [
// ["html", { open: "never", outputFolder: "playwright-report/test-results" }],
// ],
reporter: process.env.CI ? "blob" : "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: "http://127.0.0.1:3000",
baseURL: "http://localhost:3000/",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
globalTeardown: require.resolve("./tests/globalTeardown.ts"),
/* Configure projects for major browsers */
projects: [
{
@ -48,39 +52,28 @@ export default defineConfig({
// name: "webkit",
// use: { ...devices["Desktop Safari"] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: [
// {
// command: "npm run backend",
// reuseExistingServer: !process.env.CI,
// timeout: 120 * 1000,
// },
// {
// command: "npm run start",
// url: "http://127.0.0.1:3000",
// reuseExistingServer: !process.env.CI,
// },
// ],
webServer: [
{
command:
"poetry run uvicorn --factory langflow.main:create_app --host 127.0.0.1 --port 7860",
port: 7860,
env: {
LANGFLOW_DATABASE_URL: "sqlite:///./temp",
LANGFLOW_AUTO_LOGIN: "true",
},
stdout: "ignore",
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
{
command: "npm start",
port: 3000,
env: {
VITE_PROXY_TARGET: "http://127.0.0.1:7860",
},
},
],
});

View file

@ -3,6 +3,17 @@
# Default value for the --ui flag
ui=false
# Absolute path to the project root directory
PROJECT_ROOT="../../"
# Check if necessary commands are available
for cmd in npx poetry fuser; do
if ! command -v $cmd &> /dev/null; then
echo "Error: Required command '$cmd' is not installed. Aborting."
exit 1
fi
done
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
key="$1"
@ -23,56 +34,85 @@ done
terminate_process_by_port() {
port="$1"
echo "Terminating process on port: $port"
fuser -k -n tcp "$port" # Forcefully terminate processes using the specified port
echo "Process terminated."
if ! fuser -k -n tcp "$port"; then
echo "Failed to terminate process on port $port. Please check manually."
else
echo "Process terminated."
fi
}
delete_temp() {
if cd "$PROJECT_ROOT"; then
echo "Deleting temp database"
rm -f temp && echo "Temp database deleted." || echo "Failed to delete temp database."
else
echo "Failed to navigate to project root for cleanup."
fi
}
# Trap signals to ensure cleanup on script termination
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000; delete_temp' EXIT
# install playwright if there is not installed yet
npx playwright install
# Ensure the script is executed from the project root directory
if ! cd "$PROJECT_ROOT"; then
echo "Error: Failed to navigate to project root directory. Aborting."
exit 1
fi
# Navigate to the project root directory (where the Makefile is located)
cd ../../
# Install playwright if not installed yet
if ! npx playwright install; then
echo "Error: Failed to install Playwright. Aborting."
exit 1
fi
# Start the frontend using 'make frontend' in the background
make frontend &
# Start the frontend
make frontend > /dev/null 2>&1 &
# Give some time for the frontend to start (adjust sleep duration as needed)
# Adjust sleep duration as needed
sleep 10
# Navigate to the test directory
cd src/frontend
# Run frontend only Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
# Install backend dependencies
if ! poetry install; then
echo "Error: Failed to install backend dependencies. Aborting."
exit 1
fi
# Navigate back to the project root directory
cd ../../
# Start the backend using 'make backend' in the background
make backend &
# Give some time for the backend to start (adjust sleep duration as needed)
# Start the backend
LANGFLOW_DATABASE_URL=sqlite:///./temp LANGFLOW_AUTO_LOGIN=True poetry run langflow run --backend-only --port 7860 --host 0.0.0.0 --no-open-browser > /dev/null 2>&1 &
backend_pid=$! # Capture PID of the backend process
# Adjust sleep duration as needed
sleep 25
# Navigate back to the test directory
cd src/frontend
# Run Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --project=chromium
# Navigate to the test directory
if ! cd src/frontend; then
echo "Error: Failed to navigate to test directory. Aborting."
kill $backend_pid # Terminate the backend process if navigation fails
echo "Backend process terminated."
exit 1
fi
npx playwright show-report
# Check if backend is running
if ! lsof -i :7860; then
echo "Error: Backend is not running. Aborting."
exit 1
fi
# After the tests are finished, you can add cleanup or teardown logic here if needed
# Run Playwright tests
if [ "$ui" = true ]; then
TEST_COMMAND="npx playwright test tests/end-to-end --ui --project=chromium"
else
TEST_COMMAND="npx playwright test tests/end-to-end --project=chromium"
fi
# The trap will automatically terminate processes by port on script exit
if ! PLAYWRIGHT_HTML_REPORT=playwright-report/e2e $TEST_COMMAND; then
echo "Error: Playwright tests failed. Aborting."
exit 1
fi
if [ "$ui" = true ]; then
echo "Opening Playwright report..."
npx playwright show-report
fi
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000; delete_temp; kill $backend_pid 2>/dev/null' EXIT

View file

@ -90,3 +90,12 @@ body {
.jv-indent::-webkit-scrollbar-thumb:hover {
background-color: #bbb !important;
}
.custom-hover {
transition: background-color 0.5s ease;
}
.custom-hover:hover {
background-color: rgba(99, 102, 241, 0.1); /* Medium indigo color with 20% opacity */
}

View file

@ -1,9 +1,9 @@
import _ from "lodash";
import { useContext, useEffect, useState } from "react";
import "reactflow/dist/style.css";
import "./App.css";
import { ErrorBoundary } from "react-error-boundary";
import { useNavigate } from "react-router-dom";
import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success";
@ -15,109 +15,27 @@ import {
FETCH_ERROR_MESSAGE,
} from "./constants/constants";
import { AuthContext } from "./contexts/authContext";
import { getHealth } from "./controllers/API";
import { getGlobalVariables, getHealth } from "./controllers/API";
import Router from "./routes";
import useAlertStore from "./stores/alertStore";
import { useDarkStore } from "./stores/darkStore";
import useFlowsManagerStore from "./stores/flowsManagerStore";
import { useGlobalVariablesStore } from "./stores/globalVariables";
import { useStoreStore } from "./stores/storeStore";
import { useTypesStore } from "./stores/typesStore";
export default function App() {
const errorData = useAlertStore((state) => state.errorData);
const errorOpen = useAlertStore((state) => state.errorOpen);
const setErrorOpen = useAlertStore((state) => state.setErrorOpen);
const noticeData = useAlertStore((state) => state.noticeData);
const noticeOpen = useAlertStore((state) => state.noticeOpen);
const setNoticeOpen = useAlertStore((state) => state.setNoticeOpen);
const successData = useAlertStore((state) => state.successData);
const successOpen = useAlertStore((state) => state.successOpen);
const setSuccessOpen = useAlertStore((state) => state.setSuccessOpen);
const removeFromTempNotificationList = useAlertStore(
(state) => state.removeFromTempNotificationList
);
const tempNotificationList = useAlertStore(
(state) => state.tempNotificationList
);
const [fetchError, setFetchError] = useState(false);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
// Initialize state variable for the list of alerts
const [alertsList, setAlertsList] = useState<
Array<{
type: string;
data: { title: string; list?: Array<string>; link?: string };
id: string;
}>
>([]);
// Use effect hook to update alertsList when a new alert is added
useEffect(() => {
// If there is an error alert open with data, add it to the alertsList
if (errorOpen && errorData) {
if (
alertsList.length > 0 &&
JSON.stringify(alertsList[alertsList.length - 1].data) ===
JSON.stringify(errorData)
) {
return;
}
setErrorOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "error", data: _.cloneDeep(errorData), id: _.uniqueId() },
];
return newAlertsList;
});
}
// If there is a notice alert open with data, add it to the alertsList
else if (noticeOpen && noticeData) {
if (
alertsList.length > 0 &&
JSON.stringify(alertsList[alertsList.length - 1].data) ===
JSON.stringify(noticeData)
) {
return;
}
setNoticeOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "notice", data: _.cloneDeep(noticeData), id: _.uniqueId() },
];
return newAlertsList;
});
}
// If there is a success alert open with data, add it to the alertsList
else if (successOpen && successData) {
if (
alertsList.length > 0 &&
JSON.stringify(alertsList[alertsList.length - 1].data) ===
JSON.stringify(successData)
) {
return;
}
setSuccessOpen(false);
setAlertsList((old) => {
let newAlertsList = [
...old,
{ type: "success", data: _.cloneDeep(successData), id: _.uniqueId() },
];
return newAlertsList;
});
}
}, [
_,
errorData,
errorOpen,
noticeData,
noticeOpen,
setErrorOpen,
setNoticeOpen,
setSuccessOpen,
successData,
successOpen,
]);
const removeAlert = (id: string) => {
setAlertsList((prevAlertsList) =>
prevAlertsList.filter((alert) => alert.id !== id)
);
removeFromTempNotificationList(id);
};
const { isAuthenticated } = useContext(AuthContext);
@ -126,7 +44,13 @@ export default function App() {
const getTypes = useTypesStore((state) => state.getTypes);
const refreshVersion = useDarkStore((state) => state.refreshVersion);
const refreshStars = useDarkStore((state) => state.refreshStars);
const setGlobalVariables = useGlobalVariablesStore(
(state) => state.setGlobalVariables
);
const checkHasStore = useStoreStore((state) => state.checkHasStore);
const navigate = useNavigate();
const [isLoadingHealth, setIsLoadingHealth] = useState(false);
useEffect(() => {
refreshStars();
@ -138,22 +62,26 @@ export default function App() {
getTypes().then(() => {
refreshFlows();
});
getGlobalVariables().then((res) => {
setGlobalVariables(res);
});
checkHasStore();
fetchApiData();
}
}, [isAuthenticated]);
useEffect(() => {
checkApplicationHealth();
// Timer to call getHealth every 5 seconds
const timer = setInterval(() => {
getHealth()
.then(() => {
if (fetchError) setFetchError(false);
onHealthCheck();
})
.catch(() => {
setFetchError(true);
});
}, 20000);
}, 20000); // 20 seconds
// Clean up the timer on component unmount
return () => {
@ -161,6 +89,30 @@ export default function App() {
};
}, []);
const checkApplicationHealth = () => {
setIsLoadingHealth(true);
getHealth()
.then(() => {
onHealthCheck();
})
.catch(() => {
setFetchError(true);
});
setTimeout(() => {
setIsLoadingHealth(false);
}, 2000);
};
const onHealthCheck = () => {
setFetchError(false);
//This condition is necessary to avoid infinite loop on starter page when the application is not healthy
if (isLoading === true && window.location.pathname === "/") {
navigate("/flows");
window.location.reload();
}
};
return (
//need parent component with width and height
<div className="flex h-full flex-col">
@ -170,51 +122,71 @@ export default function App() {
}}
FallbackComponent={CrashErrorComponent}
>
{fetchError ? (
<FetchErrorComponent
description={FETCH_ERROR_DESCRIPION}
message={FETCH_ERROR_MESSAGE}
></FetchErrorComponent>
) : isLoading ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
<>
{
<FetchErrorComponent
description={FETCH_ERROR_DESCRIPION}
message={FETCH_ERROR_MESSAGE}
openModal={fetchError}
setRetry={() => {
checkApplicationHealth();
}}
isLoadingHealth={isLoadingHealth}
></FetchErrorComponent>
}
{isLoading ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
</>
</ErrorBoundary>
<div></div>
<div className="app-div" style={{ zIndex: 999 }}>
{alertsList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" ? (
<ErrorAlert
key={alert.id}
title={alert.data.title}
list={alert.data.list}
id={alert.id}
removeAlert={removeAlert}
/>
) : alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.data.title}
link={alert.data.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
<SuccessAlert
key={alert.id}
title={alert.data.title}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
<div className="app-div">
<div className="flex flex-col-reverse" style={{ zIndex: 999 }}>
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" && (
<ErrorAlert
key={alert.id}
title={alert.title}
list={alert.list}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
<div className="z-40 flex flex-col-reverse">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
alert.type === "success" && (
<SuccessAlert
key={alert.id}
title={alert.title}
id={alert.id}
removeAlert={removeAlert}
/>
)
)}
</div>
))}
</div>
</div>
</div>
);

View file

@ -6,9 +6,9 @@ import CodeAreaComponent from "../../../../components/codeAreaComponent";
import DictComponent from "../../../../components/dictComponent";
import Dropdown from "../../../../components/dropdownComponent";
import FloatComponent from "../../../../components/floatComponent";
import IconComponent from "../../../../components/genericIconComponent";
import InputComponent from "../../../../components/inputComponent";
import { default as IconComponent } from "../../../../components/genericIconComponent";
import InputFileComponent from "../../../../components/inputFileComponent";
import InputGlobalComponent from "../../../../components/inputGlobalComponent";
import InputListComponent from "../../../../components/inputListComponent";
import IntComponent from "../../../../components/intComponent";
import KeypairListComponent from "../../../../components/keypairListComponent";
@ -16,18 +16,27 @@ import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { RefreshButton } from "../../../../components/ui/refreshButton";
import {
INPUT_HANDLER_HOVER,
LANGFLOW_SUPPORTED_TYPES,
OUTPUT_HANDLER_HOVER,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { postCustomComponentUpdate } from "../../../../controllers/API";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useTypesStore } from "../../../../stores/typesStore";
import { APIClassType } from "../../../../types/api";
import {
APIClassType,
ResponseErrorDetailAPI,
ResponseErrorTypeAPI,
} from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components";
import { NodeDataType } from "../../../../types/flow";
import {
debouncedHandleUpdateValues,
handleUpdateValues,
} from "../../../../utils/parameterUtils";
import {
convertObjToArray,
convertValuesToNumbers,
@ -67,6 +76,7 @@ export default function ParameterComponent({
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const [isLoading, setIsLoading] = useState(false);
const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null);
@ -83,32 +93,94 @@ export default function ParameterComponent({
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const handleUpdateValues = async (name: string, data: NodeDataType) => {
const code = data.node?.template["code"]?.value;
if (!code) {
console.error("Code not found in the template");
return;
}
const handleRefreshButtonPress = async (name, data) => {
setIsLoading(true);
try {
const res = await postCustomComponentUpdate(code, name);
if (res.status === 200 && data.node?.template) {
data.node!.template[name] = res.data.template[name];
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
};
const handleOnNewValue = (
useEffect(() => {
async function fetchData() {
if (
(data.node?.template[name]?.real_time_refresh ||
data.node?.template[name]?.refresh_button) &&
// options can be undefined but not an empty array
(data.node?.template[name]?.options?.length ?? 0) === 0
) {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
}
}
fetchData();
}, []);
const handleOnNewValue = async (
newValue: string | string[] | boolean | Object[]
): void => {
): Promise<void> => {
if (data.node!.template[name].value !== newValue) {
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
data.node!.template[name].value !== newValue;
data.node!.template[name].value = newValue; // necessary to enable ctrl+z inside the input
let newTemplate;
if (shouldUpdate) {
setIsLoading(true);
try {
newTemplate = await debouncedHandleUpdateValues(name, data);
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
});
}
setIsLoading(false);
// this de
}
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
@ -116,7 +188,9 @@ export default function ParameterComponent({
...newNode.data,
};
newNode.data.node.template[name].value = newValue;
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else newNode.data.node.template[name].value = newValue;
return newNode;
});
@ -180,11 +254,7 @@ export default function ParameterComponent({
return (
<div key={index}>
{index === 0 && (
<span>
{left
? "Avaliable input components:"
: "Avaliable output components:"}
</span>
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</span>
)}
<span
key={index}
@ -214,7 +284,7 @@ export default function ParameterComponent({
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<React.Fragment key={el + name}>
<span>
{index ===
item.display_name.split(", ").length - 1
@ -231,7 +301,7 @@ export default function ParameterComponent({
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
@ -252,11 +322,14 @@ export default function ParameterComponent({
refHtml.current = <span>{TOOLTIP_EMPTY}</span>;
}
}
// If optionalHandle is an empty list, then it is not an optional handle
if (optionalHandle && optionalHandle.length === 0) {
optionalHandle = null;
}
useEffect(() => {
renderTooltips();
}, [tooltipTitle, flow]);
return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
@ -265,7 +338,7 @@ export default function ParameterComponent({
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
delayDuration={1000}
content={refHtml.current}
side={left ? "left" : "right"}
>
@ -291,7 +364,7 @@ export default function ParameterComponent({
!showNode ? "mt-0" : ""
)}
style={{
borderColor: color,
borderColor: color ?? nodeColors.unknown,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
@ -304,24 +377,40 @@ export default function ParameterComponent({
) : (
<div
ref={ref}
className="relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2"
className={
"relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" +
((name === "code" && type === "code") ||
(name.includes("code") && proxy)
? " hidden "
: "")
}
>
<>
<div
className={
"w-full truncate text-sm" +
(left ? "" : " text-end") +
(info !== "" ? " flex items-center" : "")
"flex w-full items-center truncate text-sm" +
(left ? "" : " justify-end")
}
>
{!left && data.node?.frozen && (
<div className="pr-1">
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
</div>
)}
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
{title}
</span>
</ShadTooltip>
) : (
title
<span className={!left && data.node?.frozen ? " text-ice" : ""}>
{title}
</span>
)}
<span className="text-status-red">{required ? " *" : ""}</span>
<span className={(required ? "ml-2 " : "") + "text-status-red"}>
{required ? "*" : ""}
</span>
<div className="">
{info !== "" && (
<ShadTooltip content={infoHtml.current}>
@ -329,7 +418,7 @@ export default function ParameterComponent({
<div>
<IconComponent
name="Info"
className="relative bottom-0.5 ml-2 h-3 w-4"
className="relative bottom-px ml-1.5 h-3 w-4"
/>
</div>
</ShadTooltip>
@ -343,7 +432,7 @@ export default function ParameterComponent({
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
delayDuration={1000}
content={refHtml.current}
side={left ? "left" : "right"}
>
@ -368,7 +457,7 @@ export default function ParameterComponent({
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
borderColor: color ?? nodeColors.unknown,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
@ -382,46 +471,110 @@ export default function ParameterComponent({
{left === true &&
type === "str" &&
!data.node?.template[name].options ? (
<div className="mt-2 w-full">
<div className="w-full">
{data.node?.template[name].list ? (
<InputListComponent
disabled={disabled}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
? [""]
: data.node.template[name].value
<div
className={
// Commenting this out until we have a better
// way to display
// (data.node?.template[name].refresh ? "w-5/6 " : "") +
"flex-grow"
}
onChange={handleOnNewValue}
/>
>
<InputListComponent
disabled={disabled}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
? [""]
: data.node.template[name].value
}
onChange={handleOnNewValue}
/>
{/* {data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)} */}
</div>
) : data.node?.template[name].multiline ? (
<TextAreaComponent
disabled={disabled}
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + data.node.template[name].name}
data-testid={"textarea-" + data.node.template[name].name}
/>
) : (
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<InputComponent
id={"input-" + index}
<div className="mt-2 flex w-full flex-col ">
<div className="flex-grow">
<TextAreaComponent
disabled={disabled}
password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""}
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + data.node.template[name].name}
data-testid={"textarea-" + data.node.template[name].name}
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
{data.node?.template[name].refresh_button && (
<div className="flex-grow">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
) : (
<div className="mt-2 flex w-full items-center">
<div
className={
"flex-grow " +
(data.node?.template[name].refresh_button ? "w-5/6" : "")
}
>
<InputGlobalComponent
disabled={disabled}
onChange={handleOnNewValue}
setDb={(value) => {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[name].load_from_db = value;
return newNode;
});
}}
>
<IconComponent name="RefreshCcw" />
</button>
name={name}
data={data}
/>
</div>
{data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
)}
@ -429,11 +582,12 @@ export default function ParameterComponent({
) : left === true && type === "bool" ? (
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + index}
id={"toggle-" + name}
disabled={disabled}
enabled={data.node?.template[name].value ?? false}
setEnabled={handleOnNewValue}
size="large"
editNode={false}
/>
</div>
) : left === true && type === "float" ? (
@ -447,26 +601,33 @@ export default function ParameterComponent({
</div>
) : left === true &&
type === "str" &&
data.node?.template[name].options ? (
(data.node?.template[name].options ||
data.node?.template[name]?.real_time_refresh) ? (
// TODO: Improve CSS
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
disabled={disabled}
isLoading={isLoading}
options={data.node.template[name].options}
onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"}
id={"dropdown-" + index}
value={data.node.template[name].value}
id={"dropdown-" + name}
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
{data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={data.node?.template[name].refresh_button_text}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)}
</div>
) : left === true && type === "code" ? (
@ -483,7 +644,7 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + index}
id={"code-input-" + name}
/>
</div>
) : left === true && type === "file" ? (
@ -501,10 +662,11 @@ export default function ParameterComponent({
) : left === true && type === "int" ? (
<div className="mt-2 w-full">
<IntComponent
rangeSpec={data.node?.template[name].rangeSpec}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + index}
id={"int-input-" + name}
/>
</div>
) : left === true && type === "prompt" ? (
@ -517,8 +679,8 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"prompt-input-" + index}
data-testid={"prompt-input-" + index}
id={"prompt-input-" + name}
data-testid={"prompt-input-" + name}
/>
</div>
) : left === true && type === "NestedDict" ? (
@ -553,8 +715,14 @@ export default function ParameterComponent({
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
handleOnNewValue(valueToNumbers);
// if data.node?.template[name].list is true, then the value is an array of objects
// else we need to get the first object of the array
if (data.node?.template[name].list) {
handleOnNewValue(valueToNumbers);
} else handleOnNewValue(valueToNumbers[0]);
}}
isList={data.node?.template[name].list ?? false}
/>
</div>
) : (

View file

@ -1,20 +1,33 @@
import { useCallback, useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import { cloneDeep } from "lodash";
import { useCallback, useEffect, useMemo, useState } from "react";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import Tooltip from "../../components/TooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Button } from "../../components/ui/button";
import Checkmark from "../../components/ui/checkmark";
import Loading from "../../components/ui/loading";
import { Textarea } from "../../components/ui/textarea";
import { priorityFields } from "../../constants/constants";
import Xmark from "../../components/ui/xmark";
import {
NATIVE_CATEGORIES,
RUN_TIMESTAMP_PREFIX,
STATUS_BUILD,
STATUS_BUILDING,
} from "../../constants/constants";
import { BuildStatus } from "../../constants/enums";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useTypesStore } from "../../stores/typesStore";
import { APIClassType } from "../../types/api";
import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, cn, getFieldTitle } from "../../utils/utils";
import { classNames, cn, getFieldTitle, sortFields } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({
@ -29,8 +42,13 @@ export default function GenericNode({
yPos: number;
}): JSX.Element {
const types = useTypesStore((state) => state.types);
const templates = useTypesStore((state) => state.templates);
const deleteNode = useFlowStore((state) => state.deleteNode);
const flowPool = useFlowStore((state) => state.flowPool);
const buildFlow = useFlowStore((state) => state.buildFlow);
const setNode = useFlowStore((state) => state.setNode);
const updateNodeInternals = useUpdateNodeInternals();
const setErrorData = useAlertStore((state) => state.setErrorData);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(false);
const [nodeName, setNodeName] = useState(data.node!.display_name);
@ -38,12 +56,83 @@ export default function GenericNode({
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const [isOutdated, setIsOutdated] = useState(false);
const buildStatus = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.status
);
const lastRunTime = useFlowStore(
(state) => state.flowBuildStatus[data.id]?.timestamp
);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
const [handles, setHandles] = useState<number>(0);
const [validationString, setValidationString] = useState<string>("");
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
useEffect(() => {
// This one should run only once
// first check if data.type in NATIVE_CATEGORIES
// if not return
if (
!NATIVE_CATEGORIES.includes(types[data.type]) ||
!data.node?.template?.code?.value
)
return;
const thisNodeTemplate = templates[data.type].template;
// if the template does not have a code key
// return
if (!thisNodeTemplate.code) return;
const currentCode = thisNodeTemplate.code?.value;
const thisNodesCode = data.node!.template?.code?.value;
const componentsToIgnore = ["Custom Component", "Prompt"];
if (
currentCode !== thisNodesCode &&
!componentsToIgnore.includes(data.node!.display_name)
) {
setIsOutdated(true);
} else {
setIsOutdated(false);
}
// template.code can be undefined
}, [data.node?.template?.code?.value]);
const updateNodeCode = useCallback(
(newNodeClass: APIClassType, code: string, name: string) => {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
node: newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
newNode.data.node.template[name].value = code;
setIsOutdated(false);
return newNode;
});
updateNodeInternals(data.id);
},
[data.id, data.node, setNode, setIsOutdated]
);
if (!data.node!.template) {
setErrorData({
title: `Error in component ${data.node!.display_name}`,
list: [
`The component ${data.node!.display_name} has no template.`,
`Please contact the developer of the component to fix this issue.`,
],
});
takeSnapshot();
deleteNode(data.id);
}
function countHandles(): void {
let count = Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
@ -68,7 +157,6 @@ export default function GenericNode({
setHandles(count);
}
useEffect(() => {
countHandles();
}, [data, data.node]);
@ -81,9 +169,19 @@ export default function GenericNode({
}, [selected]);
// State for outline color
const sseData = useFlowStore((state) => state.sseData);
const isBuilding = useFlowStore((state) => state.isBuilding);
// should be empty string if no duration
// else should be `Duration: ${duration}`
const getDurationString = (duration: number | undefined): string => {
if (duration === undefined) {
return "";
} else {
return `${duration}`;
}
};
const durationString = getDurationString(validationStatus?.data.duration);
useEffect(() => {
setNodeDescription(data.node!.description);
}, [data.node!.description]);
@ -92,20 +190,38 @@ export default function GenericNode({
setNodeName(data.node!.display_name);
}, [data.node!.display_name]);
// New useEffect to watch for changes in sseData and update validation status
useEffect(() => {
const relevantData = sseData[data.id];
const relevantData =
flowPool[data.id] && flowPool[data.id]?.length > 0
? flowPool[data.id][flowPool[data.id].length - 1]
: null;
if (relevantData) {
// Extract validation information from relevantData and update the validationStatus state
setValidationStatus(relevantData);
} else {
setValidationStatus(null);
}
}, [sseData, data.id]);
}, [flowPool[data.id], data.id]);
const showNode = data.showNode ?? true;
useEffect(() => {
if (validationStatus?.params) {
// if it is not a string turn it into a string
let newValidationString = validationStatus.params;
if (typeof newValidationString !== "string") {
newValidationString = JSON.stringify(validationStatus.params);
}
const nameEditable = data.node?.flow || data.type === "CustomComponent";
setValidationString(newValidationString);
}
}, [validationStatus, validationStatus?.params]);
const [showNode, setShowNode] = useState(data.showNode ?? true);
useEffect(() => {
setShowNode(data.showNode ?? true);
}, [data.showNode]);
const nameEditable = true;
const emojiRegex = /\p{Emoji}/u;
const isEmoji = emojiRegex.test(data?.node?.icon!);
@ -116,9 +232,8 @@ export default function GenericNode({
const iconName =
iconElement || (data.node?.flow ? "group_components" : name);
const iconClassName = `generic-node-icon ${
!showNode ? "absolute inset-x-6 h-12 w-12" : ""
!showNode ? " absolute inset-x-6 h-12 w-12 " : ""
}`;
if (iconElement && isEmoji) {
return nodeIconFragment(iconElement);
} else {
@ -140,32 +255,144 @@ export default function GenericNode({
);
};
return (
<>
const isDark = useDarkStore((state) => state.dark);
const renderIconStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
) => {
if (buildStatus === BuildStatus.BUILDING) {
return <Loading className="text-medium-indigo" />;
} else {
return (
<>
<IconComponent
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-medium-indigo opacity-0 transition-all group-hover:opacity-100"
/>
{validationStatus && validationStatus.valid ? (
<Checkmark
className="absolute ml-0.5 h-5 stroke-2 text-status-green opacity-100 transition-all group-hover:opacity-0"
isVisible={true}
/>
) : validationStatus &&
!validationStatus.valid &&
buildStatus === BuildStatus.INACTIVE ? (
<IconComponent
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-green opacity-30 transition-all group-hover:opacity-0"
/>
) : buildStatus === BuildStatus.ERROR ||
(validationStatus && !validationStatus.valid) ? (
<Xmark
isVisible={true}
className="absolute ml-0.5 h-5 fill-current stroke-2 text-status-red opacity-100 transition-all group-hover:opacity-0"
/>
) : (
<IconComponent
name="Play"
className="absolute ml-0.5 h-5 fill-current stroke-2 text-muted-foreground opacity-100 transition-all group-hover:opacity-0"
/>
)}
</>
);
}
};
const getSpecificClassFromBuildStatus = (
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
) => {
let isInvalid = validationStatus && !validationStatus.valid;
if (buildStatus === BuildStatus.INACTIVE && isInvalid) {
// INACTIVE should have its own class
return "inactive-status";
}
if (
(buildStatus === BuildStatus.BUILT && isInvalid) ||
buildStatus === BuildStatus.ERROR
) {
return isDark ? "built-invalid-status-dark" : "built-invalid-status";
} else if (buildStatus === BuildStatus.BUILDING) {
return "building-status";
} else {
return "";
}
};
const getNodeBorderClassName = (
selected: boolean,
showNode: boolean,
buildStatus: BuildStatus | undefined,
validationStatus: validationStatusType | null
) => {
const specificClassFromBuildStatus = getSpecificClassFromBuildStatus(
buildStatus,
validationStatus
);
const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode);
return classNames(
baseBorderClass,
nodeSizeClass,
"generic-node-div",
specificClassFromBuildStatus
);
};
const getBaseBorderClass = (selected) =>
selected ? "border border-ring" : "border";
const getNodeSizeClass = (showNode) =>
showNode ? "w-96 rounded-lg" : "w-26 h-26 rounded-full";
const memoizedNodeToolbarComponent = useMemo(() => {
return (
<NodeToolbar>
<NodeToolbarComponent
position={{ x: xPos, y: yPos }}
data={data}
deleteNode={(id) => {
takeSnapshot();
deleteNode(id);
}}
setShowNode={(show: boolean) => {
setShowNode={(show) => {
setNode(data.id, (old) => ({
...old,
data: { ...old.data, showNode: show },
}));
}}
setShowState={setShowNode}
numberOfHandles={handles}
showNode={showNode}
></NodeToolbarComponent>
openAdvancedModal={false}
onCloseAdvancedModal={() => {}}
updateNodeCode={updateNodeCode}
isOutdated={isOutdated}
selected={selected}
/>
</NodeToolbar>
);
}, [
data,
deleteNode,
takeSnapshot,
setNode,
setShowNode,
handles,
showNode,
updateNodeCode,
isOutdated,
selected,
]);
return (
<>
{memoizedNodeToolbarComponent}
<div
className={classNames(
selected ? "border border-ring" : "border",
showNode ? " w-96 rounded-lg" : " w-26 h-26 rounded-full",
"generic-node-div"
className={getNodeBorderClassName(
selected,
showNode,
buildStatus,
validationStatus
)}
>
{data.node?.beta && showNode && (
@ -220,28 +447,39 @@ export default function GenericNode({
/>
</div>
) : (
<ShadTooltip content={data.node?.display_name}>
<div
className="flex"
onDoubleClick={() => {
setInputName(true);
takeSnapshot();
}}
>
<div className="group flex items-start gap-1.5">
<ShadTooltip content={data.node?.display_name}>
<div
onDoubleClick={(event) => {
if (nameEditable) {
setInputName(true);
}
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
data-testid={"title-" + data.node?.display_name}
className="generic-node-tooltip-div pr-2 text-primary"
className="generic-node-tooltip-div cursor-text text-primary"
>
{data.node?.display_name}
</div>
{nameEditable && (
</ShadTooltip>
{nameEditable && (
<div
onClick={(event) => {
setInputName(true);
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
>
<IconComponent
name="Pencil"
className="h-4 w-4 text-ring"
name="PencilLine"
className="hidden h-3 w-3 text-status-blue group-hover:block"
/>
)}
</div>
</ShadTooltip>
</div>
)}
</div>
)}
</div>
)}
@ -267,13 +505,34 @@ export default function GenericNode({
})}
data={data}
color={
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors.unknown
data.node?.template[templateField].input_types &&
data.node?.template[templateField].input_types!
.length > 0
? nodeColors[
data.node?.template[templateField]
.input_types![
data.node?.template[templateField]
.input_types!.length - 1
]
] ??
nodeColors[
types[
data.node?.template[templateField]
.input_types![
data.node?.template[templateField]
.input_types!.length - 1
]
]
]
: nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors[
types[
data.node?.template[templateField].type!
]
] ??
nodeColors.unknown
}
title={getFieldTitle(
data.node?.template!,
@ -334,65 +593,64 @@ export default function GenericNode({
</>
)}
</div>
{showNode && (
<div className="round-button-div">
<div>
<Tooltip
title={
isBuilding ? (
<span>Building...</span>
) : !validationStatus ? (
<span className="flex">
Build{" "}
<IconComponent
name="Zap"
className="mx-0.5 h-5 fill-build-trigger stroke-build-trigger stroke-1"
/>{" "}
flow to validate status.
</span>
) : (
<div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string"
? `Duration: ${validationStatus.duration}\n${validationStatus.params}`
.split("\n")
.map((line, index) => (
<div key={index}>{line}</div>
))
: ""}
<ShadTooltip
content={
buildStatus === BuildStatus.BUILDING ? (
<span> {STATUS_BUILDING} </span>
) : !validationStatus ? (
<span className="flex">{STATUS_BUILD}</span>
) : (
<div className="max-h-100 p-2">
<div>
{lastRunTime && (
<div className="justify-left flex font-normal text-muted-foreground">
<div>{RUN_TIMESTAMP_PREFIX}</div>
<div className="ml-1 text-status-blue">
{lastRunTime}
</div>
</div>
)}
</div>
<div className="justify-left flex font-normal text-muted-foreground">
<div>Duration:</div>
<div className="mb-3 ml-1 text-status-blue">
{validationStatus?.data.duration}
</div>
)
}
>
<div className="generic-node-status-position">
<div
className={classNames(
validationStatus && validationStatus.valid
? "green-status"
: "status-build-animation",
"status-div"
)}
></div>
<div
className={classNames(
validationStatus && !validationStatus.valid
? "red-status"
: "status-build-animation",
"status-div"
)}
></div>
<div
className={classNames(
!validationStatus || isBuilding
? "yellow-status"
: "status-build-animation",
"status-div"
)}
></div>
</div>
<hr />
<span className="mb-2 mt-2 flex justify-center font-semibold text-muted-foreground">
Output
</span>
<div className="max-h-96 overflow-auto font-normal custom-scroll">
{validationString.split("\n").map((line, index) => (
<div className="font-normal" key={index}>
{line}
</div>
))}
</div>
</div>
</Tooltip>
</div>
</div>
)
}
side="bottom"
>
<Button
onClick={() => {
if (buildStatus === BuildStatus.BUILDING || isBuilding)
return;
setValidationStatus(null);
buildFlow({ stopNodeId: data.id });
}}
variant="secondary"
className={"group h-9 px-1.5"}
>
<div>
<div className="generic-node-status-position flex items-center justify-center">
{renderIconStatus(buildStatus, validationStatus)}
</div>
</div>
</Button>
</ShadTooltip>
)}
</div>
</div>
@ -413,6 +671,7 @@ export default function GenericNode({
autoFocus
onBlur={() => {
setInputDescription(false);
setInputName(false);
setNodeDescription(nodeDescription);
setNode(data.id, (old) => ({
...old,
@ -460,7 +719,7 @@ export default function GenericNode({
? "font-light italic"
: ""
)}
onDoubleClick={() => {
onDoubleClick={(e) => {
setInputDescription(true);
takeSnapshot();
}}
@ -475,15 +734,7 @@ export default function GenericNode({
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.sort((a, b) => {
if (priorityFields.has(a.toLowerCase())) {
return -1;
} else if (priorityFields.has(b.toLowerCase())) {
return 1;
} else {
return a.localeCompare(b);
}
})
.sort((a, b) => sortFields(a, b, data.node?.field_order ?? []))
.map((templateField: string, idx) => (
<div key={idx}>
{data.node!.template[templateField].show &&
@ -500,13 +751,31 @@ export default function GenericNode({
})}
data={data}
color={
nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors.unknown
data.node?.template[templateField].input_types &&
data.node?.template[templateField].input_types!
.length > 0
? nodeColors[
data.node?.template[templateField].input_types![
data.node?.template[templateField]
.input_types!.length - 1
]
] ??
nodeColors[
types[
data.node?.template[templateField]
.input_types![
data.node?.template[templateField]
.input_types!.length - 1
]
]
]
: nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors.unknown
}
title={getFieldTitle(
data.node?.template!,

View file

@ -1,3 +1,4 @@
import { Cross2Icon } from "@radix-ui/react-icons";
import { useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import {
@ -5,6 +6,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "../../components/ui/popover";
import { ZERO_NOTIFICATIONS } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import { AlertDropdownType } from "../../types/alerts";
import SingleAlert from "./components/singleAlertComponent";
@ -45,15 +47,15 @@ export default function AlertDropdown({
setTimeout(clearNotificationList, 100);
}}
>
<IconComponent name="Trash2" className="h-[1.1rem] w-[1.1rem]" />
<IconComponent name="Trash2" className="h-4 w-4" />
</button>
<button
className="text-foreground hover:text-status-red"
className="text-foreground opacity-70 hover:opacity-100"
onClick={() => {
setOpen(false);
}}
>
<IconComponent name="X" className="h-5 w-5" />
<Cross2Icon className="h-4 w-4" />
</button>
</div>
</div>
@ -68,7 +70,7 @@ export default function AlertDropdown({
))
) : (
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
No new notifications
{ZERO_NOTIFICATIONS}
</div>
)}
</div>

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="margin: auto; background: none; display: block; shape-rendering: auto;" width="271px" height="271px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
<defs>
<filter id="ldio-978hsxudfzl-filter" x="-100%" y="-100%" width="300%" height="300%" color-interpolation-filters="sRGB">
<feGaussianBlur in="SourceGraphic" stdDeviation="3.6"></feGaussianBlur>
<feComponentTransfer result="cutoff">
<feFuncA type="table" tableValues="0 0 0 0 0 0 1 1 1 1 1"></feFuncA>
</feComponentTransfer>
</filter>
</defs>
<g filter="url(#ldio-978hsxudfzl-filter)"><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#2edbb5">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="5s" repeatCount="indefinite" begin="-0.2s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="5s" repeatCount="indefinite" begin="0s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#1d99ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="2.5s" repeatCount="indefinite" begin="-0.15000000000000002s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="2.5s" repeatCount="indefinite" begin="-0.05s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#4f41ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="1.6666666666666665s" repeatCount="indefinite" begin="-0.1s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="1.6666666666666665s" repeatCount="indefinite" begin="-0.1s"></animateTransform>
</g>
</g><g transform="translate(50 50)">
<g>
<circle cx="8" cy="0" r="5" fill="#8400ff">
<animate attributeName="r" keyTimes="0;0.5;1" values="5.3999999999999995;12.6;5.3999999999999995" dur="1.25s" repeatCount="indefinite" begin="-0.05s"></animate>
</circle>
<animateTransform attributeName="transform" type="rotate" keyTimes="0;1" values="0;360" dur="1.25s" repeatCount="indefinite" begin="-0.15000000000000002s"></animateTransform>
</g>
</g></g>
<!-- [ldio] generated by https://loading.io/ --></svg>

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.5 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 53 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -12,6 +12,7 @@ export default function AccordionComponent({
children,
open = [],
keyValue,
sideBar,
}: AccordionComponentType): JSX.Element {
const [value, setValue] = useState(
open.length === 0 ? "" : getOpenAccordion()
@ -45,12 +46,14 @@ export default function AccordionComponent({
onClick={() => {
handleClick();
}}
className="ml-3"
className={
sideBar ? "w-full bg-muted px-[0.75rem] py-[0.5rem]" : "ml-3"
}
>
{trigger}
</AccordionTrigger>
<AccordionContent className="AccordionContent">
{children}
<AccordionContent>
<div className="AccordionContent flex flex-col">{children}</div>
</AccordionContent>
</AccordionItem>
</Accordion>

View file

@ -26,7 +26,7 @@ export default function CrashErrorComponent({
<CardContent className="grid">
<div>
<p>
Please report errors with detailed tracebacks on the{" "}
Please report errors with detailed tracebacks on the{" "}
<a
href="https://github.com/logspace-ai/langflow/issues"
target="_blank"
@ -37,7 +37,7 @@ export default function CrashErrorComponent({
</a>{" "}
page.
<br></br>
Thank you!
Thank you!
</p>
</div>
</CardContent>

View file

@ -13,6 +13,8 @@ export default function DropdownButton({
firstButtonName,
onFirstBtnClick,
options,
plusButton = false,
dropdownOptions = true,
}: dropdownButtonPropsType): JSX.Element {
const [showOptions, setShowOptions] = useState<boolean>(false);
@ -23,28 +25,35 @@ export default function DropdownButton({
<Button
id="new-project-btn"
variant="primary"
className="relative pr-10"
className={
"relative" + dropdownOptions ? " pl-[12px]" : " pl-[12px] pr-10"
}
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
onFirstBtnClick();
}}
>
{plusButton && (
<IconComponent name="Plus" className="main-page-nav-button" />
)}
{firstButtonName}
<div
className="absolute right-2 items-center text-muted-foreground"
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
setShowOptions(!showOptions);
}}
>
{!showOptions ? (
<IconComponent name="ChevronDown" />
) : (
<IconComponent name="ChevronUp" />
)}
</div>
{dropdownOptions && (
<div
className="absolute right-2 items-center text-muted-foreground"
onClick={(event) => {
event.stopPropagation();
event.preventDefault();
setShowOptions(!showOptions);
}}
>
{!showOptions ? (
<IconComponent name="ChevronDown" />
) : (
<IconComponent name="ChevronUp" />
)}
</div>
)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent

View file

@ -74,7 +74,7 @@ export const EditFlowSettings: React.FC<InputProps> = ({
onChange={handleDescriptionChange}
value={description!}
placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal"
className="mt-2 max-h-[100px] resize-none font-normal"
rows={3}
onDoubleClickCapture={(event) => {
handleFocus(event);

View file

@ -1,17 +0,0 @@
import Tooltip, { TooltipProps, tooltipClasses } from "@mui/material/Tooltip";
import { styled } from "@mui/material/styles";
export const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))(({ theme }) => ({
[`& .${tooltipClasses.tooltip}`]: {
backgroundColor: theme.palette.common.white,
color: "rgba(0, 0, 0, 0.87)",
boxShadow: theme.shadows[2],
fontSize: 14,
},
[`& .${tooltipClasses.arrow}:before`]: {
color: theme.palette.common.white,
boxShadow: theme.shadows[1],
},
}));

View file

@ -1,3 +0,0 @@
export default function LoadingSpinner({}) {
return <></>;
}

View file

@ -1,18 +0,0 @@
import { RadialProgressType } from "../../types/components";
export default function RadialProgressComponent({
value,
color,
}: RadialProgressType): JSX.Element {
const style = {
"--value": value! * 100,
"--size": "1.5rem",
"--thickness": "2px",
} as React.CSSProperties;
return (
<div className={"radial-progress " + color} style={style}>
<strong className="text-[8px]">{Math.trunc(value! * 100)}%</strong>
</div>
);
}

View file

@ -1,45 +0,0 @@
"use client";
import type { FC } from "react";
import React from "react";
import { Tooltip as ReactTooltip } from "react-tooltip";
import "react-tooltip/dist/react-tooltip.css";
import { TooltipProps } from "../../types/components";
import { classNames } from "../../utils/utils";
const TooltipReact: FC<TooltipProps> = ({
selector,
content,
disabled,
position = "top",
children,
htmlContent,
className,
clickable,
delayShow,
}: TooltipProps): JSX.Element => {
return (
<div className="tooltip-container">
{React.cloneElement(children as React.ReactElement, {
"data-tooltip-id": selector,
})}
<ReactTooltip
id={selector}
content={content}
className={classNames(
"z-[9999] !bg-white !text-xs !font-normal !text-foreground !opacity-100 !shadow-md",
className!
)}
place={position}
clickable={clickable}
isOpen={disabled ? false : undefined}
delayShow={delayShow}
positionStrategy="absolute"
float={true}
>
{htmlContent && htmlContent}
</ReactTooltip>
</div>
);
};
export default TooltipReact;

View file

@ -1,4 +1,5 @@
import { ShadToolTipType } from "../../types/components";
import { cn } from "../../utils/utils";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
export default function ShadTooltip({
@ -14,7 +15,7 @@ export default function ShadTooltip({
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent
className={styleClasses}
className={cn(styleClasses, "max-w-96")}
side={side}
avoidCollisions={false}
sticky="always"

View file

@ -1,14 +0,0 @@
import { TooltipComponentType } from "../../types/components";
import { LightTooltip } from "../LightTooltipComponent";
export default function Tooltip({
children,
title,
placement,
}: TooltipComponentType): JSX.Element {
return (
<LightTooltip placement={placement} title={title} arrow>
{children}
</LightTooltip>
);
}

View file

@ -0,0 +1,31 @@
import { Transition } from "@headlessui/react";
import IconComponent from "../../genericIconComponent";
export default function ChatTrigger({}): JSX.Element {
return (
<Transition
show={true}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<button
className={
"shadow-round-btn-shadow hover:shadow-round-btn-shadow message-button cursor-pointer"
}
>
<div className="flex gap-3">
<IconComponent
name="Zap"
className={"message-button-icon h-6 w-6 transition-all"}
/>
</div>
</button>
</Transition>
);
}

View file

@ -0,0 +1,99 @@
import { useState } from "react";
import { registerGlobalVariable } from "../../controllers/API";
import BaseModal from "../../modals/baseModal";
import useAlertStore from "../../stores/alertStore";
import { useGlobalVariablesStore } from "../../stores/globalVariables";
import { ResponseErrorDetailAPI } from "../../types/api";
import ForwardedIconComponent from "../genericIconComponent";
import InputComponent from "../inputComponent";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
import { Label } from "../ui/label";
import { Textarea } from "../ui/textarea";
//TODO IMPLEMENT FORM LOGIC
export default function AddNewVariableButton({ children }): JSX.Element {
const [key, setKey] = useState("");
const [value, setValue] = useState("");
const [type, setType] = useState("");
const [open, setOpen] = useState(false);
const setErrorData = useAlertStore((state) => state.setErrorData);
const addGlobalVariable = useGlobalVariablesStore(
(state) => state.addGlobalVariable
);
function handleSaveVariable() {
let data: { name: string; value: string; type?: string } = {
name: key,
type,
value,
};
registerGlobalVariable(data)
.then((res) => {
const { name, id, type } = res.data;
addGlobalVariable(name, id, type);
setKey("");
setValue("");
setType("");
setOpen(false);
})
.catch((error) => {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error creating variable",
list: [responseError.response.data.detail ?? "Unknown error"],
});
});
}
return (
<BaseModal open={open} setOpen={setOpen} size="x-small">
<BaseModal.Header
description={
"This variable will be encrypted and will be available for you to use in any of your projects."
}
>
<span className="pr-2"> Create Variable </span>
<ForwardedIconComponent
name="Globe"
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Content>
<div className="flex h-full w-full flex-col gap-4 align-middle">
<Label>Variable Name</Label>
<Input
value={key}
onChange={(e) => {
setKey(e.target.value);
}}
placeholder="Insert a name for the variable..."
></Input>
<Label>Type (optional)</Label>
<InputComponent
setSelectedOption={(e) => {
setType(e);
}}
selectedOption={type}
password={false}
options={["Generic", "Credential"]}
placeholder="Choose a type for the variable..."
></InputComponent>
<Label>Value</Label>
<Textarea
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
placeholder="Insert a value for the variable..."
className="w-full resize-none custom-scroll"
/>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<Button onClick={handleSaveVariable}>Save Variable</Button>
</BaseModal.Footer>
</BaseModal>
);
}

View file

@ -130,7 +130,7 @@ export default function CollectionCardComponent({
return (
<Card
className={cn(
"group relative flex flex-col justify-between overflow-hidden transition-all hover:shadow-md",
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:shadow-md",
disabled ? "pointer-events-none opacity-50" : ""
)}
>
@ -250,7 +250,7 @@ export default function CollectionCardComponent({
>
<Button
variant="ghost"
size="xs"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
@ -275,7 +275,7 @@ export default function CollectionCardComponent({
<Button
disabled={loadingLike}
variant="ghost"
size="xs"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
@ -312,7 +312,7 @@ export default function CollectionCardComponent({
<Button
disabled={loading}
variant="ghost"
size="xs"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "") +

View file

@ -1,189 +0,0 @@
import { Transition } from "@headlessui/react";
import { useState } from "react";
import Loading from "../../../components/ui/loading";
import { postBuildInit } from "../../../controllers/API";
import { FlowType } from "../../../types/flow";
import useAlertStore from "../../../stores/alertStore";
import useFlowStore from "../../../stores/flowStore";
import { parsedDataType } from "../../../types/components";
import { validateNodes } from "../../../utils/reactflowUtils";
import RadialProgressComponent from "../../RadialProgress";
import IconComponent from "../../genericIconComponent";
export default function BuildTrigger({
open,
flow,
setIsBuilt,
}: {
open: boolean;
flow: FlowType;
setIsBuilt: any;
isBuilt: boolean;
}): JSX.Element {
const updateSSEData = useFlowStore((state) => state.updateSSEData);
const isBuilding = useFlowStore((state) => state.isBuilding);
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setFlowState = useFlowStore((state) => state.setFlowState);
const eventClick = isBuilding ? "pointer-events-none" : "";
const [progress, setProgress] = useState(0);
async function handleBuild(flow: FlowType): Promise<void> {
try {
if (isBuilding) {
return;
}
const errors = validateNodes(nodes, edges);
if (errors.length > 0) {
setErrorData({
title: "Oops! Looks like you missed something",
list: errors,
});
return;
}
const minimumLoadingTime = 200; // in milliseconds
const startTime = Date.now();
setIsBuilding(true);
const allNodesValid = await streamNodeData(flow);
await enforceMinimumLoadingTime(startTime, minimumLoadingTime);
setIsBuilt(allNodesValid);
if (!allNodesValid) {
setErrorData({
title: "Oops! Looks like you missed something",
list: [
"Check components and retry. Hover over component status icon 🔴 to inspect.",
],
});
}
if (errors.length === 0 && allNodesValid) {
setSuccessData({
title: "Flow is ready to run",
});
}
} catch (error) {
console.error("Error:", error);
} finally {
setIsBuilding(false);
}
}
async function streamNodeData(flow: FlowType) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID
const response = await postBuildInit(flow);
const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults: boolean[] = [];
const apiUrl = `/api/v1/build/stream/${flowId}`;
return new Promise<boolean>((resolve, reject) => {
const eventSource = new EventSource(apiUrl);
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
eventSource.close();
resolve(validationResults.every((result) => result));
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
setFlowState(parsedData);
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
eventSource.close();
reject(new Error("Streaming failed"));
};
});
}
function processStreamResult(parsedData: parsedDataType) {
// Process each chunk of data here
// Parse the chunk and update the context
try {
updateSSEData({ [parsedData.id]: parsedData });
} catch (err) {
console.log("Error parsing stream data: ", err);
}
return parsedData.valid;
}
async function enforceMinimumLoadingTime(
startTime: number,
minimumLoadingTime: number
) {
const elapsedTime = Date.now() - startTime;
const remainingTime = minimumLoadingTime - elapsedTime;
if (remainingTime > 0) {
return new Promise((resolve) => setTimeout(resolve, remainingTime));
}
}
return (
<Transition
show={!open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div className="fixed bottom-20 right-4">
<div
className={`${eventClick} round-button-form`}
onClick={() => {
handleBuild(flow);
}}
>
<button>
<div className="round-button-div">
{isBuilding && progress < 1 ? (
// Render your loading animation here when isBuilding is true
<RadialProgressComponent
// ! confirm below works
color={"text-build-trigger"}
value={progress}
></RadialProgressComponent>
) : isBuilding ? (
<Loading
strokeWidth={1.5}
className="build-trigger-loading-icon"
/>
) : (
<IconComponent
name="Zap"
className="sh-6 w-6 fill-build-trigger stroke-build-trigger stroke-1"
/>
)}
</div>
</button>
</div>
</div>
</Transition>
);
}

View file

@ -1,71 +0,0 @@
import { Transition } from "@headlessui/react";
import {
CHAT_CANNOT_OPEN_DESCRIPTION,
CHAT_CANNOT_OPEN_TITLE,
FLOW_NOT_BUILT_DESCRIPTION,
FLOW_NOT_BUILT_TITLE,
} from "../../../constants/constants";
import useAlertStore from "../../../stores/alertStore";
import { chatTriggerPropType } from "../../../types/components";
import IconComponent from "../../genericIconComponent";
export default function ChatTrigger({
open,
setOpen,
isBuilt,
canOpen,
}: chatTriggerPropType): JSX.Element {
const setErrorData = useAlertStore((state) => state.setErrorData);
function handleClick(): void {
if (isBuilt) {
if (canOpen) {
setOpen(true);
} else {
setErrorData({
title: CHAT_CANNOT_OPEN_TITLE,
list: [CHAT_CANNOT_OPEN_DESCRIPTION],
});
}
} else {
setErrorData({
title: FLOW_NOT_BUILT_TITLE,
list: [FLOW_NOT_BUILT_DESCRIPTION],
});
}
}
return (
<Transition
show={!open}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<button
onClick={handleClick}
className={
"shadow-round-btn-shadow hover:shadow-round-btn-shadow message-button " +
(!isBuilt || !canOpen ? "cursor-not-allowed" : "cursor-pointer")
}
>
<div className="flex gap-3">
<IconComponent
name="MessagesSquare"
className={
"h-6 w-6 transition-all " +
(isBuilt && canOpen
? "message-button-icon"
: "disabled-message-button-icon")
}
/>
</div>
</button>
</Transition>
);
}

View file

@ -1,25 +1,29 @@
import { useEffect, useRef, useState } from "react";
import { useNodes } from "reactflow";
import { ChatType } from "../../types/chat";
import BuildTrigger from "./buildTrigger";
import ChatTrigger from "./chatTrigger";
import * as _ from "lodash";
import FormModal from "../../modals/formModal";
import { Transition } from "@headlessui/react";
import { useEffect, useMemo, useRef, useState } from "react";
import ApiModal from "../../modals/ApiModal";
import IOModal from "../../modals/IOModal";
import ShareModal from "../../modals/shareModal";
import useFlowStore from "../../stores/flowStore";
import { NodeType } from "../../types/flow";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore";
import { classNames } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import { Separator } from "../ui/separator";
export default function Chat({ flow }: ChatType): JSX.Element {
export default function FlowToolbar(): JSX.Element {
const [open, setOpen] = useState(false);
const isBuilt = useFlowStore((state) => state.isBuilt);
const setIsBuilt = useFlowStore((state) => state.setIsBuilt);
const flowState = useFlowStore((state) => state.flowState);
const hasIO = useFlowStore((state) => state.hasIO);
const hasStore = useStoreStore((state) => state.hasStore);
const validApiKey = useStoreStore((state) => state.validApiKey);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
(event.key === "K" || event.key === "k") &&
(event.metaKey || event.ctrlKey) &&
isBuilt
useFlowStore.getState().hasIO
) {
event.preventDefault();
setOpen((oldState) => !oldState);
@ -29,40 +33,122 @@ export default function Chat({ flow }: ChatType): JSX.Element {
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [isBuilt]);
}, []);
const prevNodesRef = useRef<any[] | undefined>();
const nodes: NodeType[] = useNodes();
useEffect(() => {
const prevNodes = prevNodesRef.current;
const currentNodes = nodes.map((node: NodeType) =>
_.cloneDeep(node.data.node?.template)
);
if (JSON.stringify(prevNodes) !== JSON.stringify(currentNodes)) {
setIsBuilt(false);
}
prevNodesRef.current = currentNodes;
}, [flowState, flow.id]);
const ModalMemo = useMemo(
() => (
<ShareModal
is_component={false}
component={currentFlow!}
disabled={!hasApiKey || !validApiKey || !hasStore}
>
<button
disabled={!hasApiKey || !validApiKey || !hasStore}
className={classNames(
"relative inline-flex h-full w-full items-center justify-center gap-[4px] bg-muted px-5 py-3 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-background hover:bg-hover ",
!hasApiKey || !validApiKey || !hasStore
? " button-disable text-muted-foreground "
: ""
)}
>
<ForwardedIconComponent
name="Share3"
className={classNames(
"-m-0.5 -ml-1 h-6 w-6",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: ""
)}
/>
Share
</button>
</ShareModal>
),
[hasApiKey, validApiKey, currentFlow, hasStore]
);
return (
<>
<div>
<BuildTrigger
open={open}
flow={flow}
setIsBuilt={setIsBuilt}
isBuilt={isBuilt}
/>
{isBuilt && flowState && !!flowState?.input_keys && (
<FormModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
)}
<ChatTrigger
canOpen={!!flowState?.input_keys}
open={open}
setOpen={setOpen}
isBuilt={isBuilt}
/>
</div>
<Transition
show={true}
appear={true}
enter="transition ease-out duration-300"
enterFrom="translate-y-96"
enterTo="translate-y-0"
leave="transition ease-in duration-300"
leaveFrom="translate-y-0"
leaveTo="translate-y-96"
>
<div
className={
"shadow-round-btn-shadow hover:shadow-round-btn-shadow message-button-position flex items-center justify-center gap-7 rounded-sm border bg-muted shadow-md transition-all"
}
>
<div className="flex">
<div className="flex h-full w-full gap-1 rounded-sm text-medium-indigo transition-all">
{hasIO ? (
<IOModal open={open} setOpen={setOpen} disable={!hasIO}>
<div className="relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-medium-indigo transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="Zap"
className={"message-button-icon h-5 w-5 transition-all"}
/>
Run
</div>
</IOModal>
) : (
<div
className={`relative inline-flex w-full cursor-not-allowed items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-muted-foreground transition-all duration-150 ease-in-out ease-in-out`}
>
<ForwardedIconComponent
name="Zap"
className={
"message-button-icon h-5 w-5 fill-muted-foreground stroke-muted-foreground transition-all"
}
/>
Run
</div>
)}
</div>
<div>
<Separator orientation="vertical" />
</div>
<div className="flex cursor-pointer items-center gap-2">
{currentFlow && currentFlow.data && (
<ApiModal flow={currentFlow}>
<div
className={classNames(
"relative inline-flex w-full items-center justify-center gap-1 px-5 py-3 text-sm font-semibold text-foreground transition-all duration-150 ease-in-out hover:bg-hover"
)}
>
<ForwardedIconComponent
name="Code2"
className={" h-5 w-5"}
/>
API
</div>
</ApiModal>
)}
</div>
<div>
<Separator orientation="vertical" />
</div>
<div className="flex items-center gap-2">
<div
className={`side-bar-button ${
!hasApiKey || !validApiKey || !hasStore
? " cursor-not-allowed"
: " cursor-pointer"
}`}
>
{ModalMemo}
</div>
</div>
</div>
</div>
</Transition>
</>
);
}

View file

@ -14,6 +14,8 @@ export default function CodeAreaComponent({
setNodeClass,
id = "",
readonly = false,
open,
setOpen,
}: CodeAreaComponentType) {
const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value)
@ -32,6 +34,8 @@ export default function CodeAreaComponent({
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<CodeAreaModal
open={open}
setOpen={setOpen}
readonly={readonly}
dynamic={dynamic}
value={myValue}

View file

@ -6,7 +6,6 @@ import AccordionComponent from "../../components/AccordionComponent";
import CodeAreaComponent from "../../components/codeAreaComponent";
import Dropdown from "../../components/dropdownComponent";
import FloatComponent from "../../components/floatComponent";
import InputComponent from "../../components/inputComponent";
import InputFileComponent from "../../components/inputFileComponent";
import InputListComponent from "../../components/inputListComponent";
import IntComponent from "../../components/intComponent";
@ -37,8 +36,10 @@ import {
hasDuplicateKeys,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import ShadTooltip from "../ShadTooltipComponent";
import DictComponent from "../dictComponent";
import IconComponent from "../genericIconComponent";
import InputGlobalComponent from "../inputGlobalComponent";
import KeypairListComponent from "../keypairListComponent";
export default function CodeTabsComponent({
@ -54,8 +55,7 @@ export default function CodeTabsComponent({
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
const dark = useDarkStore((state) => state.dark);
const unselectAll = useFlowStore((state) => state.unselectAll);
const setNodes = useFlowStore((state) => state.setNodes);
const setNode = useFlowStore((state) => state.setNode);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
@ -126,7 +126,7 @@ export default function CodeTabsComponent({
<Tabs
value={activeTab}
className={
"api-modal-tabs " +
"api-modal-tabs inset-0 m-0 " +
(isMessage ? "dark " : "") +
(dark && isMessage ? "bg-background" : "")
}
@ -166,7 +166,7 @@ export default function CodeTabsComponent({
) : (
<IconComponent name="Clipboard" className="h-4 w-4" />
)}
{isCopied ? "Copied!" : "Copy code"}
{isCopied ? "Copied!" : "Copy Code"}
</button>
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
@ -181,7 +181,7 @@ export default function CodeTabsComponent({
{tabs.map((tab, idx) => (
<TabsContent
value={idx.toString()}
className="api-modal-tabs-content"
className="api-modal-tabs-content overflow-hidden"
key={idx} // Remember to add a unique key prop
>
{idx < 4 ? (
@ -193,9 +193,9 @@ export default function CodeTabsComponent({
></div>
)}
<SyntaxHighlighter
className="mt-0 h-full w-full overflow-auto custom-scroll"
language={tab.mode}
language={tab.language}
style={oneDark}
className="mt-0 h-full overflow-auto rounded-sm text-left custom-scroll"
>
{tab.code}
</SyntaxHighlighter>
@ -205,10 +205,7 @@ export default function CodeTabsComponent({
<div className="api-modal-according-display">
<div
className={classNames(
"h-[70vh] w-full rounded-lg bg-muted",
1 == 1
? "overflow-scroll overflow-x-hidden custom-scroll"
: "overflow-hidden"
"h-[70vh] w-full overflow-y-auto overflow-x-hidden rounded-lg bg-muted custom-scroll"
)}
>
{data?.map((node: any, i) => (
@ -217,14 +214,22 @@ export default function CodeTabsComponent({
node["data"]["id"]
) && (
<AccordionComponent
trigger={node["data"]["id"]}
trigger={
<ShadTooltip
side="top"
styleClasses="z-50"
content={node["data"]["id"]}
>
<div>{node["data"]["node"]["display_name"]}</div>
</ShadTooltip>
}
open={openAccordion}
keyValue={node["data"]["id"]}
>
<div className="api-modal-table-arrangement">
<Table className="table-fixed bg-muted outline-1">
<TableHeader className="h-10 border-input text-xs font-medium text-ring">
<TableRow className="dark:border-b-muted">
<TableRow className="">
<TableHead className="h-7 text-center">
PARAM
</TableHead>
@ -247,10 +252,7 @@ export default function CodeTabsComponent({
)
.map((templateField, indx) => {
return (
<TableRow
key={indx}
className="h-10 dark:border-b-muted"
>
<TableRow key={indx} className="h-10">
<TableCell className="p-0 text-center text-sm text-foreground">
{templateField}
</TableCell>
@ -346,38 +348,31 @@ export default function CodeTabsComponent({
/>
</div>
) : (
<InputComponent
<InputGlobalComponent
editNode={true}
disabled={false}
password={
node.data.node.template[
templateField
].password ?? false
}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].value === ""
? ""
: node.data.node
.template[
templateField
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
if (node.data) {
setNode(
node.data.id,
(oldNode) => {
let newNode =
cloneDeep(
oldNode
);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[
templateField
].value = target;
return newNode;
}
);
}
tweaks.buildTweakObject!(
node["data"]["id"],
target,
@ -386,6 +381,25 @@ export default function CodeTabsComponent({
]
);
}}
setDb={(value) => {
setNode(
node.data.id,
(oldNode) => {
let newNode =
cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[
templateField
].load_from_db =
value;
return newNode;
}
);
}}
name={templateField}
data={node.data}
/>
)}
</div>
@ -504,7 +518,6 @@ export default function CodeTabsComponent({
<div className="mx-auto">
<Dropdown
editNode={true}
apiModal={true}
options={
node.data.node.template[
templateField
@ -550,6 +563,11 @@ export default function CodeTabsComponent({
<IntComponent
disabled={false}
editNode={true}
rangeSpec={
node.data.node.template[
templateField
].rangeSpec
}
value={
!node.data.node.template[
templateField

View file

@ -1,139 +1,116 @@
import { Listbox, Transition } from "@headlessui/react";
import { Fragment, useEffect, useState } from "react";
import { useRef, useState } from "react";
import { DropDownComponentType } from "../../types/components";
import { classNames } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
import { cn } from "../../utils/utils";
import { default as ForwardedIconComponent } from "../genericIconComponent";
import { Button } from "../ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../ui/command";
import {
Popover,
PopoverContentWithoutPortal,
PopoverTrigger,
} from "../ui/popover";
export default function Dropdown({
disabled,
isLoading,
value,
options,
onSelect,
editNode = false,
numberOfOptions = 0,
apiModal = false,
id = "",
}: DropDownComponentType): JSX.Element {
let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value
);
const [open, setOpen] = useState(false);
useEffect(() => {
setInternalValue(value === "" || !value ? "Choose an option" : value);
}, [value]);
const refButton = useRef<HTMLButtonElement>(null);
return (
<>
{Object.keys(options)?.length > 0 ? (
<>
<Listbox
value={internalValue}
onChange={(value) => {
setInternalValue(value);
onSelect(value);
}}
>
{({ open }) => (
<>
<div className={"relative mt-1"}>
<Listbox.Button
data-test={`${id ?? ""}`}
className={
editNode
? "dropdown-component-outline"
: "dropdown-component-false-outline"
}
>
<span
className="dropdown-component-display"
data-testid={`${id ?? ""}-display`}
>
{internalValue}
</span>
<span className={"dropdown-component-arrow"}>
<IconComponent
name="ChevronsUpDown"
className="dropdown-component-arrow-color"
aria-hidden="true"
/>
</span>
</Listbox.Button>
<Transition
show={open}
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className={classNames(
editNode
? "dropdown-component-true-options nowheel custom-scroll"
: "dropdown-component-false-options nowheel custom-scroll",
apiModal ? "mb-2 w-[250px]" : "absolute w-full"
)}
>
{options?.map((option, id) => (
<Listbox.Option
key={id}
className={({ active }) =>
classNames(
active ? " bg-accent" : "",
editNode
? "dropdown-component-false-option"
: "dropdown-component-true-option"
)
}
value={option}
>
{({ selected, active }) => (
<>
<span
className={classNames(
selected ? "font-semibold" : "font-normal",
"block truncate "
)}
data-testid={`${option}-${id ?? ""}-option`}
>
{option}
</span>
{selected ? (
<span
className={classNames(
active ? "text-background " : "",
"dropdown-component-choosal"
)}
>
<IconComponent
name="Check"
className={
active
? "dropdown-component-check-icon"
: "dropdown-component-check-icon"
}
aria-hidden="true"
/>
</span>
) : null}
</>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
disabled={disabled}
variant="primary"
size="xs"
role="combobox"
ref={refButton}
aria-expanded={open}
data-test={`${id ?? ""}`}
className={cn(
editNode
? "dropdown-component-outline"
: "dropdown-component-false-outline",
"w-full justify-between font-normal",
editNode ? "input-edit-node" : "py-2"
)}
>
{value &&
value !== "" &&
options.find((option) => option === value)
? options.find((option) => option === value)
: "Choose an option..."}
<ForwardedIconComponent
name="ChevronsUpDown"
className="ml-2 h-4 w-4 shrink-0 opacity-50"
/>
</Button>
</PopoverTrigger>
<PopoverContentWithoutPortal
className="nocopy nowheel nopan nodelete nodrag noundo w-full p-0"
style={{ minWidth: refButton?.current?.clientWidth ?? "200px" }}
>
<Command>
<CommandInput placeholder="Search options..." className="h-9" />
<CommandList>
<CommandEmpty>No values found.</CommandEmpty>
<CommandGroup defaultChecked={false}>
{options?.map((option, id) => (
<CommandItem
key={id}
value={option}
onSelect={(currentValue) => {
onSelect(currentValue);
setOpen(false);
}}
data-testid={`${option}-${id ?? ""}-option`}
>
{option}
<ForwardedIconComponent
name="Check"
className={cn(
"ml-auto h-4 w-4 text-primary",
value === option ? "opacity-100" : "opacity-0"
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
)}
</Listbox>
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContentWithoutPortal>
</Popover>
</>
) : (
<>
<div>
<span className="text-sm italic">
No parameters are available for display.
</span>
</div>
{(!isLoading && (
<div>
<span className="text-sm italic">
No parameters are available for display.
</span>
</div>
)) || (
<div>
<span className="text-sm italic">Loading...</span>
</div>
)}
</>
)}
</>

View file

@ -0,0 +1,100 @@
import { useNavigate } from "react-router-dom";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FlowType } from "../../types/flow";
import { updateIds } from "../../utils/reactflowUtils";
import { cn } from "../../utils/utils";
import ShadTooltip from "../ShadTooltipComponent";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "../ui/card";
export default function CollectionCardComponent({
flow,
}: {
flow: FlowType;
authorized?: boolean;
}) {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const navigate = useNavigate();
const emojiRegex = /\p{Emoji}/u;
const isEmoji = (str: string) => emojiRegex.test(str);
return (
<Card
className={cn(
"group relative flex h-48 w-2/6 flex-col justify-between overflow-hidden transition-all hover:shadow-md"
)}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
{flow.icon && isEmoji(flow.icon) && (
<div
className="flex items-center justify-center rounded-md p-2 align-middle"
style={{ backgroundColor: flow.icon_bg_color }}
>
<div className="h-7 w-7 pl-0.5">{flow.icon}</div>
</div>
)}
{(!flow.icon || !isEmoji(flow.icon)) && (
<div
className="flex items-center justify-center rounded-md p-2 align-middle"
style={{ backgroundColor: flow.icon_bg_color }}
>
<IconComponent
className={cn("h-7 w-7 flex-shrink-0 text-flow-icon")}
name={flow.icon || "Group"}
/>
</div>
)}
<ShadTooltip content={flow.name}>
<div className="w-full truncate">{flow.name}</div>
</ShadTooltip>
</CardTitle>
</div>
<CardDescription className="pb-2 pt-2">
<ShadTooltip
side="bottom"
styleClasses="z-50"
content={flow.description}
>
<div className="truncate-doubleline">{flow.description}</div>
</ShadTooltip>
</CardDescription>
</CardHeader>
</div>
<CardFooter>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-full flex-wrap justify-end gap-2">
<Button
onClick={() => {
updateIds(flow.data!);
addFlow(true, flow).then((id) => {
navigate("/flow/" + id);
});
}}
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Select Flow
</Button>
</div>
</div>
</CardFooter>
</Card>
);
}

View file

@ -1,16 +1,51 @@
import BaseModal from "../../modals/baseModal";
import { fetchErrorComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
export default function FetchErrorComponent({
message,
description,
openModal,
setRetry,
isLoadingHealth,
}: fetchErrorComponentType) {
return (
<div role="status" className="m-auto flex flex-col items-center">
<IconComponent className={`h-16 w-16`} name="Unplug"></IconComponent>
<br></br>
<span className="text-lg text-almost-medium-blue">{message}</span>
<span className="text-lg text-almost-medium-blue">{description}</span>
</div>
<>
<BaseModal size="small-h-full" open={openModal} type="modal">
<BaseModal.Content>
<div role="status" className="m-auto flex flex-col items-center">
<IconComponent
className={`h-16 w-16`}
name="Unplug"
></IconComponent>
<br></br>
<span className="text-lg text-almost-medium-blue">{message}</span>
<span className="text-lg text-almost-medium-blue">
{description}
</span>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<div className="m-auto">
<Button
disabled={isLoadingHealth}
onClick={() => {
setRetry();
}}
>
{isLoadingHealth ? (
<div>
<IconComponent name={"Loader2"} className={"animate-spin"} />
</div>
) : (
"Retry"
)}
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
</>
);
}

View file

@ -1,36 +1,57 @@
import { forwardRef } from "react";
import dynamicIconImports from "lucide-react/dynamicIconImports";
import { Suspense, forwardRef, lazy, memo } from "react";
import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import { cn } from "../../utils/utils";
import Loading from "../ui/loading";
const ForwardedIconComponent = forwardRef(
(
{
name,
className,
iconColor,
stroke,
strokeWidth,
id = "",
}: IconComponentProps,
ref
) => {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
const ForwardedIconComponent = memo(
forwardRef(
(
{
name,
className,
iconColor,
stroke,
strokeWidth,
id = "",
}: IconComponentProps,
ref
) => {
let TargetIcon = nodeIconsLucide[name];
if (!TargetIcon) {
// check if name exists in dynamicIconImports
if (!dynamicIconImports[name]) {
TargetIcon = nodeIconsLucide["unknown"];
} else TargetIcon = lazy(dynamicIconImports[name]);
}
const style = {
strokeWidth: strokeWidth ?? 1.5,
...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }),
};
const style = {
strokeWidth: strokeWidth ?? 1.5,
...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }),
};
return (
<TargetIcon
className={className}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
);
}
if (!TargetIcon) {
return null; // Render nothing until the icon is loaded
}
const fallback = (
<div className={cn(className, "flex items-center justify-center")}>
<Loading />
</div>
);
return (
<Suspense fallback={fallback}>
<TargetIcon
className={className}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
</Suspense>
);
}
)
);
export default ForwardedIconComponent;

View file

@ -9,10 +9,15 @@ import {
import { useNavigate } from "react-router-dom";
import { Node } from "reactflow";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { SAVED_HOVER } from "../../../../constants/constants";
import ExportModal from "../../../../modals/exportModal";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { cn } from "../../../../utils/utils";
import ShadTooltip from "../../../ShadTooltipComponent";
import IconComponent from "../../../genericIconComponent";
import { Button } from "../../../ui/button";
@ -24,30 +29,51 @@ export const MenuBar = ({
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const undo = useFlowsManagerStore((state) => state.undo);
const redo = useFlowsManagerStore((state) => state.redo);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const [openSettings, setOpenSettings] = useState(false);
const n = useFlowStore((state) => state.nodes);
const nodes = useFlowStore((state) => state.nodes);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const navigate = useNavigate();
const isBuilding = useFlowStore((state) => state.isBuilding);
function handleAddFlow() {
function handleAddFlow(duplicate?: boolean) {
try {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
// saveFlowStyleInDataBase();
if (duplicate) {
if (!currentFlow) {
throw new Error("No flow to duplicate");
}
addFlow(true, currentFlow).then((id) => {
setSuccessData({ title: "Flow duplicated successfully" });
navigate("/flow/" + id);
});
} else {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
}
function printByBuildStatus() {
if (isBuilding) {
return "Building...";
} else if (saveLoading) {
return "Saving...";
}
return "Saved";
}
return currentFlow ? (
<div className="round-button-div">
<button
onClick={() => {
removeFunction(n);
navigate(-1);
removeFunction(nodes);
navigate("/");
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
@ -73,6 +99,15 @@ export const MenuBar = ({
<IconComponent name="Plus" className="header-menu-options" />
New
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
handleAddFlow(true);
}}
className="cursor-pointer"
>
<IconComponent name="Copy" className="header-menu-options" />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
@ -86,7 +121,31 @@ export const MenuBar = ({
/>
Settings
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
uploadFlow({ newProject: false, isComponent: false }).catch(
(error) => {
setErrorData({
title: UPLOAD_ERROR_ALERT,
list: [error],
});
}
);
}}
>
<IconComponent name="FileUp" className="header-menu-options " />
Import
</DropdownMenuItem>
<ExportModal>
<div className="header-menubar-item">
<IconComponent
name="FileDown"
className="header-menu-options"
/>
Export
</div>
</ExportModal>
<DropdownMenuItem
onClick={() => {
undo();
@ -95,6 +154,17 @@ export const MenuBar = ({
>
<IconComponent name="Undo" className="header-menu-options " />
Undo
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
/>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
Ctrl +{" "}
</span>
)}
<span className="absolute right-2 top-[0.4em]">Z</span>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
@ -104,6 +174,17 @@ export const MenuBar = ({
>
<IconComponent name="Redo" className="header-menu-options " />
Redo
{navigator.userAgent.toUpperCase().includes("MAC") ? (
<IconComponent
name="Command"
className="absolute right-[1.15rem] top-[0.65em] h-3.5 w-3.5 stroke-2"
/>
) : (
<span className="absolute right-[1.15rem] top-[0.40em] stroke-2">
Ctrl +{" "}
</span>
)}
<span className="absolute right-2 top-[0.4em]">Y</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@ -112,6 +193,31 @@ export const MenuBar = ({
setOpen={setOpenSettings}
></FlowSettingsModal>
</div>
{(currentFlow.updated_at || saveLoading) && (
<ShadTooltip
content={
SAVED_HOVER +
new Date(currentFlow.updated_at ?? "").toLocaleString("en-US", {
hour: "numeric",
minute: "numeric",
second: "numeric",
})
}
side="bottom"
styleClasses="cursor-default"
>
<div className="flex cursor-default items-center gap-1.5 text-sm text-muted-foreground">
<IconComponent
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
"h-4 w-4",
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
)}
/>
{printByBuildStatus()}
</div>
</ShadTooltip>
)}
</div>
) : (
<></>

View file

@ -1,5 +1,6 @@
import { useContext, useEffect } from "react";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
import { FaDiscord, FaGithub } from "react-icons/fa";
import { RiTwitterXFill } from "react-icons/ri";
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
import AlertDropdown from "../../alerts/alertDropDown";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
@ -33,7 +34,7 @@ export default function Header(): JSX.Element {
const removeFlow = useFlowsManagerStore((store) => store.removeFlow);
const hasStore = useStoreStore((state) => state.hasStore);
const { id } = useParams();
const n = useFlowStore((state) => state.nodes);
const nodes = useFlowStore((state) => state.nodes);
const dark = useDarkStore((state) => state.dark);
const setDark = useDarkStore((state) => state.setDark);
@ -57,7 +58,7 @@ export default function Header(): JSX.Element {
return (
<div className="header-arrangement">
<div className="header-start-display lg:w-[30%]">
<Link to="/" onClick={() => checkForChanges(n)}>
<Link to="/" onClick={() => checkForChanges(nodes)}>
<span className="ml-4 text-2xl">⛓️</span>
</Link>
<MenuBar removeFunction={checkForChanges} />
@ -74,7 +75,7 @@ export default function Header(): JSX.Element {
}
size="sm"
onClick={() => {
checkForChanges(n);
checkForChanges(nodes);
}}
>
<IconComponent name="Home" className="h-4 w-4" />
@ -100,7 +101,7 @@ export default function Header(): JSX.Element {
variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm"
onClick={() => {
checkForChanges(n);
checkForChanges(nodes);
}}
>
<IconComponent name="Store" className="h-4 w-4" />
@ -127,7 +128,7 @@ export default function Header(): JSX.Element {
rel="noreferrer"
className="text-muted-foreground"
>
<FaTwitter className="side-bar-button-size" />
<RiTwitterXFill className="side-bar-button-size" />
</a>
<a
href="https://discord.gg/EqksyE2EX9"

View file

@ -1,14 +1,25 @@
import * as Form from "@radix-ui/react-form";
import { PopoverAnchor } from "@radix-ui/react-popover";
import { useEffect, useRef, useState } from "react";
import useAlertStore from "../../stores/alertStore";
import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import { classNames, cn } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import {
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "../ui/command";
import { Input } from "../ui/input";
import { Popover, PopoverContentWithoutPortal } from "../ui/popover";
export default function InputComponent({
autoFocus = false,
onBlur,
value,
value = "",
onChange,
disabled,
required = false,
@ -19,16 +30,30 @@ export default function InputComponent({
className,
id = "",
blurOnEnter = false,
optionsIcon = "ChevronsUpDown",
selectedOption,
setSelectedOption,
options = [],
optionsPlaceholder = "Search options...",
optionsButton,
optionButton,
}: InputComponentType): JSX.Element {
const setErrorData = useAlertStore.getState().setErrorData;
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
const [showOptions, setShowOptions] = useState<boolean>(false);
// Clear component state
useEffect(() => {
if (disabled && value !== "") {
if (disabled && value && onChange && value !== "") {
onChange("");
}
}, [disabled]);
function onInputLostFocus(event): void {
if (onBlur) onBlur(event);
}
return (
<div className="relative w-full">
{isForm ? (
@ -36,7 +61,7 @@ export default function InputComponent({
<Input
id={"form-" + id}
ref={refInput}
onBlur={onBlur}
onBlur={onInputLostFocus}
autoFocus={autoFocus}
type={password && !pwdVisible ? "password" : "text"}
value={value}
@ -53,7 +78,7 @@ export default function InputComponent({
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
onChange(e.target.value);
onChange && onChange(e.target.value);
}}
onCopy={(e) => {
e.preventDefault();
@ -65,39 +90,194 @@ export default function InputComponent({
/>
</Form.Control>
) : (
<Input
id={id}
ref={refInput}
type="text"
onBlur={onBlur}
value={value}
autoFocus={autoFocus}
disabled={disabled}
required={required}
className={classNames(
password && !pwdVisible && value !== ""
? " text-clip password "
: "",
editNode ? " input-edit-node " : "",
password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : "",
className!
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
onChange(e.target.value);
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
/>
<>
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
<PopoverAnchor>
<Input
id={id}
ref={refInput}
type="text"
onBlur={onInputLostFocus}
value={
(selectedOption !== "" || !onChange) && setSelectedOption
? selectedOption
: value
}
autoFocus={autoFocus}
disabled={disabled}
onClick={() => {
(selectedOption !== "" || !onChange) &&
setSelectedOption &&
setShowOptions(true);
}}
required={required}
className={classNames(
password &&
selectedOption === "" &&
!pwdVisible &&
value !== ""
? " text-clip password "
: "",
editNode ? " input-edit-node " : "",
password && setSelectedOption ? "pr-[62.9px]" : "",
(!password && setSelectedOption) ||
(password && !setSelectedOption)
? "pr-8"
: "",
className!
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
// if the user copies a password from another input
// it might come as ••••••••••• it causes errors
// in ascii encoding, so we need to handle it
if (password) {
// check if all chars are •
if (
e.target.value.split("").every((char) => char === "•") &&
e.target.value !== ""
) {
setErrorData({
title: `Invalid characters: ${e.target.value}`,
list: [
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
],
});
}
}
onChange && onChange(e.target.value);
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter")
refInput.current?.blur();
}}
data-testid={editNode ? id + "-edit" : id}
/>
</PopoverAnchor>
<PopoverContentWithoutPortal
className="nocopy nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom"
align="center"
>
<Command
filter={(value, search) => {
if (value.includes(search) || value.includes("doNotFilter-"))
return 1; // ensures items arent filtered
return 0;
}}
>
<CommandInput placeholder={optionsPlaceholder} />
<CommandList>
<CommandGroup defaultChecked={false}>
{options.map((option, id) => (
<CommandItem
className="group"
key={option + id}
value={option}
onSelect={(currentValue) => {
setSelectedOption &&
setSelectedOption(
currentValue === selectedOption
? ""
: currentValue
);
setShowOptions(false);
}}
>
<div className="group flex w-full items-center justify-between">
<div className="flex items-center">
<div
className={cn(
"relative mr-2 h-4 w-4",
selectedOption === option
? "opacity-100"
: "opacity-0"
)}
>
<div className="absolute opacity-100 transition-all group-hover:opacity-0">
<ForwardedIconComponent
name="Check"
className="mr-2 h-4 w-4 text-primary"
aria-hidden="true"
/>
</div>
<div className="absolute opacity-0 transition-all group-hover:opacity-100">
<ForwardedIconComponent
name="X"
className="mr-2 h-4 w-4 text-status-red"
aria-hidden="true"
/>
</div>
</div>
{option}
</div>
{optionButton && optionButton(option)}
</div>
</CommandItem>
))}
{optionsButton && optionsButton}
</CommandGroup>
</CommandList>
</Command>
</PopoverContentWithoutPortal>
</Popover>
<div
className={cn(
"pointer-events-auto absolute inset-y-0 h-full w-full cursor-pointer",
(selectedOption !== "" || !onChange) && setSelectedOption
? ""
: "hidden"
)}
onClick={
(selectedOption !== "" || !onChange) && setSelectedOption
? (e) => {
setShowOptions((old) => !old);
e.preventDefault();
e.stopPropagation();
}
: () => {}
}
></div>
</>
)}
{password && (
{setSelectedOption && (
<span
className={cn(
password && selectedOption === "" ? "right-8" : "right-0",
"absolute inset-y-0 flex items-center pr-2.5"
)}
>
<button
onClick={() => {
setShowOptions(!showOptions);
}}
className={cn(
selectedOption !== ""
? "text-medium-indigo"
: "text-muted-foreground",
"hover:text-accent-foreground"
)}
>
<ForwardedIconComponent
name={optionsIcon}
className={"h-4 w-4"}
aria-hidden="true"
/>
</button>
</span>
)}
{password && selectedOption === "" && (
<button
type="button"
tabIndex={-1}
className={classNames(
"mb-px",
editNode
? "input-component-true-button"
: "input-component-false-button"
@ -108,6 +288,7 @@ export default function InputComponent({
}}
>
{password &&
selectedOption === "" &&
(pwdVisible ? (
<svg
xmlns="http://www.w3.org/2000/svg"

View file

@ -1,4 +1,9 @@
import { useEffect, useState } from "react";
import {
CONSOLE_ERROR_MSG,
CONSOLE_SUCCESS_MSG,
INVALID_FILE_ALERT,
} from "../../constants/alerts_constants";
import { uploadFile } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
@ -61,7 +66,7 @@ export default function InputFileComponent({
uploadFile(file, currentFlowId)
.then((res) => res.data)
.then((data) => {
console.log("File uploaded successfully");
console.log(CONSOLE_SUCCESS_MSG);
// Get the file name from the response
const { file_path } = data;
console.log("File name:", file_path);
@ -75,14 +80,13 @@ export default function InputFileComponent({
setLoading(false);
})
.catch(() => {
console.error("Error occurred while uploading file");
console.error(CONSOLE_ERROR_MSG);
setLoading(false);
});
} else {
// Show an error if the file type is not allowed
setErrorData({
title:
"Please select a valid file. Only these file types are allowed:",
title: INVALID_FILE_ALERT,
list: fileTypes,
});
setLoading(false);

View file

@ -0,0 +1,134 @@
import { useEffect } from "react";
import { deleteGlobalVariable } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
import useAlertStore from "../../stores/alertStore";
import { useGlobalVariablesStore } from "../../stores/globalVariables";
import { ResponseErrorDetailAPI } from "../../types/api";
import { InputGlobalComponentType } from "../../types/components";
import { cn } from "../../utils/utils";
import AddNewVariableButton from "../addNewVariableButtonComponent/addNewVariableButton";
import ForwardedIconComponent from "../genericIconComponent";
import InputComponent from "../inputComponent";
import { CommandItem } from "../ui/command";
export default function InputGlobalComponent({
disabled,
onChange,
setDb,
name,
data,
editNode = false,
}: InputGlobalComponentType): JSX.Element {
const globalVariablesEntries = useGlobalVariablesStore(
(state) => state.globalVariablesEntries
);
const getVariableId = useGlobalVariablesStore((state) => state.getVariableId);
const removeGlobalVariable = useGlobalVariablesStore(
(state) => state.removeGlobalVariable
);
const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => {
if (data.node?.template[name])
if (
!globalVariablesEntries.includes(data.node?.template[name].value) &&
data.node?.template[name].load_from_db
) {
onChange("");
setDb(false);
}
}, [globalVariablesEntries]);
function handleDelete(key: string) {
const id = getVariableId(key);
if (id !== undefined) {
deleteGlobalVariable(id)
.then((_) => {
removeGlobalVariable(key);
if (
data?.node?.template[name].value === key &&
data?.node?.template[name].load_from_db
) {
onChange("");
setDb(false);
}
})
.catch((error) => {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error deleting variable",
list: [responseError.response.data.detail ?? "Unknown error"],
});
});
} else {
setErrorData({
title: "Error deleting variable",
list: [cn("ID not found for variable: ", key)],
});
}
}
return (
<InputComponent
id={"input-" + name}
editNode={editNode}
disabled={disabled}
password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""}
options={globalVariablesEntries}
optionsPlaceholder={"Global Variables"}
optionsIcon="Globe"
optionsButton={
<AddNewVariableButton>
<CommandItem value="doNotFilter-addNewVariable">
<ForwardedIconComponent
name="Plus"
className={cn("mr-2 h-4 w-4 text-primary")}
aria-hidden="true"
/>
<span>Add New Variable</span>
</CommandItem>
</AddNewVariableButton>
}
optionButton={(option) => (
<DeleteConfirmationModal
onConfirm={(e) => {
e.stopPropagation();
e.preventDefault();
handleDelete(option);
}}
description={'variable "' + option + '"'}
asChild
>
<button
onClick={(e) => {
e.stopPropagation();
}}
className="pr-1"
>
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-4 w-4 text-primary opacity-0 hover:text-status-red group-hover:opacity-100"
)}
aria-hidden="true"
/>
</button>
</DeleteConfirmationModal>
)}
selectedOption={
data?.node?.template[name].load_from_db ?? false
? data?.node?.template[name].value
: ""
}
setSelectedOption={(value) => {
onChange(value);
setDb(value !== "" ? true : false);
}}
onChange={(value) => {
onChange(value);
setDb(false);
}}
/>
);
}

View file

@ -9,11 +9,12 @@ import { Input } from "../ui/input";
export default function IntComponent({
value,
onChange,
rangeSpec,
disabled,
editNode = false,
id = "",
}: IntComponentType): JSX.Element {
const min = 0;
const min = -Infinity;
// Clear component state
useEffect(() => {
@ -31,8 +32,9 @@ export default function IntComponent({
handleKeyDown(event, value, "");
}}
type="number"
step="1"
min={0}
step={rangeSpec?.step ?? 1}
min={rangeSpec?.min ?? min}
max={rangeSpec?.max ?? undefined}
onInput={(event: React.ChangeEvent<HTMLInputElement>) => {
if (Number(event.target.value) < min) {
event.target.value = min.toString();
@ -45,6 +47,7 @@ export default function IntComponent({
onChange={(event) => {
onChange(event.target.value);
}}
data-testid={id}
/>
</div>
);

View file

@ -12,10 +12,11 @@ export default function KeypairListComponent({
disabled,
editNode = false,
duplicateKey,
isList = true,
}: KeyPairListComponentType): JSX.Element {
useEffect(() => {
if (disabled && value.length > 0 && value[0] !== "") {
onChange([""]);
onChange([{ "": "" }]);
}
}, [disabled]);
@ -79,6 +80,7 @@ export default function KeypairListComponent({
: "keypair" + (index + 100).toString()
}
type="text"
disabled={disabled}
value={obj[key]}
className={editNode ? "input-edit-node" : ""}
placeholder="Type a value..."
@ -87,8 +89,9 @@ export default function KeypairListComponent({
}
/>
{index === ref.current.length - 1 ? (
{isList && index === ref.current.length - 1 ? (
<button
disabled={disabled}
onClick={() => {
let newInputList = _.cloneDeep(ref.current);
newInputList.push({ "": "" });
@ -110,7 +113,7 @@ export default function KeypairListComponent({
className={"h-4 w-4 hover:text-accent-foreground"}
/>
</button>
) : (
) : isList ? (
<button
onClick={() => {
let newInputList = _.cloneDeep(ref.current);
@ -133,6 +136,8 @@ export default function KeypairListComponent({
className="h-4 w-4 hover:text-status-red"
/>
</button>
) : (
""
)}
</div>
);

View file

@ -26,6 +26,7 @@ export default function PromptAreaComponent({
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal
id={id}
field_name={field_name}
readonly={readonly}
type={TypeModal.PROMPT}
value={value}

View file

@ -1,30 +0,0 @@
import React, { ReactNode } from "react";
interface ElementStackProps {
children: ReactNode[];
}
const ElementStack: React.FC<ElementStackProps> = ({ children }) => {
return (
<div
className={`grid grid-cols-1`}
style={{ display: "grid", gridAutoFlow: "row" }}
>
{children.map((child, index) => (
<div
key={index}
style={{
gridColumn: 1,
gridRow: 1,
transform: `translateX(${index * 0.1}rem)`,
zIndex: children.length - index,
}}
>
{child}
</div>
))}
</div>
);
};
export default ElementStack;

View file

@ -1,4 +1,5 @@
import { useEffect } from "react";
import { EDIT_TEXT_MODAL_TITLE } from "../../constants/constants";
import { TypeModal } from "../../constants/enums";
import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components";
@ -20,48 +21,42 @@ export default function TextAreaComponent({
}, [disabled]);
return (
<div
className={
"flex w-full items-center " + (disabled ? "pointer-events-none" : "")
}
>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
setValue={(value: string) => {
onChange(value);
}}
>
<div className="flex w-full items-center" data-testid={"div-" + id}>
<Input
id={id}
data-testid={id}
<div className={"flex w-full items-center " + (disabled ? "" : "")}>
<div className="flex w-full items-center" data-testid={"div-" + id}>
<Input
id={id}
data-testid={id}
value={value}
disabled={disabled}
className={editNode ? "input-edit-node w-full" : " w-full"}
placeholder={"Type something..."}
onChange={(event) => {
onChange(event.target.value);
}}
/>
<div>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finish Editing"
modalTitle={EDIT_TEXT_MODAL_TITLE}
value={value}
disabled={disabled}
className={
editNode
? "input-edit-node pointer-events-none "
: " pointer-events-none"
}
placeholder={"Type something..."}
onChange={(event) => {
onChange(event.target.value);
setValue={(value: string) => {
onChange(value);
}}
/>
{!editNode && (
<IconComponent
id={id}
name="ExternalLink"
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
>
{!editNode && (
<IconComponent
id={id}
name="ExternalLink"
className={
"icons-parameters-comp" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>
)}
</GenericModal>
</div>
</GenericModal>
</div>
</div>
);
}

View file

@ -0,0 +1,15 @@
export default function TextInputComponent({
text,
emissor,
}: {
text: string;
emissor: string;
}) {
return (
<div>
<strong> {emissor}</strong>
<br></br>
<span>{text}</span>
</div>
);
}

View file

@ -0,0 +1,15 @@
export default function TextOutputComponent({
text,
emissor,
}: {
text: string;
emissor: string;
}) {
return (
<div>
<strong>{emissor}</strong>
<br></br>
<div className="w-80 break-all">{text}</div>
</div>
);
}

View file

@ -1,59 +0,0 @@
import { Switch } from "@headlessui/react";
import { useEffect } from "react";
import { ToggleComponentType } from "../../types/components";
import { classNames } from "../../utils/utils";
export default function ToggleComponent({
enabled,
setEnabled,
disabled,
}: ToggleComponentType): JSX.Element {
// set component state as disabled
useEffect(() => {
if (disabled) {
setEnabled(false);
}
}, [disabled, setEnabled]);
return (
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
<Switch
checked={enabled}
onChange={(isEnabled: boolean) => {
setEnabled(isEnabled);
}}
className={classNames(
enabled ? "bg-primary" : "bg-input",
"toggle-component-switch "
)}
>
<span className="sr-only">Use setting</span>
<span
className={classNames(
enabled ? "translate-x-5" : "translate-x-0",
"toggle-component-span",
disabled ? "bg-input " : "bg-background"
)}
>
<span
className={classNames(
enabled
? "opacity-0 duration-100 ease-out"
: "opacity-100 duration-200 ease-in",
"toggle-component-second-span"
)}
aria-hidden="true"
></span>
<span
className={classNames(
enabled
? "opacity-100 duration-200 ease-in"
: "opacity-0 duration-100 ease-out",
"toggle-component-second-span"
)}
aria-hidden="true"
></span>
</span>
</Switch>
</div>
);
}

View file

@ -7,6 +7,7 @@ export default function ToggleShadComponent({
disabled,
size,
id = "",
editNode = false,
}: ToggleComponentType): JSX.Element {
let scaleX, scaleY;
switch (size) {
@ -31,6 +32,7 @@ export default function ToggleShadComponent({
<div className={disabled ? "pointer-events-none cursor-not-allowed " : ""}>
<Switch
id={id}
data-testid={id}
style={{
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
}}

View file

@ -32,7 +32,7 @@ const AccordionTrigger = React.forwardRef<
)}
>
{children}
<ChevronDownIcon className="h-4 w-4 text-muted-foreground transition-transform duration-200" />
<ChevronDownIcon className="h-4 w-4 font-bold text-primary transition-transform duration-200" />
</div>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>

View file

@ -4,7 +4,7 @@ import * as React from "react";
import { cn } from "../../utils/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
{
variants: {
variant: {
@ -12,7 +12,7 @@ const buttonVariants = cva(
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input hover:bg-accent hover:text-accent-foreground",
"border border-input hover:bg-input hover:text-accent-foreground",
primary:
"border bg-background text-secondary-foreground hover:bg-secondary-foreground/5 dark:hover:bg-background/10 hover:shadow-sm",
secondary:
@ -23,8 +23,9 @@ const buttonVariants = cva(
size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-3 rounded-md",
xs: "py-1 px-1 rounded-md",
xs: "py-0.5 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
icon: "py-1 px-1 rounded-md",
},
},
defaultVariants: {
@ -40,13 +41,25 @@ export interface ButtonProps
asChild?: boolean;
}
function toTitleCase(text: string) {
return text
.split(" ")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(" ");
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
({ className, variant, size, asChild = false, children, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
let newChildren = children;
if (typeof children === "string") {
newChildren = toTitleCase(children);
}
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
children={newChildren}
{...props}
/>
);

View file

@ -0,0 +1,31 @@
import { AnimatePresence, motion } from "framer-motion";
export default function Checkmark({ initial = true, isVisible, className }) {
return (
<AnimatePresence initial={initial}>
{isVisible && (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={"CheckIcon " + className}
>
<motion.path
initial={{ pathLength: 0, pathOffset: 1 }}
animate={{ pathLength: 1, pathOffset: 0 }}
exit={{ pathLength: 0, pathOffset: 1 }}
transition={{
type: "tween",
duration: 0.3,
ease: isVisible ? "easeOut" : "easeIn",
}}
strokeLinecap="round"
strokeLinejoin="round"
d="M20 6 9 17l-5-5"
/>
</svg>
)}
</AnimatePresence>
);
}

View file

@ -117,7 +117,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50",
className
)}
{...props}

View file

@ -0,0 +1,119 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
import { cn } from "../../utils/utils";
const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = ({
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal {...props}>
<div className="nopan nodelete nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"nopan nodelete nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 flex w-full max-w-lg flex-col gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;
export {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
};

View file

@ -43,7 +43,7 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full",
"fixed z-50 flex w-full max-w-lg flex-col gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}

View file

@ -27,4 +27,21 @@ const PopoverContent = React.forwardRef<
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverContent, PopoverTrigger };
const PopoverContentWithoutPortal = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
));
PopoverContentWithoutPortal.displayName = PopoverPrimitive.Content.displayName;
export { Popover, PopoverContent, PopoverContentWithoutPortal, PopoverTrigger };

View file

@ -0,0 +1,58 @@
import IconComponent from "../../components/genericIconComponent";
import { NodeDataType } from "../../types/flow";
import { cn } from "../../utils/utils";
import { Button } from "./button";
function RefreshButton({
isLoading,
disabled,
name,
button_text,
data,
handleUpdateValues,
className,
id,
}: {
isLoading: boolean;
disabled: boolean;
name: string;
button_text?: string;
data: NodeDataType;
className?: string;
handleUpdateValues: (name: string, data: NodeDataType) => void;
id: string;
}) {
const handleClick = async () => {
if (disabled) return;
handleUpdateValues(name, data);
};
const classNames = cn(className, disabled ? "cursor-not-allowed" : "");
// icon class name should take into account the disabled state and the loading state
const disabledIconTextClass = disabled ? "text-muted-foreground" : "";
const iconClassName = cn(
"h-4 w-4",
isLoading ? "animate-spin" : "animate-wiggle",
disabledIconTextClass
);
return (
<Button
variant="primary"
disabled={disabled}
className={classNames}
onClick={handleClick}
id={id}
>
{button_text && <span className="mr-1">{button_text}</span>}
<IconComponent
name={isLoading ? "Loader2" : "RefreshCcw"}
className={iconClassName}
id={id + "-icon"}
/>
</Button>
);
}
export { RefreshButton };

View file

@ -28,6 +28,9 @@ export default function RenameLabel(props) {
}
}
resizeInput();
return () => {
if (isRename) document.removeEventListener("keydown", () => {});
};
}, [isRename]);
const inputRef = useRef<HTMLInputElement | null>(null);

View file

@ -33,7 +33,7 @@ const SelectContent = React.forwardRef<
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"relative z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className

View file

@ -0,0 +1,45 @@
import { AnimatePresence, motion } from "framer-motion";
export default function Xmark({ initial = true, isVisible, className }) {
return (
<AnimatePresence initial={initial}>
{isVisible && (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={"CheckIcon " + className}
>
<motion.path
initial={{ pathLength: 0, pathOffset: 1, strokeLinecap: "butt" }}
animate={{ pathLength: 1, pathOffset: 0, strokeLinecap: "round" }}
exit={{ pathLength: 0, pathOffset: 1, strokeLinecap: "butt" }}
transition={{
type: "tween",
duration: 0.3,
ease: isVisible ? "easeOut" : "easeIn",
delay: 0.2,
}}
strokeLinecap="round"
strokeLinejoin="round"
d="M18 6 6 18"
/>
<motion.path
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
exit={{ pathLength: 0 }}
transition={{
type: "tween",
duration: 0.3,
ease: isVisible ? "easeOut" : "easeIn",
}}
strokeLinecap="round"
strokeLinejoin="round"
d="m6 6 12 12"
/>
</svg>
)}
</AnimatePresence>
);
}

View file

@ -0,0 +1,60 @@
// ERROR
export const MISSED_ERROR_ALERT = "Oops! Looks like you missed something";
export const INVALID_FILE_ALERT =
"Please select a valid file. Only these file types are allowed:";
export const CONSOLE_ERROR_MSG = "Error occurred while uploading file";
export const CONSOLE_SUCCESS_MSG = "File uploaded successfully";
export const INFO_MISSING_ALERT =
"Oops! Looks like you missed some required information:";
export const FUNC_ERROR_ALERT = "There is an error in your function";
export const IMPORT_ERROR_ALERT = "There is an error in your imports";
export const BUG_ALERT = "Something went wrong, please try again";
export const CODE_ERROR_ALERT =
"There is something wrong with this code, please review it";
export const CHAT_ERROR_ALERT =
"Please build the flow again before using the chat.";
export const MSG_ERROR_ALERT = "There was an error sending the message";
export const PROMPT_ERROR_ALERT =
"There is something wrong with this prompt, please review it";
export const API_ERROR_ALERT =
"There was an error saving the API Key, please try again.";
export const USER_DEL_ERROR_ALERT = "Error on delete user";
export const USER_EDIT_ERROR_ALERT = "Error on edit user";
export const USER_ADD_ERROR_ALERT = "Error when adding new user";
export const SIGNIN_ERROR_ALERT = "Error signing in";
export const DEL_KEY_ERROR_ALERT = "Error on delete key";
export const UPLOAD_ERROR_ALERT = "Error uploading file";
export const WRONG_FILE_ERROR_ALERT = "Invalid file type";
export const UPLOAD_ALERT_LIST = "Please upload a JSON file";
export const INVALID_SELECTION_ERROR_ALERT = "Invalid selection";
export const EDIT_PASSWORD_ERROR_ALERT = "Error changing password";
export const EDIT_PASSWORD_ALERT_LIST = "Passwords do not match";
export const SAVE_ERROR_ALERT = "Error saving changes";
export const SIGNUP_ERROR_ALERT = "Error signing up";
export const APIKEY_ERROR_ALERT = "API Key Error";
export const NOAPI_ERROR_ALERT =
"You don't have an API Key. Please add one to use the Langflow Store.";
export const INVALID_API_ERROR_ALERT =
"Your API Key is not valid. Please add a valid API Key to use the Langflow Store.";
export const COMPONENTS_ERROR_ALERT = "Error getting components.";
// NOTICE
export const NOCHATOUTPUT_NOTICE_ALERT =
"There is no ChatOutput Component in the flow.";
export const API_WARNING_NOTICE_ALERT =
"Warning: Critical data, JSON file may include API keys.";
export const COPIED_NOTICE_ALERT = "API Key copied!";
export const TEMP_NOTICE_ALERT = "Your template does not have any variables.";
// SUCCESS
export const CODE_SUCCESS_ALERT = "Code is ready to run";
export const PROMPT_SUCCESS_ALERT = "Prompt is ready";
export const API_SUCCESS_ALERT = "Success! Your API Key has been saved.";
export const USER_DEL_SUCCESS_ALERT = "Success! User deleted!";
export const USER_EDIT_SUCCESS_ALERT = "Success! User edited!";
export const USER_ADD_SUCCESS_ALERT = "Success! New user added!";
export const DEL_KEY_SUCCESS_ALERT = "Success! Key deleted!";
export const FLOW_BUILD_SUCCESS_ALERT = `Flow built successfully`;
export const SAVE_SUCCESS_ALERT = "Changes saved successfully!";
// Generic Node

View file

@ -25,10 +25,18 @@ export const INVALID_CHARACTERS = [
/**
* regex to highlight the variables in the text
* @constant
* @constant regexHighlight
* @type {RegExp}
* @default
* @example
* {{variable}} or {variable}
* @returns {RegExp}
* @description
* This regex is used to highlight the variables in the text.
* It matches the variables in the text that are between {{}} or {}.
*/
export const regexHighlight = /\{([^}]+)\}/g;
export const regexHighlight = /\{\{(.*?)\}\}|\{([^{}]+)\}/g;
export const specialCharsRegex = /[!@#$%^&*()\-_=+[\]{}|;:'",.<>/?\\`´]/;
export const programmingLanguages: languageMap = {
@ -519,6 +527,8 @@ export const ADMIN_HEADER_DESCRIPTION =
export const BASE_URL_API = "/api/v1/";
export const BACKEND_URL = "http://localhost:7860/";
/**
* URLs excluded from error retries.
* @constant
@ -677,3 +687,70 @@ export const LANGFLOW_SUPPORTED_TYPES = new Set([
]);
export const priorityFields = new Set(["code", "template"]);
export const INPUT_TYPES = new Set(["ChatInput", "TextInput"]);
export const OUTPUT_TYPES = new Set(["ChatOutput", "TextOutput"]);
export const CHAT_FIRST_INITIAL_TEXT =
"Start a conversation and click the agent's thoughts";
export const CHAT_SECOND_INITIAL_TEXT = "to inspect the chaining process.";
export const ZERO_NOTIFICATIONS = "No new notifications";
export const SUCCESS_BUILD = "Built sucessfully ✨";
export const ALERT_SAVE_WITH_API =
"Caution: Uncheck this box only removes API keys from fields specifically designated for API keys.";
export const SAVE_WITH_API_CHECKBOX = "Save with my API keys";
export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
export const EDIT_TEXT_PLACEHOLDER = "Type message here.";
export const INPUT_HANDLER_HOVER = "Avaliable input components:";
export const OUTPUT_HANDLER_HOVER = "Avaliable output components:";
export const TEXT_INPUT_MODAL_TITLE = "Text Inputs";
export const OUTPUTS_MODAL_TITLE = "Text Outputs";
export const LANGFLOW_CHAT_TITLE = "Langflow Chat";
export const CHAT_INPUT_PLACEHOLDER =
"No chat input variables found. Click to run your flow.";
export const CHAT_INPUT_PLACEHOLDER_SEND = "Send a message...";
export const EDIT_CODE_TITLE = "Edit Code";
export const MY_COLLECTION_DESC =
"Manage your personal projects. Download and upload entire collections.";
export const STORE_DESC = "Explore community-shared flows and components.";
export const STORE_TITLE = "Langflow Store";
export const NO_API_KEY = "You don't have an API key. ";
export const INSERT_API_KEY = "Insert your Langflow API key.";
export const INVALID_API_KEY = "Your API key is not valid. ";
export const CREATE_API_KEY = `Don’t have an API key? Sign up at`;
export const STATUS_BUILD = "Build to validate status.";
export const STATUS_BUILDING = "Building...";
export const SAVED_HOVER = "Last saved at ";
export const RUN_TIMESTAMP_PREFIX = "Last Run: ";
export const STARTER_FOLDER_NAME = "Starter Projects";
export const PRIORITY_SIDEBAR_ORDER = [
"saved_components",
"inputs",
"outputs",
"prompts",
"data",
"prompt",
"models",
"helpers",
"vectorstores",
"vectorsearch",
"embeddings",
];
export const NATIVE_CATEGORIES = [
"inputs",
"outputs",
"prompts",
"data",
"prompt",
"models",
"helpers",
"experimental",
"agents",
];
export const SAVE_DEBOUNCE_TIME = 300;

View file

@ -6,3 +6,16 @@ export enum TypeModal {
TEXT = 1,
PROMPT = 2,
}
export enum BuildStatus {
BUILDING = "BUILDING",
TO_BUILD = "TO_BUILD",
BUILT = "BUILT",
INACTIVE = "INACTIVE",
ERROR = "ERROR",
}
export enum InputOutput {
INPUT = "input",
OUTPUT = "output",
}

View file

@ -83,7 +83,7 @@ export function AuthProvider({ children }): React.ReactElement {
useFlowsManagerStore.setState({ isLoading: false });
}
});
}, [setUserData, setLoading, autoLogin, setIsAdmin]);
}, [autoLogin]);
function getUser() {
getLoggedUser()
@ -94,7 +94,6 @@ export function AuthProvider({ children }): React.ReactElement {
setIsAdmin(isSuperUser);
})
.catch((error) => {
console.log("auth context");
setLoading(false);
});
}

View file

@ -1,10 +1,11 @@
import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { useNavigate } from "react-router-dom";
import { renewAccessToken } from ".";
import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
// Create a new Axios instance
const api: AxiosInstance = axios.create({
@ -15,54 +16,31 @@ function ApiInterceptor() {
const setErrorData = useAlertStore((state) => state.setErrorData);
let { accessToken, login, logout, authenticationErrorCount, autoLogin } =
useContext(AuthContext);
const navigate = useNavigate();
const cookies = new Cookies();
useEffect(() => {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 401) {
const accessToken = cookies.get("access_token_lf");
if (accessToken && !autoLogin) {
authenticationErrorCount = authenticationErrorCount + 1;
if (authenticationErrorCount > 3) {
authenticationErrorCount = 0;
logout();
if (error.response?.status === 403 || error.response?.status === 401) {
if (!autoLogin) {
const stillRefresh = checkErrorCount();
if (!stillRefresh) {
return Promise.reject(error);
}
try {
const res = await renewAccessToken();
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token);
}
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token_lf"
)}`;
const response = await axios.request(error.config);
return response;
}
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 401) {
logout();
} else {
console.error(error);
logout();
}
}
}
const acceptedRequest = await tryToRenewAccessToken(error);
if (!accessToken && error?.config?.url?.includes("login")) {
return Promise.reject(error);
} else {
logout();
const accessToken = cookies.get("access_token_lf");
if (!accessToken && error?.config?.url?.includes("login")) {
return Promise.reject(error);
}
return acceptedRequest;
}
} else {
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
return Promise.reject(error);
// }
}
await clearBuildVerticesState(error);
return Promise.reject(error);
}
);
@ -115,6 +93,50 @@ function ApiInterceptor() {
};
}, [accessToken, setErrorData]);
function checkErrorCount() {
authenticationErrorCount = authenticationErrorCount + 1;
if (authenticationErrorCount > 3) {
authenticationErrorCount = 0;
logout();
return false;
}
return true;
}
async function tryToRenewAccessToken(error: AxiosError) {
try {
if (window.location.pathname.includes("/login")) return;
const res = await renewAccessToken();
if (res?.data?.access_token && res?.data?.refresh_token) {
login(res?.data?.access_token);
}
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token_lf"
)}`;
const response = await axios.request(error.config);
return response;
}
} catch (error) {
clearBuildVerticesState(error);
logout();
return Promise.reject("Authentication error");
}
}
async function clearBuildVerticesState(error) {
if (error?.response?.status === 500) {
const vertices = useFlowStore.getState().verticesBuild;
useFlowStore
.getState()
.updateBuildStatus(vertices?.verticesIds ?? [], BuildStatus.BUILT);
useFlowStore.getState().setIsBuilding(false);
}
}
return null;
}

View file

@ -4,9 +4,12 @@ import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import {
APIObjectType,
APITemplateType,
Component,
LoginType,
Users,
VertexBuildTypeAPI,
VerticesOrderTypeAPI,
changeUser,
resetPasswordType,
sendAllProps,
@ -14,6 +17,7 @@ import {
import { UserInputType } from "../../types/components";
import { FlowStyleType, FlowType } from "../../types/flow";
import { StoreComponentResponse } from "../../types/store";
import { FlowPoolType } from "../../types/zustand/flow";
import {
APIClassType,
BuildStatusTypeAPI,
@ -350,7 +354,7 @@ export async function uploadFile(
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
return await api.post(`${BASE_URL_API}upload/${id}`, formData);
return await api.post(`${BASE_URL_API}files/upload/${id}`, formData);
}
export async function postCustomComponent(
@ -366,11 +370,15 @@ export async function postCustomComponent(
export async function postCustomComponentUpdate(
code: string,
field: string
template: APITemplateType,
field: string,
field_value: any
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
template,
field,
field_value,
});
}
@ -850,3 +858,102 @@ export async function requestLogout() {
throw error;
}
}
export async function getGlobalVariables(): Promise<{
[key: string]: { id: string; type: string };
}> {
const globalVariables = {};
(await api.get(`${BASE_URL_API}variables/`)).data.forEach((element) => {
globalVariables[element.name] = {
id: element.id,
type: element.type,
};
});
return globalVariables;
}
export async function registerGlobalVariable({
name,
value,
type,
}: {
name: string;
value: string;
type?: string;
}): Promise<AxiosResponse<{ name: string; id: string; type: string }>> {
return await api.post(`${BASE_URL_API}variables/`, {
name,
value,
type,
});
}
export async function deleteGlobalVariable(id: string) {
api.delete(`${BASE_URL_API}variables/${id}`);
}
export async function updateGlobalVariable(
name: string,
value: string,
id: string
) {
api.patch(`${BASE_URL_API}variables/${id}`, {
name,
value,
});
}
export async function getVerticesOrder(
flowId: string,
startNodeId?: string | null,
stopNodeId?: string | null
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
const config = {};
if (stopNodeId) {
config["params"] = { stop_component_id: stopNodeId };
} else if (startNodeId) {
config["params"] = { start_component_id: startNodeId };
}
return await api.get(`${BASE_URL_API}build/${flowId}/vertices`, config);
}
export async function postBuildVertex(
flowId: string,
vertexId: string,
input_value: string
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
input_value ? { inputs: { input_value: input_value } } : undefined
);
}
export async function downloadImage({ flowId, fileName }): Promise<any> {
return await api.get(`${BASE_URL_API}files/images/${flowId}/${fileName}`);
}
export async function getFlowPool({
flowId,
nodeId,
}: {
flowId: string;
nodeId?: string;
}): Promise<AxiosResponse<{ vertex_builds: FlowPoolType }>> {
const config = {};
config["params"] = { flow_id: flowId };
if (nodeId) {
config["params"] = { nodeId };
}
return await api.get(`${BASE_URL_API}monitor/builds`, config);
}
export async function deleteFlowPool(
flowId: string
): Promise<AxiosResponse<any>> {
const config = {};
config["params"] = { flow_id: flowId };
return await api.delete(`${BASE_URL_API}monitor/builds`, config);
}

View file

@ -0,0 +1,28 @@
const AstraSVG = (props) => (
<svg
width="96"
height="96"
viewBox="12 33 72 29"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clip-path="url(#clip0_702_1449)">
{/* <rect width="96" height="96" rx="6" fill="white"/> */}
<path
d="M38.0469 33H12V62.1892H38.0469L44.5902 57.1406V38.0485L38.0469 33ZM17.0478 38.0485H39.5424V57.1459H17.0478V38.0485Z"
fill="black"
/>
<path
d="M82.0705 38.2605V33.3243H58.2546L51.788 38.2605V45.038L58.2546 49.9742H79.0107V56.9286H53.076V61.8648H77.5334L84 56.9286V49.9742L77.5334 45.038H56.7772V38.2605H82.0705Z"
fill="black"
/>
</g>
<defs>
<clipPath id="clip0_702_1449">
<rect width="96" height="96" fill="white" />
</clipPath>
</defs>
</svg>
);
export default AstraSVG;

View file

@ -0,0 +1,12 @@
<svg width="96" height="96" viewBox="0 0 96 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_702_1449)">
<rect width="96" height="96" rx="6" fill="white"/>
<path d="M38.0469 33H12V62.1892H38.0469L44.5902 57.1406V38.0485L38.0469 33ZM17.0478 38.0485H39.5424V57.1459H17.0478V38.0485Z" fill="black"/>
<path d="M82.0705 38.2605V33.3243H58.2546L51.788 38.2605V45.038L58.2546 49.9742H79.0107V56.9286H53.076V61.8648H77.5334L84 56.9286V49.9742L77.5334 45.038H56.7772V38.2605H82.0705Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_702_1449">
<rect width="96" height="96" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 645 B

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import AstraSVG from "./AstraDB";
export const AstraDBIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <AstraSVG ref={ref} {...props} />;
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,8 @@
import React, { forwardRef } from "react";
import SvgAzure from "./Azure";
export const AzureIcon = forwardRef<SVGSVGElement, React.PropsWithChildren<{}>>(
(props, ref) => {
return <SvgAzure ref={ref} {...props} />;
}
);

View file

@ -0,0 +1,23 @@
const SvgBotMessageSquare = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
className="lucide lucide-bot-message-square"
{...props}
>
<path d="M12 6V2H8" />
<path d="m8 18-4 4V8a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2Z" />
<path d="M2 12h2" />
<path d="M9 11v2" />
<path d="M15 11v2" />
<path d="M20 12h2" />
</svg>
);
export default SvgBotMessageSquare;

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgChroma from "./BotMessageSquare";
export const BotMessageSquareIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgChroma ref={ref} {...props} />;
});

View file

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M16 8.016A8.522 8.522 0 008.016 16h-.032A8.521 8.521 0 000 8.016v-.032A8.521 8.521 0 007.984 0h.032A8.522 8.522 0 0016 7.984v.032z" fill="url(#prefix__paint0_radial_980_20147)"/><defs><radialGradient id="prefix__paint0_radial_980_20147" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="matrix(16.1326 5.4553 -43.70045 129.2322 1.588 6.503)"><stop offset=".067" stop-color="#9168C0"/><stop offset=".343" stop-color="#5684D1"/><stop offset=".672" stop-color="#1BA1E3"/></radialGradient></defs></svg>

After

Width:  |  Height:  |  Size: 599 B

View file

@ -0,0 +1,28 @@
const SvgGoogleGenerativeAI = (props) => (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
{...props}
>
<path
d="M16 8.016A8.522 8.522 0 008.016 16h-.032A8.521 8.521 0 000 8.016v-.032A8.521 8.521 0 007.984 0h.032A8.522 8.522 0 0016 7.984v.032z"
fill="url(#prefix__paint0_radial_980_20147)"
/>
<defs>
<radialGradient
id="prefix__paint0_radial_980_20147"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(16.1326 5.4553 -43.70045 129.2322 1.588 6.503)"
>
<stop offset=".067" stop-color="#9168C0" />
<stop offset=".343" stop-color="#5684D1" />
<stop offset=".672" stop-color="#1BA1E3" />
</radialGradient>
</defs>
</svg>
);
export default SvgGoogleGenerativeAI;

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgGoogleGenerativeAI from "./GoogleGemini";
export const GoogleGenerativeAIIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgGoogleGenerativeAI ref={ref} {...props} />;
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 122 KiB

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgOllama from "./Ollama";
export const OllamaIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgOllama ref={ref} {...props} />;
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 273 KiB

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgPostgres from "./Postgres";
export const PostgresIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgPostgres ref={ref} {...props} />;
});

View file

@ -0,0 +1,158 @@
export const SvgPython = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={1000}
height={1000}
viewBox="0 0 750 750"
{...props}
>
<defs>
<clipPath id="a">
<path d="M45 42h493v494H45Zm0 0" />
</clipPath>
<clipPath id="b">
<path d="M375.207 42.145c-168.414 0-157.902 73.035-157.902 73.035l.207 75.664h160.703v22.699H153.64S45.906 201.313 45.906 371.262c0 169.906 94.055 163.894 94.055 163.894h56.137v-78.86s-3.028-94.054 92.562-94.054h159.375s89.555 1.453 89.555-86.547V130.188s13.601-88.043-162.383-88.043Zm-88.621 50.87c15.984 0 28.918 12.938 28.918 28.922 0 15.981-12.934 28.918-28.918 28.918-.95 0-1.895-.042-2.836-.136a28.448 28.448 0 0 1-5.563-1.098c-.906-.277-1.8-.594-2.675-.957-.875-.36-1.73-.766-2.567-1.21a29.522 29.522 0 0 1-2.437-1.458 29.114 29.114 0 0 1-6.29-5.703 28.46 28.46 0 0 1-1.69-2.281 27.818 27.818 0 0 1-1.458-2.434 29.596 29.596 0 0 1-1.215-2.566 28.942 28.942 0 0 1-2.055-8.238c-.09-.946-.136-1.891-.132-2.837 0-15.984 12.934-28.921 28.918-28.921Zm0 0" />
</clipPath>
<clipPath id="d">
<path d="M217 209h493v494H217Zm0 0" />
</clipPath>
<clipPath id="e">
<path d="M379.977 702.059c168.414 0 157.902-73.036 157.902-73.036l-.207-75.664h-160.7V530.66h224.551s107.758 12.23 107.758-157.7c0-169.925-94.054-163.894-94.054-163.894H559.09v78.84s3.023 94.051-92.563 94.051H307.152s-89.558-1.45-89.558 86.55v145.509s-13.598 88.043 162.383 88.043Zm88.62-50.875a28.573 28.573 0 0 1-5.644-.547 29.24 29.24 0 0 1-2.754-.688 28.56 28.56 0 0 1-5.242-2.168 29.024 29.024 0 0 1-4.715-3.148 29.42 29.42 0 0 1-2.105-1.906 29.448 29.448 0 0 1-1.907-2.102 29.882 29.882 0 0 1-1.687-2.281 28.516 28.516 0 0 1-2.672-5.004 27.907 27.907 0 0 1-.953-2.676 27.808 27.808 0 0 1-.688-2.754 28.29 28.29 0 0 1-.414-2.808 28.598 28.598 0 0 1-.136-2.836c0-15.961 12.937-28.899 28.918-28.899 15.984 0 28.922 12.914 28.922 28.899 0 16.004-12.938 28.918-28.922 28.918Zm0 0" />
</clipPath>
<linearGradient
id="c"
x1={16.152}
x2={40.334}
y1={15.849}
y2={40.112}
gradientTransform="matrix(20.73047 0 0 20.73046 -285.782 -289.543)"
gradientUnits="userSpaceOnUse"
>
<stop offset={0} stopColor="#387EB8" />
<stop offset={0.125} stopColor="#387EB8" />
<stop offset={0.141} stopColor="#387EB7" />
<stop offset={0.156} stopColor="#387DB7" />
<stop offset={0.172} stopColor="#387DB6" />
<stop offset={0.188} stopColor="#387CB5" />
<stop offset={0.203} stopColor="#387CB4" />
<stop offset={0.219} stopColor="#387BB3" />
<stop offset={0.234} stopColor="#387BB2" />
<stop offset={0.25} stopColor="#387AB1" />
<stop offset={0.266} stopColor="#387AB1" />
<stop offset={0.281} stopColor="#3879B0" />
<stop offset={0.297} stopColor="#3879AF" />
<stop offset={0.313} stopColor="#3878AE" />
<stop offset={0.328} stopColor="#3778AD" />
<stop offset={0.344} stopColor="#3777AC" />
<stop offset={0.359} stopColor="#3777AB" />
<stop offset={0.375} stopColor="#3776AA" />
<stop offset={0.391} stopColor="#3776AA" />
<stop offset={0.406} stopColor="#3775A9" />
<stop offset={0.422} stopColor="#3775A8" />
<stop offset={0.438} stopColor="#3774A7" />
<stop offset={0.453} stopColor="#3774A6" />
<stop offset={0.469} stopColor="#3773A5" />
<stop offset={0.484} stopColor="#3773A4" />
<stop offset={0.498} stopColor="#3772A4" />
<stop offset={0.5} stopColor="#3772A3" />
<stop offset={0.502} stopColor="#3772A3" />
<stop offset={0.516} stopColor="#3771A2" />
<stop offset={0.531} stopColor="#3771A2" />
<stop offset={0.547} stopColor="#3771A1" />
<stop offset={0.563} stopColor="#3770A0" />
<stop offset={0.578} stopColor="#37709F" />
<stop offset={0.594} stopColor="#376F9E" />
<stop offset={0.609} stopColor="#376F9E" />
<stop offset={0.625} stopColor="#376E9D" />
<stop offset={0.641} stopColor="#376E9C" />
<stop offset={0.656} stopColor="#366D9B" />
<stop offset={0.672} stopColor="#366D9A" />
<stop offset={0.688} stopColor="#366C99" />
<stop offset={0.703} stopColor="#366C98" />
<stop offset={0.719} stopColor="#366B97" />
<stop offset={0.734} stopColor="#366B97" />
<stop offset={0.75} stopColor="#366A96" />
<stop offset={0.766} stopColor="#366A95" />
<stop offset={0.781} stopColor="#366994" />
<stop offset={0.813} stopColor="#366994" />
<stop offset={0.875} stopColor="#366994" />
<stop offset={1} stopColor="#366994" />
</linearGradient>
<linearGradient
id="f"
x1={23.548}
x2={48.282}
y1={23.842}
y2={47.538}
gradientTransform="matrix(20.73047 0 0 20.73046 -285.782 -289.543)"
gradientUnits="userSpaceOnUse"
>
<stop offset={0} stopColor="#FFE052" />
<stop offset={0.125} stopColor="#FFE052" />
<stop offset={0.188} stopColor="#FFE052" />
<stop offset={0.219} stopColor="#FFE052" />
<stop offset={0.234} stopColor="#FFDF51" />
<stop offset={0.25} stopColor="#FFDF51" />
<stop offset={0.266} stopColor="#FFDE50" />
<stop offset={0.281} stopColor="#FFDD4F" />
<stop offset={0.297} stopColor="#FFDD4E" />
<stop offset={0.313} stopColor="#FFDC4E" />
<stop offset={0.328} stopColor="#FFDB4D" />
<stop offset={0.344} stopColor="#FFDB4C" />
<stop offset={0.359} stopColor="#FFDA4B" />
<stop offset={0.375} stopColor="#FFD94B" />
<stop offset={0.391} stopColor="#FFD94A" />
<stop offset={0.406} stopColor="#FFD849" />
<stop offset={0.422} stopColor="#FFD748" />
<stop offset={0.438} stopColor="#FFD748" />
<stop offset={0.453} stopColor="#FFD647" />
<stop offset={0.469} stopColor="#FFD546" />
<stop offset={0.484} stopColor="#FFD545" />
<stop offset={0.486} stopColor="#FFD445" />
<stop offset={0.5} stopColor="#FFD444" />
<stop offset={0.514} stopColor="#FFD444" />
<stop offset={0.516} stopColor="#FFD343" />
<stop offset={0.531} stopColor="#FFD343" />
<stop offset={0.547} stopColor="#FFD242" />
<stop offset={0.563} stopColor="#FFD242" />
<stop offset={0.578} stopColor="#FFD141" />
<stop offset={0.594} stopColor="#FFD040" />
<stop offset={0.609} stopColor="#FFD03F" />
<stop offset={0.625} stopColor="#FFCF3F" />
<stop offset={0.641} stopColor="#FFCE3E" />
<stop offset={0.656} stopColor="#FFCE3D" />
<stop offset={0.672} stopColor="#FFCD3C" />
<stop offset={0.688} stopColor="#FFCC3C" />
<stop offset={0.703} stopColor="#FFCC3B" />
<stop offset={0.719} stopColor="#FFCB3A" />
<stop offset={0.734} stopColor="#FFCA39" />
<stop offset={0.75} stopColor="#FFCA39" />
<stop offset={0.766} stopColor="#FFC938" />
<stop offset={0.781} stopColor="#FFC837" />
<stop offset={0.797} stopColor="#FFC836" />
<stop offset={0.813} stopColor="#FFC736" />
<stop offset={0.828} stopColor="#FFC635" />
<stop offset={0.844} stopColor="#FFC634" />
<stop offset={0.859} stopColor="#FFC533" />
<stop offset={0.875} stopColor="#FFC433" />
<stop offset={0.891} stopColor="#FFC432" />
<stop offset={0.906} stopColor="#FFC331" />
<stop offset={0.938} stopColor="#FFC331" />
<stop offset={1} stopColor="#FFC331" />
</linearGradient>
</defs>
<path fill="#fff" d="M-75-75h900v900H-75z" />
<path fill="#fff" d="M-75-75h900v900H-75z" />
<g clipPath="url(#a)">
<g clipPath="url(#b)">
<path fill="url(#c)" d="M45.906 42.145v499.023h505.285V42.145Zm0 0" />
</g>
</g>
<g clipPath="url(#d)">
<g clipPath="url(#e)">
<path fill="url(#f)" d="M203.996 203.035V702.06h505.285V203.035Zm0 0" />
</g>
</g>
</svg>
);
export default SvgPython;

View file

@ -0,0 +1,41 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width={800}
height={800}
fill="none"
viewBox="0 0 64 64"
{...props}
>
<path
fill="url(#a)"
d="M31.885 16c-8.124 0-7.617 3.523-7.617 3.523l.01 3.65h7.752v1.095H21.197S16 23.678 16 31.876c0 8.196 4.537 7.906 4.537 7.906h2.708v-3.804s-.146-4.537 4.465-4.537h7.688s4.32.07 4.32-4.175v-7.019S40.374 16 31.885 16zm-4.275 2.454a1.394 1.394 0 1 1 0 2.79 1.393 1.393 0 0 1-1.395-1.395c0-.771.624-1.395 1.395-1.395z"
/>
<path
fill="url(#b)"
d="M32.115 47.833c8.124 0 7.617-3.523 7.617-3.523l-.01-3.65H31.97v-1.095h10.832S48 40.155 48 31.958c0-8.197-4.537-7.906-4.537-7.906h-2.708v3.803s.146 4.537-4.465 4.537h-7.688s-4.32-.07-4.32 4.175v7.019s-.656 4.247 7.833 4.247zm4.275-2.454a1.393 1.393 0 0 1-1.395-1.395 1.394 1.394 0 1 1 1.395 1.395z"
/>
<defs>
<linearGradient
id="a"
x1={19.075}
x2={34.898}
y1={18.782}
y2={34.658}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#387EB8" />
<stop offset={1} stopColor="#366994" />
</linearGradient>
<linearGradient
id="b"
x1={28.809}
x2={45.803}
y1={28.882}
y2={45.163}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#FFE052" />
<stop offset={1} stopColor="#FFC331" />
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,9 @@
import React, { forwardRef } from "react";
import SvgPython from "./Python";
export const PythonIcon = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return <SvgPython ref={ref} {...props} />;
});

View file

@ -0,0 +1,23 @@
export const SvgQianFanChat = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={1000}
height={1000}
viewBox="0 0 750 750"
{...props}
>
<path
fill="#00d263"
d="M602.984 155.953c.024-.02.059-.043.086-.07L379.008 26.43l-37.793 21.71a86.37 86.37 0 0 0-5.809 3.028L95.051 189.543l106.808 61.68a26.254 26.254 0 0 0 26.223.015l53.691-30.91 69.95-40.273a53.06 53.06 0 0 1 52.933 0l55.02 31.675 67.738 38.997a26.257 26.257 0 0 0 26.184.011l107.093-61.566Zm0 0"
/>
<path
fill="#ff502e"
d="M586.313 303.273a26.254 26.254 0 0 0-13.145 22.747V467.78c0 18.887-10.11 36.336-26.5 45.778l-125.336 72.16a26.24 26.24 0 0 0-13.156 22.75v123.054l242.152-139.41c.34-.191.649-.406.98-.605l41.848-24.098V241.754Zm0 0"
/>
<path
fill="#0062ff"
d="m331.3 585.71-121.59-70c-16.398-9.44-26.5-26.89-26.5-45.776V328.09a26.253 26.253 0 0 0-13.151-22.746L63.266 243.848l-.047-.203v.175l.004 276.547v48.934l37.5 21.629a84.188 84.188 0 0 0 5.25 3.304l238.476 137.297v-123.07a26.25 26.25 0 0 0-13.148-22.75"
/>
</svg>
);
export default SvgQianFanChat;

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