Merge remote-tracking branch 'origin/dev' into two_edges

This commit is contained in:
ogabrielluiz 2024-05-28 23:03:38 -03:00
commit 79e35dc2a2
1180 changed files with 74386 additions and 33420 deletions

View file

@ -0,0 +1,47 @@
{
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:prettier/recommended"
],
"plugins": ["react", "import-helpers", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.node.json", "./tsconfig.json"],
"extraFileExtensions:": [".mdx"],
"extensions:": [".mdx"]
},
"env": {
"browser": true,
"es2021": true
},
"settings": {
"react": {
"version": "detect"
}
},
"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",
"react/react-in-jsx-scope": 0,
"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,11 @@ yarn-error.log*
/test-results/
/playwright-report/*/
/playwright/.cache/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View file

@ -0,0 +1 @@
build/*

View file

@ -1,5 +1,5 @@
#baseline
FROM --platform=linux/amd64 node:19-bullseye-slim AS base
FROM --platform=linux/amd64 node:21-bookworm-slim AS base
RUN mkdir -p /home/node/app
RUN chown -R node:node /home/node && chmod -R 770 /home/node
RUN apt-get update && apt-get install -y jq curl

View file

@ -1,5 +1,5 @@
#baseline
FROM node:19-bullseye-slim AS base
FROM node:21-bookworm-slim AS base
RUN mkdir -p /home/node/app
RUN chown -R node:node /home/node && chmod -R 770 /home/node
RUN apt-get update && apt-get install -y jq

File diff suppressed because one or more lines are too long

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" class="dark" 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,9 @@
"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",
"@hookform/resolvers": "^3.3.4",
"@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 +16,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,32 +26,38 @@
"@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",
"ag-grid-community": "^31.2.1",
"ag-grid-react": "^31.2.1",
"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",
"dotenv": "^16.4.5",
"esbuild": "^0.17.19",
"file-saver": "^2.0.5",
"framer-motion": "^11.0.6",
"lodash": "^4.17.21",
"lucide-react": "^0.331.0",
"million": "^3.0.6",
"moment": "^2.29.4",
"react": "^18.2.0",
"openseadragon": "^4.1.1",
"playwright": "^1.42.0",
"react": "^18.2.21",
"react-ace": "^10.1.0",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.0",
"react-dom": "^18.2.21",
"react-error-boundary": "^4.0.11",
"react-icons": "^4.10.1",
"react-hook-form": "^7.51.4",
"react-icons": "^5.0.1",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
"react-pdf": "^7.7.1",
"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",
@ -62,13 +65,12 @@
"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",
"vite-plugin-svgr": "^3.2.0",
"web-vitals": "^2.1.4",
"zod": "^3.23.7",
"zustand": "^4.4.7"
},
"scripts": {
@ -76,9 +78,12 @@
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"format": "npx prettier --write \"./**/*.{js,jsx,ts,tsx,json,md}\"",
"format": "npx prettier --write \"{docs,src}/**/*.{js,jsx,ts,tsx,json,md}\" --ignore-path .prettierignore",
"type-check": "tsc --noEmit --pretty --project tsconfig.json && vite"
},
"simple-git-hooks": {
"pre-commit": "npx pretty-quick --staged"
},
"eslintConfig": {
"extends": [
"react-app",
@ -99,7 +104,7 @@
},
"proxy": "http://127.0.0.1:7860",
"devDependencies": {
"@playwright/test": "^1.41.2",
"@playwright/test": "^1.44.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9",
@ -115,13 +120,18 @@
"@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",
"simple-git-hooks": "^2.11.1",
"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

@ -1,14 +1,17 @@
import { defineConfig, devices } from "@playwright/test";
import * as dotenv from "dotenv";
import path from "path";
dotenv.config();
dotenv.config({ path: path.resolve(__dirname, "../../.env") });
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
@ -18,69 +21,70 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 2 : undefined,
workers: 1,
/* 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: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
use: {
...devices["Desktop Chrome"],
contextOptions: {
// chromium-specific permissions
permissions: ["clipboard-read", "clipboard-write"],
},
},
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
use: {
...devices["Desktop Firefox"],
launchOptions: {
firefoxUserPrefs: {
"dom.events.asyncClipboard.readText": true,
"dom.events.testing.asyncClipboard": true,
},
},
},
},
// {
// 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' },
// },
],
webServer: [
{
command:
"poetry run uvicorn --factory langflow.main:create_app --host 127.0.0.1 --port 7860 --loop asyncio",
port: 7860,
env: {
LANGFLOW_DATABASE_URL: "sqlite:///./temp",
LANGFLOW_AUTO_LOGIN: "true",
},
stdout: "ignore",
/* 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,
// },
// ],
reuseExistingServer: true,
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

@ -11,6 +11,13 @@ body {
text-align: center;
}
.label {
user-select: none;
-webkit-user-select: none; /* Safari */
-moz-user-select: none; /* Firefox */
-ms-user-select: none;
}
.react-flow__node {
width: auto;
height: auto;
@ -76,6 +83,25 @@ body {
height: 8px !important;
border-radius: 10px;
}
*::-webkit-scrollbar {
width: 8px !important;
height: 8px !important;
border-radius: 10px;
}
::-webkit-scrollbar-track {
background-color: #f1f1f1 !important;
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background-color: #ccc !important;
border-radius: 999px !important;
}
::-webkit-scrollbar-thumb:hover {
background-color: #bbb !important;
}
.jv-indent::-webkit-scrollbar-track {
background-color: #f1f1f1 !important;
@ -90,3 +116,51 @@ 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 */
}
.json-view-playground .json-view {
background-color: #fff !important;
}
.json-view-flow .json-view {
background-color: #bbb !important;
}
.ag-body-horizontal-scroll-viewport,
.ag-body-vertical-scroll-viewport {
cursor: auto;
}
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar,
.ag-body-vertical-scroll-viewport::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-track,
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-track {
background-color: #f1f1f1;
}
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-thumb,
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb {
background-color: #ccc;
border-radius: 999px;
}
.ag-body-horizontal-scroll-viewport::-webkit-scrollbar-thumb:hover,
.ag-body-vertical-scroll-viewport::-webkit-scrollbar-thumb:hover {
background-color: #bbb;
}

View file

@ -1,12 +1,13 @@
import axios from "axios";
import { useContext, useEffect, useState } from "react";
import { ErrorBoundary } from "react-error-boundary";
import { useNavigate } from "react-router-dom";
import "reactflow/dist/style.css";
import "./App.css";
import { ErrorBoundary } from "react-error-boundary";
import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success";
import CrashErrorComponent from "./components/CrashErrorComponent";
import CrashErrorComponent from "./components/crashErrorComponent";
import FetchErrorComponent from "./components/fetchErrorComponent";
import LoadingComponent from "./components/loadingComponent";
import {
@ -14,16 +15,20 @@ import {
FETCH_ERROR_MESSAGE,
} from "./constants/constants";
import { AuthContext } from "./contexts/authContext";
import { getHealth } from "./controllers/API";
import { autoLogin, getGlobalVariables, getHealth } from "./controllers/API";
import { setupAxiosDefaults } from "./controllers/API/utils";
import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path";
import Router from "./routes";
import useAlertStore from "./stores/alertStore";
import { useDarkStore } from "./stores/darkStore";
import useFlowStore from "./stores/flowStore";
import useFlowsManagerStore from "./stores/flowsManagerStore";
import { useFolderStore } from "./stores/foldersStore";
import { useGlobalVariablesStore } from "./stores/globalVariablesStore/globalVariables";
import { useStoreStore } from "./stores/storeStore";
import { useTypesStore } from "./stores/typesStore";
export default function App() {
useTrackLastVisitedPath();
const removeFromTempNotificationList = useAlertStore(
(state) => state.removeFromTempNotificationList
);
@ -37,40 +42,110 @@ export default function App() {
removeFromTempNotificationList(id);
};
const { isAuthenticated } = useContext(AuthContext);
const { isAuthenticated, login, setUserData, setAutoLogin, getUser } =
useContext(AuthContext);
const refreshFlows = useFlowsManagerStore((state) => state.refreshFlows);
const setLoading = useAlertStore((state) => state.setLoading);
const fetchApiData = useStoreStore((state) => state.fetchApiData);
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 dark = useDarkStore((state) => state.dark);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const loadingFolders = useFolderStore((state) => state.loading);
const [isLoadingHealth, setIsLoadingHealth] = useState(false);
useEffect(() => {
refreshStars();
refreshVersion();
// If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks.
if (isAuthenticated === true) {
// get data from db
getTypes().then(() => {
refreshFlows();
});
checkHasStore();
fetchApiData();
if (!dark) {
document.getElementById("body")!.classList.remove("dark");
} else {
document.getElementById("body")!.classList.add("dark");
}
}, [isAuthenticated]);
}, [dark]);
useEffect(() => {
const abortController = new AbortController();
const isLoginPage = location.pathname.includes("login");
autoLogin(abortController.signal)
.then(async (user) => {
if (user && user["access_token"]) {
user["refresh_token"] = "auto";
login(user["access_token"]);
setUserData(user);
setAutoLogin(true);
setLoading(false);
fetchAllData();
}
})
.catch(async (error) => {
if (error.name !== "CanceledError") {
setAutoLogin(false);
if (isAuthenticated && !isLoginPage) {
getUser();
fetchAllData();
} else {
setLoading(false);
useFlowsManagerStore.setState({ isLoading: false });
}
}
});
/*
Abort the request as it isn't needed anymore, the component being
unmounted. It helps avoid, among other things, the well-known "can't
perform a React state update on an unmounted component" warning.
*/
return () => abortController.abort();
}, []);
const fetchAllData = async () => {
setTimeout(async () => {
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
}, 1000);
};
const fetchData = async () => {
return new Promise<void>(async (resolve, reject) => {
if (isAuthenticated) {
try {
await setupAxiosDefaults();
await getFoldersApi();
await getTypes();
await refreshFlows();
console.log(axios.defaults);
const res = await getGlobalVariables();
setGlobalVariables(res);
checkHasStore();
fetchApiData();
resolve();
} catch (error) {
console.error("Failed to fetch data:", error);
reject();
}
}
});
};
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 () => {
@ -78,6 +153,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("/all");
window.location.reload();
}
};
return (
//need parent component with width and height
<div className="flex h-full flex-col">
@ -87,51 +186,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 || loadingFolders ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
</>
</ErrorBoundary>
<div></div>
<div className="app-div" 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}
/>
) : alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
<SuccessAlert
key={alert.id}
title={alert.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}
/>
) : (
alert.type === "notice" && (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
)
)}
</div>
))}
</div>
<div className="z-40 flex flex-col-reverse">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "success" && (
<SuccessAlert
key={alert.id}
title={alert.title}
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
</div>
</div>
);

View file

@ -1,577 +0,0 @@
import { cloneDeep } from "lodash";
import React, { ReactNode, useEffect, useRef, useState } from "react";
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
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 InputFileComponent from "../../../../components/inputFileComponent";
import InputListComponent from "../../../../components/inputListComponent";
import IntComponent from "../../../../components/intComponent";
import KeypairListComponent from "../../../../components/keypairListComponent";
import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Badge } from "../../../../components/ui/badge";
import { Button } from "../../../../components/ui/button";
import {
LANGFLOW_SUPPORTED_TYPES,
TOOLTIP_EMPTY,
inputHandleHover,
outputHandleHover,
} 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 { ParameterComponentType } from "../../../../types/components";
import { NodeDataType } from "../../../../types/flow";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { classNames, groupByFamily } from "../../../../utils/utils";
export default function ParameterComponent({
left,
id,
data,
tooltipTitle,
title,
conditionPath,
color,
type,
name = "",
required = false,
optionalHandle = null,
info = "",
proxy,
showNode,
index = "",
}: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const setErrorData = useAlertStore((state) => state.setErrorData);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
let disabled =
edges.some(
(edge) =>
edge.targetHandle === scapedJSONStringfy(proxy ? { ...id, proxy } : id)
) ?? false;
const myData = useTypesStore((state) => state.data);
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;
}
try {
const res = await postCustomComponentUpdate(code, name);
if (res.status === 200 && data.node?.template) {
data.node!.template[name] = res.data.template[name];
}
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
};
const handleOnNewValue = (
newValue: string | string[] | boolean | Object[]
): void => {
if (data.node!.template[name].value !== newValue) {
takeSnapshot();
}
data.node!.template[name].value = newValue; // necessary to enable ctrl+z inside the input
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template[name].value = newValue;
return newNode;
});
renderTooltips();
};
const updateNodeInternals = useUpdateNodeInternals();
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
if (!data.node) return;
if (data.node!.template[name].value !== code) {
takeSnapshot();
}
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;
return newNode;
});
updateNodeInternals(data.id);
renderTooltips();
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
// @ts-ignore
infoHtml.current = (
<div className="h-full w-full break-words">
{info.split("\n").map((line, index) => (
<p key={index} className="block">
{line}
</p>
))}
</div>
);
}, [info]);
function renderTooltips() {
let groupedObj: any = groupByFamily(myData, tooltipTitle!, left, flow!);
groupedEdge.current = groupedObj;
if (groupedObj && groupedObj.length > 0) {
//@ts-ignore
refHtml.current = groupedObj.map((item, index) => {
const Icon: any =
nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return (
<div key={index}>
{index === 0 && (
<span>{left ? inputHandleHover : outputHandleHover}</span>
)}
<span
key={index}
className={classNames(
index > 0 ? "mt-2 flex items-center" : "mt-3 flex items-center"
)}
>
<div
className="h-5 w-5"
style={{
color: nodeColors[item.family],
}}
>
<Icon
className="h-5 w-5"
strokeWidth={1.5}
style={{
color: nodeColors[item.family] ?? nodeColors.unknown,
}}
/>
</div>
<span className="ps-2 text-xs text-foreground">
{nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span className="text-xs">
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index ===
item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span className="text-xs">
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
);
});
} else {
//@ts-ignore
refHtml.current = <span>{TOOLTIP_EMPTY}</span>;
}
}
useEffect(() => {
renderTooltips();
}, [tooltipTitle, flow]);
return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={1000}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
key={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, nodes, edges)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background",
!showNode ? "mt-0" : ""
)}
style={{
borderColor: color,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
</ShadTooltip>
</div>
</Button>
)
) : (
<div
ref={ref}
className={
"relative mt-1 flex w-full flex-wrap items-center justify-between bg-muted px-5 py-2" +
(name === "code" ? " hidden " : "")
}
>
<>
<div
className={
"w-full truncate text-sm" +
(left ? "" : " text-end") +
(info !== "" ? " flex items-center" : "")
}
>
{" "}
{conditionPath && (
<Badge variant="gray" size="md" className="mr-2">
{conditionPath}
</Badge>
)}
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
</ShadTooltip>
) : (
title
)}
<span className={(info === "" ? "" : "ml-1 ") + " text-status-red"}>
{required ? " *" : ""}
</span>
<div className="">
{info !== "" && (
<ShadTooltip content={infoHtml.current}>
{/* put div to avoid bug that does not display tooltip */}
<div>
<IconComponent
name="Info"
className="relative bottom-px ml-1.5 h-3 w-4"
/>
</div>
</ShadTooltip>
)}
</div>
</div>
{left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
key={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, nodes, edges)
}
className={classNames(
left ? "-ml-0.5 " : "-mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
</ShadTooltip>
</div>
</Button>
)}
{left === true &&
type === "str" &&
!data.node?.template[name].options ? (
<div className="mt-2 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
}
onChange={handleOnNewValue}
/>
) : 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}
disabled={disabled}
password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
/>
</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>
)}
</div>
)}
</div>
) : left === true && type === "bool" ? (
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + index}
disabled={disabled}
enabled={data.node?.template[name].value ?? false}
setEnabled={handleOnNewValue}
size="large"
/>
</div>
) : left === true && type === "float" ? (
<div className="mt-2 w-full">
<FloatComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
rangeSpec={data.node?.template[name].rangeSpec}
onChange={handleOnNewValue}
/>
</div>
) : left === true &&
type === "str" &&
data.node?.template[name].options ? (
// TODO: Improve CSS
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
options={data.node.template[name].options}
onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"}
id={"dropdown-" + index}
/>
</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>
)}
</div>
) : left === true && type === "code" ? (
<div className="mt-2 w-full">
<CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + index}
/>
</div>
) : left === true && type === "file" ? (
<div className="mt-2 w-full">
<InputFileComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
fileTypes={data.node?.template[name].fileTypes}
onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath;
}}
></InputFileComponent>
</div>
) : left === true && type === "int" ? (
<div className="mt-2 w-full">
<IntComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + index}
/>
</div>
) : left === true && type === "prompt" ? (
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={data.node?.flow ? true : false}
field_name={name}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"prompt-input-" + index}
data-testid={"prompt-input-" + index}
/>
</div>
) : left === true && type === "NestedDict" ? (
<div className="mt-2 w-full">
<DictComponent
disabled={disabled}
editNode={false}
value={
!data.node!.template[name].value ||
data.node!.template[name].value?.toString() === "{}"
? {
yourkey: "value",
}
: data.node!.template[name].value
}
onChange={handleOnNewValue}
id="div-dict-input"
/>
</div>
) : left === true && type === "dict" ? (
<div className="mt-2 w-full">
<KeypairListComponent
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
handleOnNewValue(valueToNumbers);
}}
/>
</div>
) : (
<></>
)}
</>
</div>
);
}

View file

@ -6,7 +6,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "../../components/ui/popover";
import { zeroNotifications } from "../../constants/constants";
import { ZERO_NOTIFICATIONS } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import { AlertDropdownType } from "../../types/alerts";
import SingleAlert from "./components/singleAlertComponent";
@ -70,7 +70,7 @@ export default function AlertDropdown({
))
) : (
<div className="flex h-full w-full items-center justify-center pb-16 text-ring">
{zeroNotifications}
{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

@ -0,0 +1,9 @@
type RawComponentProps = {};
const RawComponent = ({}: RawComponentProps) => {
return (
<>
<div>RawComponent</div>
</>
);
};
export default RawComponent;

View file

@ -1,68 +0,0 @@
import { cloneDeep } from "lodash";
import useFlowStore from "../../stores/flowStore";
import { IOInputProps } from "../../types/components";
import IOFileInput from "../IOInputs/FileInput";
import { Textarea } from "../ui/textarea";
export default function IOInputField({
inputType,
inputId,
}: IOInputProps): JSX.Element | undefined {
const nodes = useFlowStore((state) => state.nodes);
const setNode = useFlowStore((state) => state.setNode);
const node = nodes.find((node) => node.id === inputId);
function handleInputType() {
if (!node) return <>"No node found!"</>;
switch (inputType) {
case "TextInput":
return (
<Textarea
className="w-full"
placeholder={"Enter text..."}
value={node.data.node!.template["input_value"].value}
onChange={(e) => {
e.target.value;
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value =
e.target.value;
setNode(node.id, newNode);
}
}}
/>
);
case "FileLoader":
return (
<IOFileInput
field={node.data.node!.template["file_path"]["value"]}
updateValue={(e) => {
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["file_path"].value = e;
setNode(node.id, newNode);
}
}}
/>
);
default:
return (
<Textarea
className="w-full custom-scroll"
placeholder={"Enter text..."}
value={node.data.node!.template["input_value"]}
onChange={(e) => {
e.target.value;
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value =
e.target.value;
setNode(node.id, newNode);
}
}}
/>
);
}
}
return handleInputType();
}

View file

@ -1,51 +0,0 @@
import { cloneDeep } from "lodash";
import useFlowStore from "../../stores/flowStore";
import { IOOutputProps } from "../../types/components";
import { Textarea } from "../ui/textarea";
export default function IOOutputView({
outputType,
outputId,
}: IOOutputProps): JSX.Element | undefined {
const nodes = useFlowStore((state) => state.nodes);
const setNode = useFlowStore((state) => state.setNode);
const flowPool = useFlowStore((state) => state.flowPool);
const node = nodes.find((node) => node.id === outputId);
function handleOutputType() {
if (!node) return <>"No node found!"</>;
switch (outputType) {
case "TextOutput":
return (
<Textarea
className="w-full custom-scroll"
placeholder={"Empty"}
// update to real value on flowPool
value={
(flowPool[node.id] ?? [])[(flowPool[node.id]?.length ?? 1) - 1]
?.params ?? ""
}
readOnly
/>
);
default:
return (
<Textarea
className="w-full custom-scroll"
placeholder={"Enter text..."}
value={node.data.node!.template["input_value"]}
onChange={(e) => {
e.target.value;
if (node) {
let newNode = cloneDeep(node);
newNode.data.node!.template["input_value"].value =
e.target.value;
setNode(node.id, newNode);
}
}}
/>
);
}
}
return handleOutputType();
}

View file

@ -1,335 +0,0 @@
import { useEffect, useState } from "react";
import {
CHAT_FORM_DIALOG_SUBTITLE,
outputsModalTitle,
textInputModalTitle,
} from "../../constants/constants";
import BaseModal from "../../modals/baseModal";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { updateVerticesOrder } from "../../utils/buildUtils";
import { cn } from "../../utils/utils";
import AccordionComponent from "../AccordionComponent";
import IOInputField from "../IOInputField";
import IOOutputView from "../IOOutputView";
import IconComponent from "../genericIconComponent";
import NewChatView from "../newChatView";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
export default function IOView({ children, open, setOpen }): JSX.Element {
const inputs = useFlowStore((state) => state.inputs).filter(
(input) => input.type !== "ChatInput"
);
const chatInput = useFlowStore((state) => state.inputs).find(
(input) => input.type === "ChatInput"
);
const outputs = useFlowStore((state) => state.outputs).filter(
(output) => output.type !== "ChatOutput"
);
const chatOutput = useFlowStore((state) => state.outputs).find(
(output) => output.type === "ChatOutput"
);
const nodes = useFlowStore((state) => state.nodes).filter(
(node) =>
inputs.some((input) => input.id === node.id) ||
outputs.some((output) => output.id === node.id)
);
const haveChat = chatInput || chatOutput;
const [selectedTab, setSelectedTab] = useState(
inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0
);
const [selectedViewField, setSelectedViewField] = useState<
{ type: string; id: string } | undefined
>(undefined);
const buildFlow = useFlowStore((state) => state.buildFlow);
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const [lockChat, setLockChat] = useState(false);
const [chatValue, setChatValue] = useState("");
const isBuilding = useFlowStore((state) => state.isBuilding);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
async function updateVertices() {
return updateVerticesOrder(currentFlow!.id, null);
}
useEffect(() => {
if (open) {
updateVertices();
}
}, [open, currentFlow]);
async function sendMessage(count = 1): Promise<void> {
if (isBuilding) return;
setIsBuilding(true);
setLockChat(true);
setChatValue("");
for (let i = 0; i < count; i++) {
await buildFlow({ input_value: chatValue }).catch((err) => {
console.error(err);
setLockChat(false);
});
}
setLockChat(false);
}
useEffect(() => {
setSelectedViewField(undefined);
setSelectedTab(inputs.length > 0 ? 1 : outputs.length > 0 ? 2 : 0);
}, [inputs.length, outputs.length]);
return (
<BaseModal
size={haveChat ? (selectedTab === 0 ? "large-thin" : "large") : "small"}
open={open}
setOpen={setOpen}
>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
{/* TODO ADAPT TO ALL TYPES OF INPUTS AND OUTPUTS */}
<BaseModal.Header description={CHAT_FORM_DIALOG_SUBTITLE}>
<div className="flex items-center">
<span className="pr-2">Chat</span>
<IconComponent
name="prompts"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</div>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex h-full flex-col">
<div className="flex-max-width mt-2 h-full">
{selectedTab !== 0 && (
<div
className={cn(
"mr-6 flex h-full w-2/6 flex-shrink-0 flex-col justify-start",
haveChat ? "w-2/6" : "w-full"
)}
>
<Tabs
value={selectedTab.toString()}
className={
"flex h-full flex-col overflow-y-auto rounded-md border bg-muted text-center custom-scroll"
}
onValueChange={(value) => {
setSelectedTab(Number(value));
}}
>
<div className="api-modal-tablist-div">
<TabsList>
{inputs.length > 0 && (
<TabsTrigger value={"1"}>Inputs</TabsTrigger>
)}
{outputs.length > 0 && (
<TabsTrigger value={"2"}>Outputs</TabsTrigger>
)}
</TabsList>
</div>
<TabsContent
value={"1"}
className="api-modal-tabs-content mt-4"
>
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
<IconComponent className="h-4 w-4" name={"Type"} />
{textInputModalTitle}
</div>
{nodes
.filter((node) =>
inputs.some((input) => input.id === node.id)
)
.map((node, index) => {
const input = inputs.find(
(input) => input.id === node.id
)!;
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<Badge variant="gray" size="md">
{node.data.node.display_name}
</Badge>
{haveChat && (
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(input);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
)}
</div>
}
key={index}
keyValue={input.id}
>
<div className="file-component-tab-column">
<div className="">
{input && (
<IOInputField
inputType={input.type}
inputId={input.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
<TabsContent
value={"2"}
className="api-modal-tabs-content mt-4"
>
<div className="mx-2 mb-2 flex items-center gap-2 text-sm font-bold">
<IconComponent className="h-4 w-4" name={"FileType2"} />
{outputsModalTitle}
</div>
{nodes
.filter((node) =>
outputs.some((output) => output.id === node.id)
)
.map((node, index) => {
const output = outputs.find(
(output) => output.id === node.id
)!;
return (
<div
className="file-component-accordion-div"
key={index}
>
<AccordionComponent
trigger={
<div className="file-component-badge-div">
<Badge variant="gray" size="md">
{output.id}
</Badge>
{haveChat && (
<div
className="-mb-1 pr-4"
onClick={(event) => {
event.stopPropagation();
setSelectedViewField(output);
}}
>
<IconComponent
className="h-4 w-4"
name="ExternalLink"
></IconComponent>
</div>
)}
</div>
}
key={index}
keyValue={output.id}
>
<div className="file-component-tab-column">
<div className="">
{output && (
<IOOutputView
outputType={output.type}
outputId={output.id}
/>
)}
</div>
</div>
</AccordionComponent>
</div>
);
})}
</TabsContent>
</Tabs>
</div>
)}
{haveChat ? (
<div className="flex h-full w-full">
{selectedViewField && (
<div
className={cn(
"flex h-full w-full flex-col items-start gap-4 p-4",
!selectedViewField ? "hidden" : ""
)}
>
<div className="font-xl flex items-center justify-center gap-3 font-semibold">
<button onClick={() => setSelectedViewField(undefined)}>
<IconComponent
name={"ArrowLeft"}
className="h-6 w-6"
></IconComponent>
</button>
{selectedViewField.type}
</div>
<div className="h-full">
{inputs.some(
(input) => input.id === selectedViewField.id
) ? (
<IOInputField
inputType={selectedViewField.type!}
inputId={selectedViewField.id!}
/>
) : (
<IOOutputView
outputType={selectedViewField.type!}
outputId={selectedViewField.id!}
/>
)}
</div>
</div>
)}
<div
className={cn(
"flex h-full w-full",
selectedViewField ? "hidden" : ""
)}
>
<NewChatView
sendMessage={sendMessage}
chatValue={chatValue}
setChatValue={setChatValue}
lockChat={lockChat}
setLockChat={setLockChat}
/>
</div>
</div>
) : (
<div className="absolute bottom-8 right-8"></div>
)}
</div>
{!haveChat && (
<div className="flex w-full justify-end pt-6">
<Button
variant={"outline"}
className="flex gap-2 px-3"
onClick={() => sendMessage(1)}
>
<IconComponent
name={isBuilding ? "Loader2" : "Play"}
className={cn(
"h-4 w-4",
isBuilding
? "animate-spin"
: "fill-current text-medium-indigo"
)}
/>
Run Flow
</Button>
</div>
)}
</div>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -0,0 +1,162 @@
import { saveAs } from "file-saver";
import OpenSeadragon from "openseadragon";
import { useEffect, useRef, useState } from "react";
import { IMGViewErrorMSG, IMGViewErrorTitle } from "../../constants/constants";
import useAlertStore from "../../stores/alertStore";
import ForwardedIconComponent from "../genericIconComponent";
import { Separator } from "../ui/separator";
export default function ImageViewer({ image }) {
const viewerRef = useRef(null);
const [errorDownloading, setErrordownloading] = useState(false);
const setErrorList = useAlertStore((state) => state.setErrorData);
const [initialMsg, setInicialMsg] = useState("Please build your flow");
useEffect(() => {
try {
if (viewerRef.current) {
// Initialize OpenSeadragon viewer
const viewer = OpenSeadragon({
element: viewerRef.current,
prefixUrl:
"https://cdnjs.cloudflare.com/ajax/libs/openseadragon/2.4.2/images/", // Optional: Set the path to OpenSeadragon images
tileSources: { type: "image", url: image },
defaultZoomLevel: 1,
maxZoomPixelRatio: 4,
showNavigationControl: false,
});
const zoomInButton = document.getElementById("zoom-in-button");
const zoomOutButton = document.getElementById("zoom-out-button");
const homeButton = document.getElementById("home-button");
const fullPageButton = document.getElementById("full-page-button");
zoomInButton!.addEventListener("click", () =>
viewer.viewport.zoomBy(1.2),
);
zoomOutButton!.addEventListener("click", () =>
viewer.viewport.zoomBy(0.8),
);
homeButton!.addEventListener("click", () => viewer.viewport.goHome());
fullPageButton!.addEventListener("click", () =>
viewer.setFullScreen(true),
);
// Optionally, you can set additional viewer options here
// Cleanup function
return () => {
viewer.destroy();
zoomInButton!.removeEventListener("click", () =>
viewer.viewport.zoomBy(1.2),
);
zoomOutButton!.removeEventListener("click", () =>
viewer.viewport.zoomBy(0.8),
);
homeButton!.removeEventListener("click", () =>
viewer.viewport.goHome(),
);
fullPageButton!.removeEventListener("click", () =>
viewer.setFullScreen(true),
);
};
}
} catch (error) {
console.error("Error initializing OpenSeadragon:", error);
}
}, [image]);
function download() {
const imageUrl = image;
// Fetch the image data
fetch(imageUrl)
.then((response) => response.blob())
.then((blob) => {
// Save the image using FileSaver.js
saveAs(blob, "image.jpg");
})
.catch((error) => {
setErrorList({ title: "There was an error downloading your image" });
console.error("Error downloading image:", error);
});
}
return image === "" ? (
<div className="align-center flex h-full w-full flex-col justify-center gap-5 rounded-md border border-border bg-muted">
<div className="align-center flex justify-center gap-2 ">
<ForwardedIconComponent name="Image" />
{IMGViewErrorTitle}
</div>
<div className="align-center flex justify-center">
<div className="langflow-chat-desc align-center flex justify-center">
<div className="langflow-chat-desc-span">{IMGViewErrorMSG}</div>
</div>
</div>
</div>
) : (
<>
<div className="align-center my-2 mb-4 flex w-full justify-center">
<div className="shadow-round-btn-shadow hover:shadow-round-btn-shadow flex w-[50%] items-center justify-center rounded-sm border bg-muted shadow-md transition-all">
<button
id="zoom-in-button"
className="relative inline-flex w-full w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="ZoomIn"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button
id="zoom-out-button"
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="ZoomOut"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button
id="home-button"
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="RotateCcw"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button
id="full-page-button"
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="Maximize2"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
<div>
<Separator orientation="vertical" />
</div>
<button
onClick={download}
className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all transition-all duration-500 ease-in-out ease-in-out hover:bg-hover"
>
<ForwardedIconComponent
name="ArrowDownToLine"
className={"h-5 w-5 text-secondary-foreground"}
/>
</button>
</div>
</div>
<div id="canvas" ref={viewerRef} className={`h-[90%] w-full `} />
</>
);
}

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,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,72 @@
import { useState } from "react";
import IconComponent from "../../../../components/genericIconComponent";
import { AccordionComponentType } from "../../../../types/components";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "../../../ui/custom-accordion";
export default function FolderAccordionComponent({
trigger,
open = [],
keyValue,
options,
}: AccordionComponentType): JSX.Element {
const [value, setValue] = useState(
open.length === 0 ? "" : getOpenAccordion(),
);
function getOpenAccordion(): string {
let value = "";
open.forEach((el) => {
if (el == trigger) {
value = trigger;
}
});
return value;
}
function handleClick(): void {
value === "" ? setValue(keyValue!) : setValue("");
}
return (
<>
<Accordion
type="single"
className="w-full"
value={value}
onValueChange={setValue}
>
<AccordionItem value={keyValue!} className="">
<AccordionTrigger
onClick={() => {
handleClick();
}}
className="px-2"
>
{trigger}
</AccordionTrigger>
<AccordionContent>
{options!.map((option, index) => (
<div
key={index}
className="flex cursor-pointer px-2 py-1 hover:bg-muted-foreground/10"
>
<IconComponent
name={option.icon}
className="relative top-[1.5px] mr-2 h-4 w-4"
aria-hidden="true"
/>
<span className="truncate">{option.title}</span>
</div>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
</>
);
}

View file

@ -12,19 +12,19 @@ export default function AccordionComponent({
children,
open = [],
keyValue,
sideBar,
}: AccordionComponentType): JSX.Element {
const [value, setValue] = useState(
open.length === 0 ? "" : getOpenAccordion()
open.length === 0 ? "" : getOpenAccordion(),
);
function getOpenAccordion(): string {
let value = "";
open.forEach((el) => {
if (el == trigger) {
value = trigger;
if (el == keyValue) {
value = keyValue;
}
});
return value;
}
@ -45,12 +45,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 flex flex-col">
{children}
<AccordionContent>
<div className="AccordionContent flex flex-col">{children}</div>
</AccordionContent>
</AccordionItem>
</Accordion>

View file

@ -0,0 +1,145 @@
import { useState } from "react";
import { registerGlobalVariable } from "../../controllers/API";
import BaseModal from "../../modals/baseModal";
import useAlertStore from "../../stores/alertStore";
import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables";
import { useTypesStore } from "../../stores/typesStore";
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";
import sortByName from "./utils/sort-by-name";
//TODO IMPLEMENT FORM LOGIC
export default function AddNewVariableButton({ children }): JSX.Element {
const [key, setKey] = useState("");
const [value, setValue] = useState("");
const [type, setType] = useState("Generic");
const [fields, setFields] = useState<string[]>([]);
const [open, setOpen] = useState(false);
const setErrorData = useAlertStore((state) => state.setErrorData);
const componentFields = useTypesStore((state) => state.ComponentFields);
const unavaliableFields = new Set(
Object.keys(useGlobalVariablesStore((state) => state.unavaliableFields)),
);
const availableFields = () => {
const fields = Array.from(componentFields).filter(
(field) => !unavaliableFields.has(field),
);
return sortByName(fields);
};
const addGlobalVariable = useGlobalVariablesStore(
(state) => state.addGlobalVariable,
);
function handleSaveVariable() {
let data: {
name: string;
value: string;
type?: string;
default_fields?: string[];
} = {
name: key,
type,
value,
default_fields: fields,
};
registerGlobalVariable(data)
.then((res) => {
const { name, id, type } = res.data;
addGlobalVariable(name, id, type, fields);
setKey("");
setValue("");
setType("");
setFields([]);
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..."
id={"type-global-variables"}
></InputComponent>
<Label>Value</Label>
{type === "Credential" ? (
<InputComponent
password
value={value}
onChange={(e) => {
setValue(e);
}}
placeholder="Insert a value for the variable..."
/>
) : (
<Textarea
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
placeholder="Insert a value for the variable..."
className="w-full resize-none custom-scroll"
/>
)}
<Label>Apply To Fields (optional)</Label>
<InputComponent
setSelectedOptions={(value) => setFields(value)}
selectedOptions={fields}
options={availableFields()}
password={false}
placeholder="Choose a field for the variable..."
id={"apply-to-fields"}
></InputComponent>
</div>
</BaseModal.Content>
<BaseModal.Footer>
<Button onClick={handleSaveVariable}>Save Variable</Button>
</BaseModal.Footer>
</BaseModal>
);
}

View file

@ -0,0 +1,3 @@
export default function sortByName(stringList: string[]): string[] {
return stringList.sort((a, b) => a.localeCompare(b));
}

View file

@ -0,0 +1,12 @@
export default function ArrayReader({ array }: { array: any[] }): JSX.Element {
//TODO check array type
return (
<div>
<ul>
{array.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}

View file

@ -0,0 +1,39 @@
import { storeComponent } from "../../../../types/store";
import { cn } from "../../../../utils/utils";
import ForwardedIconComponent from "../../../genericIconComponent";
import ShadTooltip from "../../../shadTooltipComponent";
import { Card, CardHeader, CardTitle } from "../../../ui/card";
export default function DragCardComponent({ data }: { data: storeComponent }) {
return (
<>
<Card
draggable
//TODO check color schema
className={cn(
"group relative flex flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
)}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-start justify-between gap-3 text-xl">
<ForwardedIconComponent
className={cn(
"visible flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon",
)}
name={data.is_component ? "ToyBrick" : "Group"}
/>
<div className="w-full truncate pr-3">{data.name}</div>
</CardTitle>
</div>
</CardHeader>
</div>
</Card>
</>
);
}

View file

@ -1,15 +1,21 @@
import { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import { Control } from "react-hook-form";
import { getComponent, postLikeComponent } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
import IOModal from "../../modals/IOModal";
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useStoreStore } from "../../stores/storeStore";
import { FlowType } from "../../types/flow";
import { storeComponent } from "../../types/store";
import cloneFLowWithParent from "../../utils/storeUtils";
import cloneFLowWithParent, {
getInputsAndOutputs,
} from "../../utils/storeUtils";
import { cn } from "../../utils/utils";
import ShadTooltip from "../ShadTooltipComponent";
import IconComponent from "../genericIconComponent";
import { Badge } from "../ui/badge";
import ShadTooltip from "../shadTooltipComponent";
import { Button } from "../ui/button";
import {
Card,
@ -18,37 +24,94 @@ import {
CardHeader,
CardTitle,
} from "../ui/card";
import { Checkbox } from "../ui/checkbox";
import { FormControl, FormField } from "../ui/form";
import Loading from "../ui/loading";
import { convertTestName } from "./utils/convert-test-name";
import DragCardComponent from "./components/dragCardComponent";
export default function CollectionCardComponent({
data,
authorized = true,
disabled = false,
button,
onClick,
onDelete,
playground,
control,
is_component,
}: {
data: storeComponent;
authorized?: boolean;
disabled?: boolean;
onClick?: () => void;
button?: JSX.Element;
playground?: boolean;
onDelete?: () => void;
control?: Control<any, any>;
is_component?: boolean;
}) {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const setValidApiKey = useStoreStore((state) => state.updateValidApiKey);
const cleanFlowPool = useFlowStore((state) => state.CleanFlowPool);
const isStore = false;
const [loading, setLoading] = useState(false);
const [loadingLike, setLoadingLike] = useState(false);
const [liked_by_user, setLiked_by_user] = useState(
data?.liked_by_user ?? false
data?.liked_by_user ?? false,
);
const [likes_count, setLikes_count] = useState(data?.liked_by_count ?? 0);
const [downloads_count, setDownloads_count] = useState(
data?.downloads_count ?? 0
data?.downloads_count ?? 0,
);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const setCurrentFlow = useFlowsManagerStore((state) => state.setCurrentFlow);
const getFlowById = useFlowsManagerStore((state) => state.getFlowById);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const setNodes = useFlowStore((state) => state.setNodes);
const setEdges = useFlowStore((state) => state.setEdges);
const [openPlayground, setOpenPlayground] = useState(false);
const [openDelete, setOpenDelete] = useState(false);
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId,
);
const [loadingPlayground, setLoadingPlayground] = useState(false);
const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards,
);
const name = data.is_component ? "Component" : "Flow";
async function getFlowData() {
const res = await getComponent(data.id);
const newFlow = cloneFLowWithParent(res, res.id, data.is_component, true);
return newFlow;
}
function hasPlayground(flow?: FlowType) {
if (!flow) {
return false;
}
const { inputs, outputs } = getInputsAndOutputs(flow?.data?.nodes ?? []);
return inputs.length > 0 || outputs.length > 0;
}
useEffect(() => {
if (currentFlowId && playground) {
if (openPlayground) {
setNodes(currentFlow?.data?.nodes ?? [], true);
setEdges(currentFlow?.data?.edges ?? [], true);
} else {
setNodes([], true);
setEdges([], true);
}
cleanFlowPool();
}
}, [openPlayground]);
useEffect(() => {
if (data) {
setLiked_by_user(data?.liked_by_user ?? false);
@ -127,219 +190,403 @@ export default function CollectionCardComponent({
}
}
return (
<Card
className={cn(
"group relative flex flex-col justify-between overflow-hidden transition-all hover:shadow-md",
disabled ? "pointer-events-none opacity-50" : ""
)}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
<IconComponent
className={cn(
"flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon"
)}
name={data.is_component ? "ToyBrick" : "Group"}
/>
<ShadTooltip content={data.name}>
<div className="w-full truncate">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex gap-3">
{data.private && (
<ShadTooltip content="Private">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Lock" className="h-4 w-4" />
</span>
</ShadTooltip>
)}
{!data.is_component && (
<ShadTooltip content="Components">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="ToyBrick" className="h-4 w-4" />
{data?.metadata?.total ?? 0}
</span>
</ShadTooltip>
)}
<ShadTooltip content="Likes">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Heart" className={cn("h-4 w-4 ")} />
{likes_count ?? 0}
</span>
</ShadTooltip>
<ShadTooltip content="Downloads">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="DownloadCloud" className="h-4 w-4" />
{downloads_count ?? 0}
</span>
</ShadTooltip>
</div>
)}
const isSelectedCard =
selectedFlowsComponentsCards?.includes(data?.id) ?? false;
{onDelete && data?.metadata === undefined && (
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
function onDragStart(event: React.DragEvent<any>) {
let image: JSX.Element = <DragCardComponent data={data} />; // <== whatever you want here
var ghost = document.createElement("div");
ghost.style.transform = "translate(-10000px, -10000px)";
ghost.style.position = "absolute";
document.body.appendChild(ghost);
event.dataTransfer.setDragImage(ghost, 0, 0);
const root = createRoot(ghost);
root.render(image);
const flow = getFlowById(data.id);
if (flow) {
event.dataTransfer.setData("flow", JSON.stringify(data));
}
}
return (
<>
<Card
onDragStart={onDragStart}
draggable
data-testid={`card-${convertTestName(data.name)}`}
//TODO check color schema
className={cn(
"group relative flex min-h-[11rem] flex-col justify-between overflow-hidden transition-all hover:bg-muted/50 hover:shadow-md hover:dark:bg-[#ffffff10]",
disabled ? "pointer-events-none opacity-50" : "",
onClick ? "cursor-pointer" : "",
isSelectedCard ? "border border-selected" : "",
)}
onClick={onClick}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-start justify-between gap-3 text-xl">
<IconComponent
className={cn(
"visible flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon",
)}
name={data.is_component ? "ToyBrick" : "Group"}
/>
<ShadTooltip content={data.name}>
<div className="w-full truncate pr-3">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex items-center gap-3">
{data.private && (
<ShadTooltip content="Private">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Lock" className="h-4 w-4" />
</span>
</ShadTooltip>
)}
{!data.is_component && (
<ShadTooltip content="Components">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="ToyBrick" className="h-4 w-4" />
<span data-testid={`total-${data.name}`}>
{data?.metadata?.total ?? 0}
</span>
</span>
</ShadTooltip>
)}
<ShadTooltip content="Likes">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent
name="Heart"
className={cn("h-4 w-4 ")}
/>
<span data-testid={`likes-${data.name}`}>
{likes_count ?? 0}
</span>
</span>
</ShadTooltip>
<ShadTooltip content="Downloads">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent
name="DownloadCloud"
className="h-4 w-4"
/>
<span data-testid={`downloads-${data.name}`}>
{downloads_count ?? 0}
</span>
</span>
</ShadTooltip>
</div>
)}
{control && (
<div
className="flex"
onClick={(e) => {
e.stopPropagation();
}}
>
<FormField
control={control}
name={`${data.id}`}
defaultValue={false}
render={({ field }) => (
<FormControl>
<Checkbox
aria-label="checkbox-component"
checked={field.value}
onCheckedChange={field.onChange}
className="h-5 w-5 border border-ring data-[state=checked]:border-selected data-[state=checked]:bg-selected"
/>
</FormControl>
)}
/>
</div>
)}
</CardTitle>
</div>
<div className="flex gap-2">
{data.user_created && data.user_created.username && (
<span className="text-sm text-primary">
by <b>{data.user_created.username}</b>
{data.last_tested_version && (
<>
{" "}
|{" "}
<span className="text-xs">
{" "}
⛓︎ v{data.last_tested_version}
</span>
</>
)}
</span>
)}
<div className="flex w-full flex-1 flex-wrap gap-2">
{/* {data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))} */}
</div>
</div>
<CardDescription className="pb-2 pt-2">
<div className="truncate-doubleline">{data.description}</div>
</CardDescription>
</CardHeader>
</div>
<CardFooter>
<div className="z-50 flex w-full items-center justify-between gap-2">
<div className="flex w-full flex-wrap items-end justify-end gap-2">
{playground && data?.metadata !== undefined ? (
<Button
disabled={loadingPlayground || !authorized}
key={data.id}
tabIndex={-1}
variant="outline"
size="sm"
className="z-50 gap-2 whitespace-nowrap"
data-testid={"playground-flow-button-" + data.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setLoadingPlayground(true);
const flow = getFlowById(data.id);
if (flow) {
if (!hasPlayground(flow)) {
setErrorData({
title: "Error",
list: ["This flow doesn't have a playground."],
});
setLoadingPlayground(false);
return;
}
setCurrentFlowId(data.id);
setOpenPlayground(true);
setLoadingPlayground(false);
} else {
getFlowData().then((res) => {
if (!hasPlayground(res)) {
setErrorData({
title: "Error",
list: ["This flow doesn't have a playground."],
});
setLoadingPlayground(false);
return;
}
setCurrentFlow(res);
setOpenPlayground(true);
setLoadingPlayground(false);
});
}
}}
>
<IconComponent
name="Trash2"
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
/>
</DeleteConfirmationModal>
)}
</CardTitle>
</div>
{data.user_created && data.user_created.username && (
<span className="text-sm text-primary">
by <b>{data.user_created.username}</b>
{data.last_tested_version && (
<>
{" "}
|{" "}
<span className="text-xs">
{" "}
⛓︎ v{data.last_tested_version}
</span>
</>
)}
</span>
)}
<CardDescription className="pb-2 pt-2">
<div className="truncate-doubleline">{data.description}</div>
</CardDescription>
</CardHeader>
</div>
<CardFooter>
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-full flex-wrap items-end justify-between gap-2">
<div className="flex w-full flex-1 flex-wrap gap-2">
{data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))}
</div>
{data.liked_by_count != undefined && (
<div className="flex gap-0.5">
{onDelete && data?.metadata !== undefined ? (
<ShadTooltip
content={
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
{!loadingPlayground ? (
<IconComponent
name="BotMessageSquareIcon"
className="h-4 w-4 select-none"
/>
) : (
<Loading className="h-4 w-4 text-medium-indigo" />
)}
Playground
</Button>
) : undefined}
{data.liked_by_count != undefined && (
<div className="flex gap-0.5">
{onDelete && data?.metadata !== undefined ? (
<ShadTooltip
content={
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal onConfirm={onDelete}>
<Button
variant="ghost"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
>
<IconComponent
data-testid={`delete-${convertTestName(data.name)}`}
name="Trash2"
className={cn(
"h-5 w-5",
!authorized ? " text-ring" : "",
)}
/>
</Button>
</DeleteConfirmationModal>
</ShadTooltip>
) : (
<ShadTooltip
content={
authorized ? "Like" : "Please review your API key."
}
>
<Button
disabled={loadingLike}
variant="ghost"
size="xs"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
onClick={() => {
if (!authorized) {
return;
}
handleLike();
}}
data-testid={`like-${data.name}`}
>
<IconComponent
name="Trash2"
name="Heart"
className={cn(
"h-5 w-5",
!authorized ? " text-ring" : ""
liked_by_user
? "fill-destructive stroke-destructive"
: "",
!authorized ? " text-ring" : "",
)}
/>
</Button>
</DeleteConfirmationModal>
</ShadTooltip>
) : (
</ShadTooltip>
)}
<ShadTooltip
content={
authorized ? "Like" : "Please review your API key."
authorized
? isStore
? "Download"
: "Install Locally"
: "Please review your API key."
}
>
<Button
disabled={loadingLike}
disabled={loading}
variant="ghost"
size="xs"
size="icon"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
(!authorized ? " cursor-not-allowed" : "") +
(!loading ? " p-0.5" : "")
}
onClick={() => {
if (!authorized) {
if (loading || !authorized) {
return;
}
handleLike();
handleInstall();
}}
data-testid={`install-${data.name}`}
>
<IconComponent
name="Heart"
name={
loading ? "Loader2" : isStore ? "Download" : "Plus"
}
className={cn(
"h-5 w-5",
liked_by_user
? "fill-destructive stroke-destructive"
: "",
!authorized ? " text-ring" : ""
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
!authorized ? " text-ring" : "",
)}
/>
</Button>
</ShadTooltip>
)}
<ShadTooltip
content={
authorized
? isStore
? "Download"
: "Install Locally"
: "Please review your API key."
}
>
<Button
disabled={loading}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "") +
(!loading ? " p-0.5" : "")
}
onClick={() => {
if (loading || !authorized) {
</div>
)}
{playground && data?.metadata === undefined && (
<Button
disabled={loadingPlayground || !authorized}
key={data.id}
tabIndex={-1}
variant="primary"
size="sm"
className="gap-2 whitespace-nowrap bg-muted"
data-testid={"playground-flow-button-" + data.id}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setLoadingPlayground(true);
const flow = getFlowById(data.id);
if (flow) {
if (!hasPlayground(flow)) {
setErrorData({
title: "Error",
list: ["This flow doesn't have a playground."],
});
setLoadingPlayground(false);
return;
}
handleInstall();
}}
>
setCurrentFlowId(data.id);
setOpenPlayground(true);
setLoadingPlayground(false);
} else {
getFlowData().then((res) => {
if (!hasPlayground(res)) {
setErrorData({
title: "Error",
list: ["This flow doesn't have a playground."],
});
setLoadingPlayground(false);
return;
}
setCurrentFlow(res);
setOpenPlayground(true);
setLoadingPlayground(false);
});
}
}}
>
{!loadingPlayground ? (
<IconComponent
name={loading ? "Loader2" : isStore ? "Download" : "Plus"}
className={cn(
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
!authorized ? " text-ring" : ""
)}
name="BotMessageSquareIcon"
className="h-4 w-4 select-none"
/>
</Button>
</ShadTooltip>
</div>
)}
{button && button}
) : (
<Loading className="h-4 w-4 text-medium-indigo" />
)}
Playground
</Button>
)}
</div>
</div>
</div>
</CardFooter>
</Card>
</CardFooter>
</Card>
{openPlayground && (
<IOModal
cleanOnClose={true}
open={openPlayground}
setOpen={setOpenPlayground}
>
<></>
</IOModal>
)}
{openDelete && (
<DeleteConfirmationModal
open={openDelete}
setOpen={setOpenDelete}
onConfirm={() => {
if (onDelete) onDelete();
}}
description={` ${is_component ? "component" : "flow"}`}
>
<></>
</DeleteConfirmationModal>
)}
</>
);
}

View file

@ -0,0 +1,3 @@
export function convertTestName(name: string): string {
return name.replace(/ /g, "-").toLowerCase();
}

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import { useEffect, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { cn } from "../../utils/utils";
export default function CardsWrapComponent({
onFileDrop,
@ -11,6 +12,23 @@ export default function CardsWrapComponent({
dragMessage?: string;
}) {
const [isDragging, setIsDragging] = useState(false);
useEffect(() => {
// Function to handle visibility change
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") {
// Reset hover state or perform any necessary actions when the tab becomes visible again
setIsDragging(false);
}
};
// Add event listener for visibility change
document.addEventListener("visibilitychange", handleVisibilityChange);
// Cleanup event listener on component unmount
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
};
}, []);
const dragOver = (e) => {
e.preventDefault();
@ -43,12 +61,12 @@ export default function CardsWrapComponent({
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
}
className={cn(
"h-full w-full",
isDragging
? "mb-36 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "",
)}
>
{isDragging ? (
<>

View file

@ -1,116 +0,0 @@
import { Transition } from "@headlessui/react";
import { useState } from "react";
import Loading from "../../../components/ui/loading";
import { FlowType } from "../../../types/flow";
import { MISSED_ERROR_ALERT } from "../../../constants/alerts_constants";
import useAlertStore from "../../../stores/alertStore";
import useFlowStore from "../../../stores/flowStore";
import { validateNodes } from "../../../utils/reactflowUtils";
import RadialProgressComponent from "../../RadialProgress";
import IconComponent from "../../genericIconComponent";
export default function BuildTrigger({
open,
flow,
}: {
open: boolean;
flow: FlowType;
}): JSX.Element {
const isBuilding = useFlowStore((state) => state.isBuilding);
const setIsBuilding = useFlowStore((state) => state.setIsBuilding);
const buildFlow = useFlowStore((state) => state.buildFlow);
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setErrorData = useAlertStore((state) => state.setErrorData);
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: MISSED_ERROR_ALERT,
list: errors,
});
return;
}
const minimumLoadingTime = 200; // in milliseconds
const startTime = Date.now();
setIsBuilding(true);
await enforceMinimumLoadingTime(startTime, minimumLoadingTime);
await buildFlow({});
} catch (error) {
console.error("Error:", error);
} finally {
setIsBuilding(false);
}
}
const hasIO = useFlowStore((state) => state.hasIO);
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={hasIO ? "fixed bottom-20 right-4" : "fixed bottom-4 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,14 +1,22 @@
import { useEffect, useRef, useState } from "react";
import { Transition } from "@headlessui/react";
import { useEffect, useMemo, useRef, useState } from "react";
import IOModal from "../../modals/IOModal";
import ApiModal from "../../modals/apiModal/views";
import ShareModal from "../../modals/shareModal";
import useFlowStore from "../../stores/flowStore";
import { ChatType } from "../../types/chat";
import IOView from "../IOview";
import ChatTrigger from "../ViewTriggers/chat";
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 flowState = useFlowStore((state) => state.flowState);
const nodes = useFlowStore((state) => state.nodes);
const hasIO = useFlowStore((state) => state.hasIO);
const hasStore = useStoreStore((state) => state.hasStore);
const validApiKey = useStoreStore((state) => state.validApiKey);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@ -29,16 +37,116 @@ export default function Chat({ flow }: ChatType): JSX.Element {
const prevNodesRef = useRef<any[] | undefined>();
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 className="flex flex-col">
{/* <BuildTrigger open={open} flow={flow} /> */}
{hasIO && (
<IOView open={open} setOpen={setOpen}>
<ChatTrigger />
</IOView>
)}
</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 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 transition-all duration-500 ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="BotMessageSquareIcon"
className={" h-5 w-5 transition-all"}
/>
Playground
</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="BotMessageSquareIcon"
className={" h-5 w-5 transition-all"}
/>
Playground
</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,15 +14,16 @@ export default function CodeAreaComponent({
setNodeClass,
id = "",
readonly = false,
openModal,
open,
setOpen,
}: CodeAreaComponentType) {
const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value)
typeof value == "string" ? value : JSON.stringify(value),
);
useEffect(() => {
if (disabled && myValue !== "") {
setMyValue("");
onChange("");
onChange("", true);
}
}, [disabled]);
@ -33,7 +34,8 @@ export default function CodeAreaComponent({
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<CodeAreaModal
openModal={openModal}
open={open}
setOpen={setOpen}
readonly={readonly}
dynamic={dynamic}
value={myValue}

View file

@ -2,11 +2,9 @@ import { cloneDeep } from "lodash";
import { useEffect, useState } from "react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
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";
@ -28,6 +26,8 @@ import {
TabsTrigger,
} from "../../components/ui/tabs";
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
import ExportModal from "../../modals/exportModal";
import { Case } from "../../shared/components/caseComponent";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import { codeTabsPropsType } from "../../types/components";
@ -37,9 +37,14 @@ import {
hasDuplicateKeys,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import AccordionComponent from "../accordionComponent";
import DictComponent from "../dictComponent";
import IconComponent from "../genericIconComponent";
import InputComponent from "../inputComponent";
import KeypairListComponent from "../keypairListComponent";
import ShadTooltip from "../shadTooltipComponent";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
export default function CodeTabsComponent({
flow,
@ -48,15 +53,14 @@ export default function CodeTabsComponent({
setActiveTab,
isMessage,
tweaks,
setActiveTweaks,
activeTweaks,
allowExport = false,
}: codeTabsPropsType) {
const [isCopied, setIsCopied] = useState<Boolean>(false);
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
const [openAccordion, setOpenAccordion] = useState<string[]>([]);
const dark = useDarkStore((state) => state.dark);
const unselectAll = useFlowStore((state) => state.unselectAll);
const setNodes = useFlowStore((state) => state.setNodes);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
@ -85,6 +89,10 @@ export default function CodeTabsComponent({
});
};
const type = (node, templateParam) => {
return node.data.node.template[templateParam].type;
};
const downloadAsFile = () => {
const fileExtension = tabs[activeTab].language || ".txt";
const suggestedFileName = `${"generated-code."}${fileExtension}`;
@ -107,34 +115,16 @@ export default function CodeTabsComponent({
URL.revokeObjectURL(url);
};
function openAccordions() {
let accordionsToOpen: string[] = [];
tweaks?.tweak!.current.forEach((el) => {
Object.keys(el).forEach((key) => {
if (Object.keys(el[key]).length > 0) {
accordionsToOpen.push(key);
setOpenAccordion(accordionsToOpen);
}
});
});
if (accordionsToOpen.length == 0) {
setOpenAccordion([]);
}
}
return (
<Tabs
value={activeTab}
className={
"api-modal-tabs " +
"api-modal-tabs inset-0 m-0 " +
(isMessage ? "dark " : "") +
(dark && isMessage ? "bg-background" : "")
}
onValueChange={(value) => {
setActiveTab(value);
if (value === "3") {
openAccordions();
}
}}
>
<div className="api-modal-tablist-div">
@ -155,27 +145,68 @@ export default function CodeTabsComponent({
) : (
<div></div>
)}
{Number(activeTab) < 4 && (
<div className="float-right mx-1 mb-1 mt-2 flex gap-2">
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={copyToClipboard}
<div className="float-right mx-1 mb-1 mt-2 flex gap-2">
{tweaks && (
<div
className={
Number(activeTab) > 2
? "hidden"
: "relative top-[2.5px] flex gap-2"
}
>
{isCopied ? (
<IconComponent name="Check" className="h-4 w-4" />
) : (
<IconComponent name="Clipboard" className="h-4 w-4" />
)}
{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"
onClick={downloadAsFile}
>
<IconComponent name="Download" className="h-5 w-5" />
</button>
</div>
)}
<Switch
style={{
transform: `scaleX(${0.7}) scaleY(${0.7})`,
}}
id="tweaks-switch"
onCheckedChange={setActiveTweaks}
autoFocus={false}
/>
<Label
className={
"relative right-1 top-[4px] text-xs font-medium text-gray-500 dark:text-gray-300 " +
(activeTweaks
? "font-bold text-black dark:text-white"
: "font-medium")
}
htmlFor="tweaks-switch"
>
Tweaks
</Label>
</div>
)}
{allowExport && (
<ExportModal>
<div className="flex cursor-pointer items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300">
<IconComponent name="FileDown" className="h-4 w-4" />
Export Flow
</div>
</ExportModal>
)}
{Number(activeTab) < 4 && (
<>
<button
className="flex items-center gap-1.5 rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
onClick={copyToClipboard}
>
{isCopied ? (
<IconComponent name="Check" className="h-4 w-4" />
) : (
<IconComponent name="Clipboard" className="h-4 w-4" />
)}
{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"
onClick={downloadAsFile}
>
<IconComponent name="Download" className="h-5 w-5" />
</button>
</>
)}
</div>
</div>
{tabs.map((tab, idx) => (
@ -193,9 +224,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,26 +236,28 @@ 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) => (
<div className="px-3" key={i}>
{tweaks?.tweaksList!.current.includes(
node["data"]["id"]
) && (
{tweaks?.tweaksList!.includes(node["data"]["id"]) && (
<AccordionComponent
trigger={node["data"]["id"]}
open={openAccordion}
trigger={
<ShadTooltip
side="top"
styleClasses="z-50"
content={node["data"]["id"]}
>
<div>{node["data"]["node"]["display_name"]}</div>
</ShadTooltip>
}
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>
@ -242,118 +275,82 @@ export default function CodeTabsComponent({
.show &&
LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField]
.type
)
.type,
),
)
.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>
<TableCell className="p-0 text-xs text-foreground">
<div className="m-auto w-[250px]">
{node.data.node.template[
templateField
].type === "str" &&
!node.data.node.template[
templateField
].options ? (
<div className="mx-auto">
{node.data.node.template[
<Case
condition={
type(node, templateField) ===
"str" &&
!node.data.node.template[
templateField
].list ? (
<InputListComponent
editNode={true}
disabled={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;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
);
}}
/>
) : node.data.node.template[
].options
}
>
<Case
condition={
node.data.node.template[
templateField
].multiline ? (
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node
.template[
]?.list
}
>
<InputListComponent
componentName={templateField}
editNode={true}
disabled={false}
value={
!node.data.node.template[
templateField
].value ||
node.data.node.template[
templateField
].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;
});
tweaks.buildTweakObject!(
node["data"]["id"],
target,
node.data.node
.template[
templateField
]
);
}}
/>
</div>
) : (
<InputComponent
editNode={true}
disabled={false}
password={
].value
}
onChange={(target) => {
setData((old) => {
let newInputList =
cloneDeep(old);
newInputList![
i
].data.node.template[
templateField
].value = target;
return newInputList;
});
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
].password ?? false
}
],
);
}}
/>
</Case>
<Case
condition={
node.data.node.template[
templateField
].multiline
}
>
<div>
<TextAreaComponent
disabled={false}
editNode={true}
value={
!node.data.node.template[
templateField
@ -378,20 +375,77 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
],
);
}}
/>
)}
</div>
) : node.data.node.template[
templateField
].type === "bool" ? (
</div>
</Case>
<Case
condition={
!node.data.node.template[
templateField
].multiline &&
!node.data.node.template[
templateField
].list
}
>
<InputComponent
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;
});
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
],
);
}}
/>
</Case>
</Case>
<Case
condition={
type(node, templateField) ===
"bool"
}
>
<div className="ml-auto">
{" "}
<ToggleShadComponent
@ -411,21 +465,26 @@ export default function CodeTabsComponent({
].value = e;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
e,
node.data.node.template[
templateField
]
],
);
}}
size="small"
disabled={false}
/>
</div>
) : node.data.node.template[
templateField
].type === "file" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"file"
}
>
<div className="mx-auto">
<InputFileComponent
editNode={true}
@ -442,7 +501,7 @@ export default function CodeTabsComponent({
].fileTypes
}
onFileChange={(
value: any
value: any,
) => {
node.data.node.template[
templateField
@ -450,9 +509,14 @@ export default function CodeTabsComponent({
}}
></InputFileComponent>
</div>
) : node.data.node.template[
templateField
].type === "float" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"float"
}
>
<div className="mx-auto">
<FloatComponent
disabled={false}
@ -485,26 +549,30 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
],
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "str" &&
node.data.node.template[
templateField
].options ? (
</Case>
<Case
condition={
type(node, templateField) ===
"str" &&
node.data.node.template[
templateField
].options
}
>
<div className="mx-auto">
<Dropdown
editNode={true}
apiModal={true}
options={
node.data.node.template[
templateField
@ -521,12 +589,12 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
],
);
}}
value={
@ -543,13 +611,23 @@ export default function CodeTabsComponent({
}
></Dropdown>
</div>
) : node.data.node.template[
templateField
].type === "int" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"int"
}
>
<div className="mx-auto">
<IntComponent
disabled={false}
editNode={true}
rangeSpec={
node.data.node.template[
templateField
].rangeSpec
}
value={
!node.data.node.template[
templateField
@ -573,19 +651,24 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
],
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "prompt" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"prompt"
}
>
<div className="mx-auto">
<PromptAreaComponent
readonly={true}
@ -614,19 +697,24 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
],
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "code" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"code"
}
>
<div className="mx-auto">
<CodeAreaComponent
disabled={false}
@ -655,19 +743,24 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
],
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "dict" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"dict"
}
>
<div className="mx-auto overflow-auto custom-scroll">
<KeypairListComponent
disabled={false}
@ -684,7 +777,11 @@ export default function CodeTabsComponent({
node.data.node!
.template[
templateField
].value
].value,
type(
node,
templateField,
),
)
}
duplicateKey={
@ -693,15 +790,15 @@ export default function CodeTabsComponent({
onChange={(target) => {
const valueToNumbers =
convertValuesToNumbers(
target
target,
);
node.data.node!.template[
templateField
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(
valueToNumbers
)
valueToNumbers,
),
);
setData((old) => {
let newInputList =
@ -713,19 +810,29 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
],
);
}}
isList={
node.data.node!.template[
templateField
]?.list ?? false
}
/>
</div>
) : node.data.node.template[
templateField
].type === "NestedDict" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"NestedDict"
}
>
<div className="mx-auto">
<DictComponent
disabled={false}
@ -733,9 +840,9 @@ export default function CodeTabsComponent({
value={
node.data.node!.template[
templateField
].value.toString() === "{}"
].value?.toString() === "{}"
? {
yourkey: "value",
// yourkey: "value",
}
: node.data.node!
.template[
@ -753,23 +860,26 @@ export default function CodeTabsComponent({
].value = target;
return newInputList;
});
tweaks.buildTweakObject!(
tweaks?.buildTweakObject!(
node["data"]["id"],
target,
node.data.node.template[
templateField
]
],
);
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</Case>
<Case
condition={
type(node, templateField) ===
"Any"
}
>
<>-</>
</Case>
</div>
</TableCell>
</TableRow>
@ -781,7 +891,7 @@ export default function CodeTabsComponent({
</AccordionComponent>
)}
{tweaks?.tweaksList!.current.length === 0 && (
{tweaks?.tweaksList!.length === 0 && (
<>
<div className="pt-3">
No tweaks are available for this flow.

View file

@ -26,9 +26,9 @@ 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"
href="https://github.com/langflow-ai/langflow/issues"
target="_blank"
rel="noopener noreferrer"
className="font-medium hover:underline "
@ -37,7 +37,7 @@ export default function CrashErrorComponent({
</a>{" "}
page.
<br></br>
Thank you!
Thank you!
</p>
</div>
</CardContent>
@ -47,7 +47,7 @@ export default function CrashErrorComponent({
<Button onClick={resetErrorBoundary}>Restart Langflow</Button>
<a
href="https://github.com/logspace-ai/langflow/issues/new"
href="https://github.com/langflow-ai/langflow/issues/new"
target="_blank"
rel="noopener noreferrer"
>

View file

@ -0,0 +1,26 @@
export const convertCSVToData = (csvFile, csvSeparator: string) => {
const lines = csvFile.data.trim().split("\n");
const headers = lines[0].trim().split(csvSeparator);
const initialRowData: any = [];
const initialColDefs = headers.map((header) => ({
field: header.trim(),
wrapText: true,
autoHeight: true,
height: "100%",
}));
for (let i = 1; i < lines.length; i++) {
const data = lines[i].trim().split(csvSeparator);
const rowDataEntry: any = {};
for (let j = 0; j < headers.length; j++) {
const value = isNaN(data[j]) ? data[j] : parseFloat(data[j]);
rowDataEntry[headers[j].trim()] = value;
}
initialRowData.push(rowDataEntry);
}
return { rowData: initialRowData, colDefs: initialColDefs };
};

View file

@ -0,0 +1,135 @@
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
import { useEffect, useMemo, useState } from "react";
import {
CSVError,
CSVNoDataError,
CSVViewErrorTitle,
} from "../../constants/constants";
import { useDarkStore } from "../../stores/darkStore";
import { FlowPoolObjectType } from "../../types/chat";
import { NodeType } from "../../types/flow";
import ForwardedIconComponent from "../genericIconComponent";
import TableComponent from "../tableComponent";
import Loading from "../ui/loading";
import { convertCSVToData } from "./helpers/convert-data-function";
function CsvOutputComponent({
csvNode,
flowPool,
}: {
csvNode: NodeType;
flowPool: FlowPoolObjectType;
}) {
const csvNodeArtifacts = flowPool?.data?.artifacts?.repr;
const jsonString = csvNodeArtifacts?.replace(/'/g, '"');
let file = null;
try {
file = JSON?.parse(jsonString) || "";
} catch (e) {
console.log("Error parsing JSON");
}
if (!file) {
return (
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
<div className="align-center flex w-full justify-center gap-2">
<ForwardedIconComponent name="Table" />
{CSVViewErrorTitle}
</div>
<div className="align-center flex w-full justify-center">
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
<div className="langflow-chat-desc-span">{CSVError}</div>
</div>
</div>
</div>
);
}
const separator = csvNode?.data?.node?.template?.separator?.value || ",";
const dark = useDarkStore.getState().dark;
const [rowData, setRowData] = useState([]);
const [colDefs, setColDefs] = useState([]);
const [status, setStatus] = useState("loading");
const defaultColDef = useMemo(() => {
return {
width: 200,
editable: true,
filter: true,
};
}, []);
useEffect(() => {
setStatus("loading");
if (file) {
const { rowData: data, colDefs: columns } = convertCSVToData(
file,
separator,
);
setRowData(data);
setColDefs(columns);
setTimeout(() => {
setStatus("loaded");
}, 1000);
} else {
setStatus("nodata");
}
}, [separator]);
return (
<div className=" h-full rounded-md border bg-muted">
{status === "nodata" && (
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
<div className="align-center flex w-full justify-center gap-2">
<ForwardedIconComponent name="Table" />
{CSVViewErrorTitle}
</div>
<div className="align-center flex w-full justify-center">
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
<div className="langflow-chat-desc-span">{CSVNoDataError}</div>
</div>
</div>
</div>
)}
{status === "error" && (
<div className=" align-center flex h-full w-full flex-col items-center justify-center gap-5">
<div className="align-center flex w-full justify-center gap-2">
<ForwardedIconComponent name="Table" />
{CSVViewErrorTitle}
</div>
<div className="align-center flex w-full justify-center">
<div className="langflow-chat-desc align-center flex justify-center px-6 py-8">
<div className="langflow-chat-desc-span">{CSVError}</div>
</div>
</div>
</div>
)}
{status === "loaded" && (
<div
className={`${dark ? "ag-theme-balham-dark" : "ag-theme-balham"}`}
style={{ height: "100%", width: "100%" }}
>
<TableComponent
rowData={rowData}
columnDefs={colDefs}
defaultColDef={defaultColDef}
scrollbarWidth={8}
overlayNoRowsTemplate="No data available"
/>
</div>
)}
{status === "loading" && (
<div className=" flex h-full w-full items-center justify-center align-middle">
<Loading />
</div>
)}
</div>
);
}
export default CsvOutputComponent;

View file

@ -0,0 +1,21 @@
export default function DateReader({
date: dateString,
}: {
date: string;
}): JSX.Element {
const date = new Date(dateString);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); // Months are 0-indexed in JavaScript
const day = String(date.getDate()).padStart(2, "0");
const hours = date.getHours();
const minutes = String(date.getMinutes()).padStart(2, "0");
const ampm = hours >= 12 ? "PM" : "AM";
const hours12 = hours > 12 ? hours - 12 : hours === 0 ? 12 : hours; // Convert to 12-hour format
const formattedDate = `${year}-${month}-${day} ${hours12}:${minutes} ${ampm}`;
return <span>{formattedDate}</span>;
}

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

@ -1,139 +1,136 @@
import { Listbox, Transition } from "@headlessui/react";
import { Fragment, useEffect, useState } from "react";
import { PopoverAnchor } from "@radix-ui/react-popover";
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,
PopoverContent,
PopoverContentWithoutPortal,
PopoverTrigger,
} from "../ui/popover";
export default function Dropdown({
disabled,
isLoading,
value,
options,
onSelect,
editNode = false,
numberOfOptions = 0,
apiModal = false,
id = "",
children,
}: DropDownComponentType): JSX.Element {
let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value
);
const [open, setOpen] = useState(children ? true : false);
useEffect(() => {
setInternalValue(value === "" || !value ? "Choose an option" : value);
}, [value]);
const refButton = useRef<HTMLButtonElement>(null);
const PopoverContentDropdown = children
? PopoverContent
: PopoverContentWithoutPortal;
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>
<Popover open={open} onOpenChange={children ? () => {} : setOpen}>
{children ? (
<PopoverAnchor>{children}</PopoverAnchor>
) : (
<PopoverTrigger asChild>
<Button
disabled={disabled}
variant="primary"
size="xs"
role="combobox"
ref={refButton}
aria-expanded={open}
data-testid={`${id ?? ""}`}
className={cn(
editNode
? "dropdown-component-outline"
: "dropdown-component-false-outline",
"w-full justify-between font-normal",
editNode ? "input-edit-node" : "py-2",
)}
>
<span data-testid={`value-dropdown-` + id}>
{value &&
value !== "" &&
options.find((option) => option === value)
? options.find((option) => option === value)
: "Choose an option..."}
</span>
<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}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</>
<ForwardedIconComponent
name="ChevronsUpDown"
className="ml-2 h-4 w-4 shrink-0 opacity-50"
/>
</Button>
</PopoverTrigger>
)}
</Listbox>
<PopoverContentDropdown
side="bottom"
avoidCollisions={!!children}
className="nocopy nowheel nopan nodelete nodrag noundo p-0"
style={
children
? {}
: { 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",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContentDropdown>
</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

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

@ -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 IconComponent from "../genericIconComponent";
import ShadTooltip from "../shadTooltipComponent";
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

@ -16,7 +16,7 @@ export default function FloatComponent({
// Clear component state
useEffect(() => {
if (disabled && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled]);

View file

@ -1,36 +1,72 @@
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"];
import { useEffect, useState } from "react";
const style = {
strokeWidth: strokeWidth ?? 1.5,
...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }),
};
export const ForwardedIconComponent = memo(
forwardRef(
(
{
name,
className,
iconColor,
stroke,
strokeWidth,
id = "",
}: IconComponentProps,
ref,
) => {
const [showFallback, setShowFallback] = useState(false);
return (
<TargetIcon
className={className}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
);
}
useEffect(() => {
const timer = setTimeout(() => {
setShowFallback(true);
}, 30);
return () => clearTimeout(timer);
}, []);
let TargetIcon = nodeIconsLucide[name];
if (!TargetIcon) {
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 }),
};
if (!TargetIcon) {
return null; // Render nothing until the icon is loaded
}
const fallback = showFallback ? (
<div className={cn(className, "flex items-center justify-center")}>
<Loading />
</div>
) : (
<div className={className}></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

@ -2,7 +2,7 @@ import { gradients } from "../../utils/styleUtils";
export default function GradientChooserComponent({ value, onChange }) {
return (
<div className="flex flex-wrap items-center justify-center gap-4">
<div className="flex flex-wrap items-center justify-start gap-2">
{gradients.map((gradient, idx) => (
<div
onClick={() => {

View file

@ -8,40 +8,48 @@ import {
} from "../../../ui/dropdown-menu";
import { useNavigate } from "react-router-dom";
import { Node } from "reactflow";
import { savedHover } from "../../../../constants/constants";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { SAVED_HOVER } from "../../../../constants/constants";
import ExportModal from "../../../../modals/exportModal";
import FlowLogsModal from "../../../../modals/flowLogsModal";
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 ShadTooltip from "../../../shadTooltipComponent";
import { Button } from "../../../ui/button";
export const MenuBar = ({
removeFunction,
}: {
removeFunction: (nodes: Node[]) => void;
}): JSX.Element => {
export const MenuBar = ({}: {}): JSX.Element => {
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 [openLogs, setOpenLogs] = useState(false);
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> });
}
@ -58,20 +66,14 @@ export const MenuBar = ({
return currentFlow ? (
<div className="round-button-div">
<button
onClick={() => {
removeFunction(n);
navigate(-1);
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</button>
<div className="header-menu-bar">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button asChild variant="primary" size="sm">
<div className="header-menu-bar-display">
<div className="header-menu-flow-name">{currentFlow.name}</div>
<div className="header-menu-flow-name" data-testid="flow_name">
{currentFlow.name}
</div>
<IconComponent name="ChevronDown" className="h-4 w-4" />
</div>
</Button>
@ -87,6 +89,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={() => {
@ -100,7 +111,43 @@ export const MenuBar = ({
/>
Settings
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setOpenLogs(true);
}}
className="cursor-pointer"
>
<IconComponent
name="ScrollText"
className="header-menu-options "
/>
Logs
</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();
@ -109,6 +156,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={() => {
@ -118,6 +176,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>
@ -125,30 +194,33 @@ export const MenuBar = ({
open={openSettings}
setOpen={setOpenSettings}
></FlowSettingsModal>
<FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal>
</div>
<ShadTooltip
content={
savedHover +
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>
{(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,15 +1,19 @@
import { useContext, useEffect } from "react";
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
import { useContext } from "react";
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";
import {
LOCATIONS_TO_RETURN,
USER_PROJECTS_HEADER,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { Node } from "reactflow";
import useAlertStore from "../../stores/alertStore";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { useLocationStore } from "../../stores/locationStore";
import { useStoreStore } from "../../stores/storeStore";
import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent";
@ -28,80 +32,94 @@ import MenuBar from "./components/menuBar";
export default function Header(): JSX.Element {
const notificationCenter = useAlertStore((state) => state.notificationCenter);
const location = useLocation();
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
const navigate = useNavigate();
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);
const stars = useDarkStore((state) => state.stars);
useEffect(() => {
if (dark) {
document.getElementById("body")!.classList.add("dark");
} else {
document.getElementById("body")!.classList.remove("dark");
}
window.localStorage.setItem("isDark", dark.toString());
}, [dark]);
const routeHistory = useLocationStore((state) => state.routeHistory);
async function checkForChanges(nodes: Node[]): Promise<void> {
async function checkForChanges(): Promise<void> {
if (nodes.length === 0) {
await removeFlow(id!);
}
}
const redirectToLastLocation = () => {
const lastFlowVisitedIndex = routeHistory
.reverse()
.findIndex(
(path) => path.includes("/flow/") && path !== location.pathname
);
const lastFlowVisited = routeHistory[lastFlowVisitedIndex];
lastFlowVisited && !location.pathname.includes("/flow")
? navigate(lastFlowVisited)
: navigate("/all");
};
const visitedFlowPathBefore = () => {
const lastThreeVisitedPaths = routeHistory.slice(-3);
return lastThreeVisitedPaths.some((path) => path.includes("/flow/"));
};
const showArrowReturnIcon =
LOCATIONS_TO_RETURN.some((path) => location.pathname.includes(path)) &&
visitedFlowPathBefore();
return (
<div className="header-arrangement">
<div className="header-start-display lg:w-[30%]">
<Link to="/" onClick={() => checkForChanges(n)}>
<Link to="/all" className="cursor-pointer" onClick={checkForChanges}>
<span className="ml-4 text-2xl">⛓️</span>
</Link>
<MenuBar removeFunction={checkForChanges} />
{showArrowReturnIcon && (
<button
onClick={() => {
checkForChanges();
redirectToLastLocation();
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</button>
)}
<MenuBar />
</div>
<div className="round-button-div">
<Link to="/">
<Button
className="gap-2"
variant={
location.pathname === "/flows" ||
location.pathname === "/all" ||
location.pathname === "/components"
? "primary"
: "secondary"
}
size="sm"
onClick={() => {
checkForChanges(n);
}}
onClick={checkForChanges}
>
<IconComponent name="Home" className="h-4 w-4" />
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
</Button>
</Link>
{/* <Link to="/community">
<Button
className="gap-2"
variant={
location.pathname === "/community" ? "primary" : "secondary"
}
size="sm"
>
<IconComponent name="Users2" className="h-4 w-4" />
<div className="flex-1">Community Examples</div>
</Button>
</Link> */}
{hasStore && (
<Link to="/store">
<Button
className="gap-2"
variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm"
onClick={() => {
checkForChanges(n);
}}
onClick={checkForChanges}
data-testid="button-store"
>
<IconComponent name="Store" className="h-4 w-4" />
<div className="flex-1">Store</div>
@ -112,7 +130,7 @@ export default function Header(): JSX.Element {
<div className="header-end-division lg:w-[30%]">
<div className="header-end-display">
<a
href="https://github.com/logspace-ai/langflow"
href="https://github.com/langflow-ai/langflow"
target="_blank"
rel="noreferrer"
className="header-github-link gap-2"
@ -127,7 +145,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"
@ -163,7 +181,7 @@ export default function Header(): JSX.Element {
/>
</div>
</AlertDropdown>
{!autoLogin && (
{autoLogin && (
<button
onClick={() => {
navigate("/account/api-keys");
@ -175,50 +193,57 @@ export default function Header(): JSX.Element {
/>
</button>
)}
{!autoLogin && (
<>
<Separator orientation="vertical" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className={
"h-7 w-7 rounded-full focus-visible:outline-0 " +
(userData?.profile_image ??
gradients[
parseInt(userData?.id ?? "", 30) % gradients.length
])
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
{isAdmin && (
<>
<Separator orientation="vertical" />
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
data-testid="user-profile-settings"
className={
"h-7 w-7 rounded-full focus-visible:outline-0 " +
(userData?.profile_image ??
(userData?.id
? gradients[
parseInt(userData?.id ?? "", 30) % gradients.length
]
: "bg-gray-500"))
}
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>General</DropdownMenuLabel>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/settings")}
>
Settings
</DropdownMenuItem>
{!autoLogin && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>My Account</DropdownMenuLabel>
{isAdmin && (
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/admin")}
>
Admin Page
</DropdownMenuItem>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/admin")}
onClick={() => {
logout();
}}
>
Admin Page
Sign Out
</DropdownMenuItem>
)}
<DropdownMenuItem
className="cursor-pointer"
onClick={() => navigate("/account/settings")}
>
Profile Settings
</DropdownMenuItem>
<DropdownMenuItem
className="cursor-pointer"
onClick={() => {
logout();
}}
>
Sign Out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</>
</div>
</div>
</div>

View file

@ -0,0 +1,61 @@
import { useEffect, useRef, useState } from "react";
export default function HorizontalScrollFadeComponent({
children,
isFolder = true,
}) {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const fadeContainerRef = useRef<HTMLDivElement>(null);
const [divWidth, setDivWidth] = useState<number>(0);
useEffect(() => {
const handleResize = () => {
if (scrollContainerRef.current) {
setDivWidth(scrollContainerRef.current.clientWidth);
}
};
window.addEventListener("resize", handleResize);
handleResize(); // call the function at start to get the initial width
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
const handleScroll = () => {
if (!scrollContainerRef.current || !fadeContainerRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
const atStart = scrollLeft === 0;
const atEnd = scrollLeft === scrollWidth - clientWidth;
const isScrollable = scrollWidth > clientWidth;
fadeContainerRef.current.classList.toggle(
"fade-left",
isScrollable && !atStart,
);
fadeContainerRef.current.classList.toggle(
"fade-right",
isScrollable && !atEnd,
);
};
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
// Delay the initial scroll event dispatch to ensure correct calculation
scrollContainer.dispatchEvent(new Event("scroll"));
return () => scrollContainer.removeEventListener("scroll", handleScroll);
}
}, [divWidth, children]); // Depend on divWidth
return isFolder ? (
<div className="hidden w-full flex-col gap-2 lg:flex">{children}</div>
) : (
<div ref={fadeContainerRef} className="fade-container flex">
<div ref={scrollContainerRef} className="scroll-container flex gap-2">
{children}
</div>
</div>
);
}

View file

@ -0,0 +1,192 @@
import { PopoverAnchor } from "@radix-ui/react-popover";
import useAlertStore from "../../../../stores/alertStore";
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";
const CustomInputPopover = ({
id,
refInput,
onInputLostFocus,
selectedOption,
setSelectedOption,
selectedOptions,
setSelectedOptions,
value,
autoFocus,
disabled,
setShowOptions,
required,
className,
password,
pwdVisible,
editNode,
placeholder,
onChange,
blurOnEnter,
options,
optionsPlaceholder,
optionButton,
optionsButton,
handleKeyDown,
showOptions,
}) => {
const setErrorData = useAlertStore.getState().setErrorData;
const handleInputChange = (e) => {
if (password) {
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);
};
return (
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
<PopoverAnchor>
<Input
id={id}
ref={refInput}
type="text"
onBlur={onInputLostFocus}
value={
(selectedOption !== "" || !onChange) && setSelectedOption
? selectedOption
: (selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions
? selectedOptions?.join(", ")
: value
}
autoFocus={autoFocus}
disabled={disabled}
onClick={() => {
(((selectedOption !== "" || !onChange) && setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)) &&
setShowOptions(true);
}}
required={required}
className={classNames(
password &&
(!setSelectedOption || selectedOption === "") &&
!pwdVisible &&
value !== ""
? " text-clip password "
: "",
editNode ? " input-edit-node " : "",
password && (setSelectedOption || setSelectedOptions)
? "pr-[62.9px]"
: "",
(!password && (setSelectedOption || setSelectedOptions)) ||
(password && !(setSelectedOption || setSelectedOptions))
? "pr-8"
: "",
className!,
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={handleInputChange}
onKeyDown={(e) => {
handleKeyDown(e);
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.toLowerCase().includes(search.toLowerCase()) ||
value.includes("doNotFilter-")
)
return 1;
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,
);
setSelectedOptions &&
setSelectedOptions(
selectedOptions?.includes(currentValue)
? selectedOptions.filter(
(item) => item !== currentValue,
)
: [...selectedOptions, currentValue],
);
!setSelectedOptions && 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 ||
selectedOptions?.includes(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>
);
};
export default CustomInputPopover;

View file

@ -0,0 +1,167 @@
import { PopoverAnchor } from "@radix-ui/react-popover";
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";
const CustomInputPopoverObject = ({
id,
refInput,
onInputLostFocus,
selectedOption,
setSelectedOption,
selectedOptions,
setSelectedOptions,
value,
autoFocus,
disabled,
setShowOptions,
required,
className,
placeholder,
onChange,
blurOnEnter,
options,
optionsPlaceholder,
optionButton,
optionsButton,
handleKeyDown,
showOptions,
}) => {
const handleInputChange = (e) => {
onChange && onChange(e.target.value);
};
return (
<Popover modal open={showOptions} onOpenChange={setShowOptions}>
<PopoverAnchor>
<Input
id={id}
ref={refInput}
type="text"
onBlur={onInputLostFocus}
value={
(selectedOption !== "" || !onChange) && setSelectedOption
? options.find((option) => option.id === selectedOption)?.name ||
""
: (selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions
? selectedOptions
.map(
(optionId) =>
options.find((option) => option.id === optionId)?.name,
)
.join(", ")
: value
}
autoFocus={autoFocus}
disabled={disabled}
onClick={() => {
(((selectedOption !== "" || !onChange) && setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)) &&
setShowOptions(true);
}}
required={required}
className={classNames(className!)}
placeholder={placeholder}
onChange={handleInputChange}
onKeyDown={(e) => {
handleKeyDown(e);
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
data-testid={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.toLowerCase().includes(search.toLowerCase()) ||
value.includes("doNotFilter-")
)
return 1;
return 0;
}}
>
<CommandInput placeholder={optionsPlaceholder} />
<CommandList>
<CommandGroup defaultChecked={false}>
{options.map((option, index) => (
<CommandItem
className="group"
key={option.id}
value={option.id}
onSelect={(currentValue) => {
setSelectedOption &&
setSelectedOption(
currentValue === selectedOption ? "" : currentValue,
);
setSelectedOptions &&
setSelectedOptions(
selectedOptions?.includes(currentValue)
? selectedOptions.filter(
(item) => item !== currentValue,
)
: [...selectedOptions, currentValue],
);
!setSelectedOptions && 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.id ||
selectedOptions?.includes(option.id)
? "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>
<span data-testid={`option-${index}`}>
{option.name}{" "}
</span>
{/* Display the name property of the option */}
</div>
{optionButton && optionButton(option)}
</div>
</CommandItem>
))}
{optionsButton && optionsButton}
</CommandGroup>
</CommandList>
</Command>
</PopoverContentWithoutPortal>
</Popover>
);
};
export default CustomInputPopoverObject;

View file

@ -2,13 +2,16 @@ import * as Form from "@radix-ui/react-form";
import { useEffect, useRef, useState } from "react";
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 { Input } from "../ui/input";
import CustomInputPopover from "./components/popover";
import CustomInputPopoverObject from "./components/popoverObject";
export default function InputComponent({
autoFocus = false,
onBlur,
value,
value = "",
onChange,
disabled,
required = false,
@ -19,24 +22,43 @@ export default function InputComponent({
className,
id = "",
blurOnEnter = false,
optionsIcon = "ChevronsUpDown",
selectedOption,
setSelectedOption,
selectedOptions = [],
setSelectedOptions,
options = [],
optionsPlaceholder = "Search options...",
optionsButton,
optionButton,
objectOptions,
isObjectOption = false,
name,
onChangeFolderName,
}: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
// Clear component state
const [showOptions, setShowOptions] = useState<boolean>(false);
useEffect(() => {
if (disabled && value !== "") {
onChange("");
if (disabled && value && onChange && value !== "") {
onChange("", true);
}
}, [disabled]);
function onInputLostFocus(event): void {
if (onBlur) onBlur(event);
}
return (
<div className="relative w-full">
{isForm ? (
<Form.Control asChild>
<Input
name={name}
id={"form-" + id}
ref={refInput}
onBlur={onBlur}
onBlur={onInputLostFocus}
autoFocus={autoFocus}
type={password && !pwdVisible ? "password" : "text"}
value={value}
@ -49,11 +71,14 @@ export default function InputComponent({
editNode ? " input-edit-node " : "",
password && editNode ? "pr-8" : "",
password && !editNode ? "pr-10" : "",
className!
className!,
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
onChange(e.target.value);
if (onChangeFolderName) {
return onChangeFolderName(e);
}
onChange && onChange(e.target.value);
}}
onCopy={(e) => {
e.preventDefault();
@ -65,93 +90,153 @@ 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!
<>
{isObjectOption ? (
// Content to render when isObjectOption is true
<CustomInputPopoverObject
refInput={refInput}
handleKeyDown={handleKeyDown}
optionButton={optionButton}
optionsButton={optionsButton}
showOptions={showOptions}
onChange={onChange}
id={`object-${id}`}
onInputLostFocus={onInputLostFocus}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
selectedOptions={selectedOptions}
setSelectedOptions={setSelectedOptions}
options={objectOptions}
value={value}
autoFocus={autoFocus}
disabled={disabled}
setShowOptions={setShowOptions}
required={required}
placeholder={placeholder}
blurOnEnter={blurOnEnter}
optionsPlaceholder={optionsPlaceholder}
className={className}
/>
) : (
<CustomInputPopover
refInput={refInput}
handleKeyDown={handleKeyDown}
optionButton={optionButton}
optionsButton={optionsButton}
showOptions={showOptions}
onChange={onChange}
id={`popover-anchor-${id}`}
onInputLostFocus={onInputLostFocus}
selectedOption={selectedOption}
setSelectedOption={setSelectedOption}
selectedOptions={selectedOptions}
setSelectedOptions={setSelectedOptions}
value={value}
autoFocus={autoFocus}
disabled={disabled}
setShowOptions={setShowOptions}
required={required}
password={password}
pwdVisible={pwdVisible}
editNode={editNode}
placeholder={placeholder}
blurOnEnter={blurOnEnter}
options={options}
optionsPlaceholder={optionsPlaceholder}
className={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();
}}
/>
</>
)}
{password && (
{(setSelectedOption || setSelectedOptions) && (
<span
className={cn(
password && selectedOption === "" ? "right-8" : "right-0",
"absolute inset-y-0 flex items-center pr-2.5",
)}
>
<button
onClick={(e) => {
setShowOptions(!showOptions);
e.preventDefault();
e.stopPropagation();
}}
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 && (!setSelectedOption || selectedOption === "") && (
<button
type="button"
tabIndex={-1}
className={classNames(
"mb-px",
editNode
? "input-component-true-button"
: "input-component-false-button"
: "input-component-false-button",
)}
onClick={(event) => {
event.preventDefault();
setPwdVisible(!pwdVisible);
}}
>
{password &&
(pwdVisible ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={classNames(
editNode
? "input-component-true-svg"
: "input-component-false-svg"
)}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={classNames(
editNode
? "input-component-true-svg"
: "input-component-false-svg"
)}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
))}
{pwdVisible ? (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={classNames(
editNode
? "input-component-true-svg"
: "input-component-false-svg",
)}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className={classNames(
editNode
? "input-component-true-svg"
: "input-component-false-svg",
)}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
)}
</button>
)}
</div>

View file

@ -27,7 +27,7 @@ export default function InputFileComponent({
useEffect(() => {
if (disabled && value !== "") {
setMyValue("");
onChange("");
onChange("", true);
onFileChange("");
}
}, [disabled, onChange]);
@ -106,8 +106,8 @@ export default function InputFileComponent({
editNode
? "input-edit-node input-dialog text-muted-foreground"
: disabled
? "input-disable input-dialog primary-input"
: "input-dialog primary-input text-muted-foreground"
? "input-disable input-dialog primary-input"
: "input-dialog primary-input text-muted-foreground"
}
>
{myValue !== "" ? myValue : "No file"}

View file

@ -0,0 +1,157 @@
import { useEffect } from "react";
import { deleteGlobalVariable } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
import useAlertStore from "../../stores/alertStore";
import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables";
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 unavaliableFields = useGlobalVariablesStore(
(state) => state.unavaliableFields,
);
const removeGlobalVariable = useGlobalVariablesStore(
(state) => state.removeGlobalVariable,
);
const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => {
if (data.node?.template[name])
if (
globalVariablesEntries &&
!globalVariablesEntries.includes(data.node?.template[name].value) &&
data.node?.template[name].load_from_db
) {
setTimeout(() => {
onChange("", true);
setDb(false);
}, 100);
}
}, [globalVariablesEntries]);
useEffect(() => {
if (
!data.node?.template[name].value &&
data.node?.template[name].display_name
) {
if (
unavaliableFields[data.node?.template[name].display_name!] &&
!disabled
) {
setTimeout(() => {
setDb(true);
onChange(unavaliableFields[data.node?.template[name].display_name!]);
}, 100);
}
}
}, [unavaliableFields]);
async function handleDelete(key: string) {
const id = getVariableId(key);
if (id !== undefined) {
await deleteGlobalVariable(id)
.then(() => {
removeGlobalVariable(key);
if (
data?.node?.template[name].value === key &&
data?.node?.template[name].load_from_db
) {
onChange("");
setDb(false);
}
})
.catch(() => {
setErrorData({
title: "Error deleting variable",
list: [cn("ID not found for variable: ", key)],
});
});
} 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 &&
globalVariablesEntries &&
globalVariablesEntries.includes(data?.node?.template[name].value ?? "")
? data?.node?.template[name].value
: ""
}
setSelectedOption={(value) => {
onChange(value);
setDb(value !== "" ? true : false);
}}
onChange={(value, skipSnapshot) => {
onChange(value, skipSnapshot);
setDb(false);
}}
/>
);
}

View file

@ -11,6 +11,8 @@ export default function InputListComponent({
onChange,
disabled,
editNode = false,
componentName,
playgroundDisabled,
}: InputListComponentType): JSX.Element {
useEffect(() => {
if (disabled && value.length > 0 && value[0] !== "") {
@ -23,7 +25,7 @@ export default function InputListComponent({
value = [value];
}
if (!value.length) value = [""];
if (!value?.length) value = [""];
return (
<div
@ -36,7 +38,7 @@ export default function InputListComponent({
return (
<div key={idx} className="flex w-full gap-3">
<Input
disabled={disabled}
disabled={disabled || playgroundDisabled}
type="text"
value={singleValue}
className={editNode ? "input-edit-node" : ""}
@ -46,6 +48,10 @@ export default function InputListComponent({
newInputList[idx] = event.target.value;
onChange(newInputList);
}}
data-testid={
`input-list-input${editNode ? "-edit" : ""}_${componentName}-` +
idx
}
/>
{idx === value.length - 1 ? (
<button
@ -54,6 +60,12 @@ export default function InputListComponent({
newInputList.push("");
onChange(newInputList);
}}
data-testid={
`input-list-plus-btn${
editNode ? "-edit" : ""
}_${componentName}-` + idx
}
disabled={disabled || playgroundDisabled}
>
<IconComponent
name="Plus"
@ -62,15 +74,25 @@ export default function InputListComponent({
</button>
) : (
<button
data-testid={
`input-list-minus-btn${
editNode ? "-edit" : ""
}_${componentName}-` + idx
}
onClick={() => {
let newInputList = _.cloneDeep(value);
newInputList.splice(idx, 1);
onChange(newInputList);
}}
disabled={disabled || playgroundDisabled}
>
<IconComponent
name="X"
className="h-4 w-4 hover:text-status-red"
className={`h-4 w-4 ${
disabled || playgroundDisabled
? ""
: "hover:text-accent-foreground"
}`}
/>
</button>
)}

View file

@ -9,16 +9,17 @@ 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(() => {
if (disabled && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled, onChange]);
@ -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,14 +12,21 @@ export default function KeypairListComponent({
disabled,
editNode = false,
duplicateKey,
isList = true,
}: KeyPairListComponentType): JSX.Element {
useEffect(() => {
if (disabled && value.length > 0 && value[0] !== "") {
onChange([""]);
onChange([{ "": "" }]);
}
}, [disabled]);
const ref = useRef(value.length === 0 ? [{ "": "" }] : value);
const checkValueType = (value) => {
return Array.isArray(value) ? value : [value];
};
const ref = useRef<any>([]);
ref.current =
!value || value?.length === 0 ? [{ "": "" }] : checkValueType(value);
useEffect(() => {
if (JSON.stringify(value) !== JSON.stringify(ref.current)) {
@ -79,6 +86,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 +95,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 +119,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 +142,8 @@ export default function KeypairListComponent({
className="h-4 w-4 hover:text-status-red"
/>
</button>
) : (
""
)}
</div>
);

View file

@ -1,85 +0,0 @@
import * as base64js from "base64-js";
import { useState } from "react";
import IconComponent from "../../../components/genericIconComponent";
import { fileCardPropsType } from "../../../types/components";
export default function FileCard({
fileName,
content,
fileType,
}: fileCardPropsType): JSX.Element {
const handleDownload = (): void => {
const byteArray = new Uint8Array(base64js.toByteArray(content));
const blob = new Blob([byteArray], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = fileName + ".png";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const [isHovered, setIsHovered] = useState(false);
function handleMouseEnter(): void {
setIsHovered(true);
}
function handleMouseLeave(): void {
setIsHovered(false);
}
if (fileType === "image") {
return (
<div
className="relative h-1/4 w-1/4"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<img
src={`data:image/png;base64,${content}`}
alt="generated image"
className="h-full w-full rounded-lg"
/>
{isHovered && (
<div className={`file-card-modal-image-div `}>
<button
className="file-card-modal-image-button "
onClick={handleDownload}
>
<IconComponent
name="DownloadCloud"
className="h-5 w-5 text-current hover:scale-110"
/>
</button>
</div>
)}
</div>
);
}
return (
<button onClick={handleDownload} className="file-card-modal-button">
<div className="file-card-modal-div">
ooooooooooooooo{" "}
{fileType === "image" ? (
<img
src={`data:image/png;base64,${content}`}
alt=""
className="h-8 w-8"
/>
) : (
<IconComponent name="File" className="h-8 w-8" />
)}
<div className="file-card-modal-footer">
{" "}
<div className="file-card-modal-name">{fileName}</div>
<div className="file-card-modal-type">{fileType}</div>
</div>
<IconComponent
name="DownloadCloud"
className="ml-auto h-6 w-6 text-current"
/>
</div>
</button>
);
}

View file

@ -0,0 +1,7 @@
export default function NumberReader({
number,
}: {
number: number;
}): JSX.Element {
return <span>{number}</span>;
}

View file

@ -0,0 +1,13 @@
import DictAreaModal from "../../modals/dictAreaModal";
export default function ObjectRender({ object }: { object: any }): JSX.Element {
//TODO check object type
return (
<DictAreaModal value={object}>
<div className="flex h-full w-full items-center align-middle transition-all hover:scale-105">
<div className="truncate">{JSON.stringify(object)}</div>
</div>
</DictAreaModal>
);
}

View file

@ -0,0 +1,19 @@
import { PDFCheckFlow, PDFLoadErrorTitle } from "../../../constants/constants";
import IconComponent from "../../genericIconComponent";
export default function Error(): JSX.Element {
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
<div className="chat-alert-box">
<span className="flex gap-2">
<IconComponent name="FileX2" />
<span className="langflow-chat-span">{PDFLoadErrorTitle}</span>
</span>
<br />
<div className="langflow-chat-desc">
<span className="langflow-chat-desc-span">{PDFCheckFlow} </span>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,155 @@
import { useEffect, useRef, useState } from "react";
import { Document, Page, pdfjs } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css";
import IconComponent from "../genericIconComponent";
import Loading from "../ui/loading";
import Error from "./Error";
import NoDataPdf from "./noData";
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
export default function PdfViewer({ pdf }: { pdf: string }): JSX.Element {
const [numPages, setNumPages] = useState(-1);
const [pageNumber, setPageNumber] = useState(1);
const [scale, setScale] = useState(1);
const [width, setWidth] = useState<number | undefined>(undefined);
const [showControl, setShowControl] = useState(false);
const container = useRef<null | HTMLDivElement>(null);
//shortcuts to change page
useEffect(() => {
function handleKeyDown(event: KeyboardEvent) {
if (event.key === "ArrowLeft") {
if (pageNumber > 1) previousPage();
} else if (event.key === "ArrowRight") {
if (pageNumber < numPages) nextPage();
}
}
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, [pageNumber]);
function onDocumentLoadSuccess({ numPages }) {
setNumPages(numPages);
setPageNumber(1);
}
function changePage(offset) {
setPageNumber((prevPageNumber) => prevPageNumber + offset);
}
function previousPage() {
changePage(-1);
}
function nextPage() {
changePage(1);
}
//set handle scale in % to real number
function handleScaleChange(e) {
//check if e is a number
if (isNaN(e) || e < 0.1) return;
// round to 2 decimal places
e = Math.round(e * 10) / 10;
setScale(e);
}
function zoomIn() {
handleScaleChange(scale + 0.1);
}
function zoomOut() {
if (scale > 0.1) handleScaleChange(scale - 0.1);
}
function handlePageLoad(page) {
if (!container.current) return;
const containerWidth = container.current.clientWidth;
const pageWidth = page.width;
if (containerWidth > pageWidth) {
setWidth(containerWidth - 10);
}
}
return (
<div
ref={container}
onMouseEnter={(_) => setShowControl(true)}
onMouseLeave={(_) => setShowControl(false)}
className="flex h-full w-full flex-col items-center justify-end overflow-clip rounded-lg border border-border"
>
<div className={"h-full min-h-0 w-full overflow-auto custom-scroll"}>
<Document
loading={
<div className="flex h-full w-full items-center justify-center align-middle">
<Loading />
</div>
}
onLoadSuccess={onDocumentLoadSuccess}
file={pdf}
noData={<NoDataPdf />}
error={<Error />}
className="h-full w-full"
>
<Page
width={width}
onLoadSuccess={handlePageLoad}
scale={scale}
renderTextLayer
pageNumber={pageNumber}
className={"h-full max-h-0 w-full"}
/>
</Document>
</div>
<div
className={
"absolute z-50 pb-5 " + (showControl && numPages > 0 ? "" : " hidden")
}
>
<div className=" flex w-min items-center justify-center gap-0.5 rounded-xl bg-secondary px-2 align-middle">
<button
type="button"
disabled={pageNumber <= 1}
onClick={previousPage}
>
<IconComponent
name={"ChevronLeft"}
className="h-6 w-6"
></IconComponent>
</button>
<p>
{pageNumber || (numPages ? 1 : "--")}/{numPages || "--"}
</p>
<button
type="button"
disabled={pageNumber >= numPages}
onClick={nextPage}
>
<IconComponent
name={"ChevronRight"}
className="h-6 w-6"
></IconComponent>
</button>
<p className="px-2">|</p>
<button type="button" onClick={zoomOut}>
<IconComponent name={"ZoomOut"} className="h-6 w-6"></IconComponent>
</button>
<input
type="number"
step={0.1}
className="w-6 border-b bg-transparent text-center arrow-hide"
onChange={(e) => handleScaleChange(e.target.value)}
value={scale}
/>
<button type="button" onClick={zoomIn}>
<IconComponent name={"ZoomIn"} className="h-6 w-6"></IconComponent>
</button>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,17 @@
import { PDFErrorTitle, PDFLoadError } from "../../../constants/constants";
export default function NoDataPdf(): JSX.Element {
return (
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
<div className="chat-alert-box">
<span>
📄 <span className="langflow-chat-span">{PDFErrorTitle}</span>
</span>
<br />
<div className="langflow-chat-desc">
<span className="langflow-chat-desc-span">{PDFLoadError} </span>
</div>
</div>
</div>
);
}

View file

@ -18,7 +18,7 @@ export default function PromptAreaComponent({
}: PromptAreaComponentType): JSX.Element {
useEffect(() => {
if (disabled && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled]);
@ -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

@ -0,0 +1,34 @@
import { ColDef, ColGroupDef } from "ag-grid-community";
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-balham.css"; // Optional Theme applied to the grid
import { FlowPoolObjectType } from "../../types/chat";
import TableComponent from "../tableComponent";
import { extractColumnsFromRows } from "../../utils/utils";
function RecordsOutputComponent({
flowPool,
pagination,
}: {
flowPool: FlowPoolObjectType;
pagination: boolean;
}) {
const rows = flowPool?.data?.artifacts?.records ?? [];
const columns = extractColumnsFromRows(rows, "union");
const columnDefs = columns.map((col, idx) => ({
...col,
resizable: idx !== columns.length - 1,
flex: idx !== columns.length - 1 ? 1 : 2,
})) as (ColDef<any> | ColGroupDef<any>)[];
return (
<TableComponent
overlayNoRowsTemplate="No data available"
suppressRowClickSelection={true}
pagination={pagination}
columnDefs={columnDefs}
rowData={rows}
/>
);
}
export default RecordsOutputComponent;

View file

@ -0,0 +1,34 @@
import { Link } from "react-router-dom";
import { cn } from "../../../../utils/utils";
import { buttonVariants } from "../../../ui/button";
type SideBarButtonsComponentProps = {
items: {
href?: string;
title: string;
icon: React.ReactNode;
}[];
pathname: string;
handleOpenNewFolderModal?: () => void;
};
const SideBarButtonsComponent = ({ items }: SideBarButtonsComponentProps) => {
return (
<>
{items.map((item) => (
<Link to={item.href!}>
<div
key={item.title}
data-testid={`sidebar-nav-${item.title}`}
className={cn(
buttonVariants({ variant: "ghost" }),
"!w-[200px] cursor-pointer justify-start gap-2 border border-transparent hover:border-border hover:bg-transparent",
)}
>
{item.title}
</div>
</Link>
))}
</>
);
};
export default SideBarButtonsComponent;

View file

@ -0,0 +1,326 @@
import { useEffect, useRef, useState } from "react";
import { useLocation } from "react-router-dom";
import { FolderType } from "../../../../pages/MainPage/entities";
import { addFolder, updateFolder } from "../../../../pages/MainPage/services";
import { handleDownloadFolderFn } from "../../../../pages/MainPage/utils/handle-download-folder";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { handleKeyDown } from "../../../../utils/reactflowUtils";
import { cn } from "../../../../utils/utils";
import IconComponent, {
ForwardedIconComponent,
} from "../../../genericIconComponent";
import { Button, buttonVariants } from "../../../ui/button";
import { Input } from "../../../ui/input";
import useFileDrop from "../../hooks/use-on-file-drop";
type SideBarFoldersButtonsComponentProps = {
folders: FolderType[];
pathname: string;
handleChangeFolder?: (id: string) => void;
handleEditFolder?: (item: FolderType) => void;
handleDeleteFolder?: (item: FolderType) => void;
};
const SideBarFoldersButtonsComponent = ({
pathname,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
}: SideBarFoldersButtonsComponentProps) => {
const refInput = useRef<HTMLInputElement>(null);
const setFolders = useFolderStore((state) => state.setFolders);
const folders = useFolderStore((state) => state.folders);
const [foldersNames, setFoldersNames] = useState({});
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [editFolders, setEditFolderName] = useState(
folders.map((obj) => ({ name: obj.name, edit: false })),
);
const uploadFolder = useFolderStore((state) => state.uploadFolder);
const currentFolder = pathname.split("/");
const urlWithoutPath = pathname.split("/").length < 4;
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const folderIdDragging = useFolderStore((state) => state.folderIdDragging);
const checkPathName = (itemId: string) => {
if (urlWithoutPath && itemId === myCollectionId) {
return true;
}
return currentFolder.includes(itemId);
};
const location = useLocation();
const folderId = location?.state?.folderId ?? myCollectionId;
const getFolderById = useFolderStore((state) => state.getFolderById);
const handleFolderChange = (folderId: string) => {
getFolderById(folderId);
};
const { dragOver, dragEnter, dragLeave, onDrop } = useFileDrop(
folderId,
handleFolderChange,
);
const handleUploadFlowsToFolder = () => {
uploadFolder(folderId);
};
const handleDownloadFolder = (id: string) => {
handleDownloadFolderFn(id);
};
function addNewFolder() {
addFolder({ name: "New Folder", parent_id: null, description: "" }).then(
(res) => {
getFoldersApi(true);
},
);
}
function handleEditFolderName(e, name): void {
const {
target: { value },
} = e;
setFoldersNames((old) => ({
...old,
[name]: value,
}));
}
useEffect(() => {
folders.map((obj) => ({ name: obj.name, edit: false }));
}, [folders]);
console.log(folderId, folderIdDragging);
return (
<>
<div className="flex shrink-0 items-center justify-between">
<Button variant="primary" onClick={addNewFolder}>
<ForwardedIconComponent
name="Plus"
className="main-page-nav-button"
/>
New Folder
</Button>
<Button
variant="primary"
className="px-7"
onClick={handleUploadFlowsToFolder}
>
<ForwardedIconComponent
name="Upload"
className="main-page-nav-button"
/>
Upload
</Button>
</div>
<div className="flex gap-2 overflow-auto lg:h-[70vh] lg:flex-col">
<>
{folders.map((item, index) => {
const editFolderName = editFolders?.filter(
(folder) => folder.name === item.name,
)[0];
return (
<div
onDragOver={(e) => dragOver(e, item.id!)}
onDragEnter={(e) => dragEnter(e, item.id!)}
onDragLeave={dragLeave}
onDrop={(e) => onDrop(e, item.id!)}
key={item.id}
data-testid={`sidebar-nav-${item.name}`}
className={cn(
buttonVariants({ variant: "ghost" }),
checkPathName(item.id!)
? "border border-border bg-muted hover:bg-muted"
: "border hover:bg-transparent lg:border-transparent lg:hover:border-border",
"group flex w-full shrink-0 cursor-pointer gap-2 opacity-100 lg:min-w-full",
folderIdDragging === item.id! ? "bg-border" : "",
)}
onClick={() => handleChangeFolder!(item.id!)}
>
<div
onDoubleClick={(event) => {
if (item.name === "My Projects") {
return;
}
if (!foldersNames[item.name]) {
setFoldersNames({ [item.name]: item.name });
}
if (
editFolders.find((obj) => obj.name === item.name)?.name
) {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: true };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
takeSnapshot();
event.stopPropagation();
event.preventDefault();
return;
}
setEditFolderName((old) => [
...old,
{ name: item.name, edit: true },
]);
setFoldersNames((oldFolder) => ({
...oldFolder,
[item.name]: item.name,
}));
takeSnapshot();
event.stopPropagation();
event.preventDefault();
}}
className="flex w-full items-center gap-2"
>
<IconComponent
name={"folder"}
className="mr-2 w-4 flex-shrink-0 justify-start stroke-[1.5] opacity-100"
/>
{editFolderName?.edit ? (
<div>
<Input
className="w-36"
onChange={(e) => {
handleEditFolderName(e, item.name);
}}
ref={refInput}
onKeyDown={(e) => {
if (e.key === "Escape") {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: false };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
setFoldersNames({});
setEditFolderName(
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
);
}
if (e.key === "Enter") {
refInput.current?.blur();
}
handleKeyDown(e, e.key, "");
}}
autoFocus={true}
onBlur={async () => {
const newEditFolders = editFolders.map((obj) => {
if (obj.name === item.name) {
return { name: item.name, edit: false };
}
return { name: obj.name, edit: false };
});
setEditFolderName(newEditFolders);
if (foldersNames[item.name].trim() !== "") {
setFoldersNames((old) => ({
...old,
[item.name]: foldersNames[item.name],
}));
const body = {
...item,
name: foldersNames[item.name],
flows: item.flows?.length > 0 ? item.flows : [],
components:
item.components?.length > 0
? item.components
: [],
};
const updatedFolder = await updateFolder(
body,
item.id!,
);
const updateFolders = folders.filter(
(f) => f.name !== item.name,
);
setFolders([...updateFolders, updatedFolder]);
setFoldersNames({});
setEditFolderName(
folders.map((obj) => ({
name: obj.name,
edit: false,
})),
);
} else {
setFoldersNames((old) => ({
...old,
[item.name]: item.name,
}));
}
}}
value={foldersNames[item.name]}
id={`input-folder-${item.name}`}
/>
</div>
) : (
<span className="block max-w-full truncate opacity-100">
{item.name}
</span>
)}
<div className="flex-1" />
{index > 0 && (
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleDeleteFolder!(item);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"trash"}
className=" w-4 stroke-[1.5]"
/>
</Button>
)}
{/* {index > 0 && (
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"pencil"}
className=" w-4 stroke-[1.5] text-white "
/>
</Button>
)} */}
<Button
className="hidden p-0 hover:bg-white group-hover:block hover:dark:bg-[#0c101a00]"
onClick={(e) => {
handleDownloadFolder(item.id!);
e.stopPropagation();
e.preventDefault();
}}
variant={"ghost"}
>
<IconComponent
name={"Download"}
className=" w-4 stroke-[1.5] text-white "
/>
</Button>
</div>
</div>
);
})}
</>
</div>
</>
);
};
export default SideBarFoldersButtonsComponent;

View file

@ -0,0 +1,145 @@
import {
UPLOAD_ALERT_LIST,
WRONG_FILE_ERROR_ALERT,
} from "../../../constants/alerts_constants";
import { updateFlowInDatabase } from "../../../controllers/API";
import { uploadFlowsFromFolders } from "../../../pages/MainPage/services";
import useAlertStore from "../../../stores/alertStore";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../stores/foldersStore";
import { addVersionToDuplicates } from "../../../utils/reactflowUtils";
const useFileDrop = (folderId, folderChangeCallback) => {
const setFolderDragging = useFolderStore((state) => state.setFolderDragging);
const setFolderIdDragging = useFolderStore(
(state) => state.setFolderIdDragging,
);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const flows = useFlowsManagerStore((state) => state.flows);
const triggerFolderChange = (folderId) => {
if (folderChangeCallback) {
folderChangeCallback(folderId);
}
};
const handleFileDrop = async (e) => {
if (e.dataTransfer.types.some((type) => type === "Files")) {
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
const firstFile = e.dataTransfer.files[0];
if (firstFile.type === "application/json") {
uploadFormData(firstFile);
} else {
setErrorData({
title: WRONG_FILE_ERROR_ALERT,
list: [UPLOAD_ALERT_LIST],
});
}
}
}
};
const dragOver = (
e:
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
folderId: string,
) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
setFolderDragging(true);
}
setFolderIdDragging(folderId);
};
const dragEnter = (
e:
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
folderId: string,
) => {
if (e.dataTransfer.types.some((types) => types === "Files")) {
setFolderDragging(true);
}
setFolderIdDragging(folderId);
e.preventDefault();
};
const dragLeave = (
e:
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
) => {
e.preventDefault();
if (e.target === e.currentTarget) {
setFolderDragging(false);
setFolderIdDragging("");
}
};
const onDrop = (
e:
| React.DragEvent<HTMLDivElement>
| React.DragEvent<HTMLButtonElement>
| React.DragEvent<HTMLAnchorElement>,
folderId: string,
) => {
if (e?.dataTransfer?.getData("flow")) {
const data = JSON.parse(e?.dataTransfer?.getData("flow"));
if (data) {
uploadFromDragCard(data.id, folderId);
return;
}
}
e.preventDefault();
handleFileDrop(e);
};
const uploadFromDragCard = (flowId, folderId) => {
const selectedFlow = flows.find((flow) => flow.id === flowId);
if (!selectedFlow) {
throw new Error("Flow not found");
}
const updatedFlow = { ...selectedFlow, folder_id: folderId };
const newName = addVersionToDuplicates(updatedFlow, flows);
updatedFlow.name = newName;
setFolderDragging(false);
setFolderIdDragging("");
updateFlowInDatabase(updatedFlow).then(() => {
getFoldersApi(true);
triggerFolderChange(folderId);
});
};
const uploadFormData = (data) => {
const formData = new FormData();
formData.append("file", data);
setFolderDragging(false);
setFolderIdDragging("");
uploadFlowsFromFolders(formData).then(() => {
getFoldersApi(true);
triggerFolderChange(folderId);
});
};
return {
dragOver,
dragEnter,
dragLeave,
onDrop,
};
};
export default useFileDrop;

View file

@ -1,47 +1,58 @@
import { Link, useLocation } from "react-router-dom";
import { useLocation } from "react-router-dom";
import { FolderType } from "../../pages/MainPage/entities";
import { useFolderStore } from "../../stores/foldersStore";
import { cn } from "../../utils/utils";
import { buttonVariants } from "../ui/button";
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
import SideBarButtonsComponent from "./components/sideBarButtons";
import SideBarFoldersButtonsComponent from "./components/sideBarFolderButtons";
import { addFolder } from "../../pages/MainPage/services";
import { useNavigate } from "react-router-dom";
import useFlowStore from "../../stores/flowStore";
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
type SidebarNavProps = {
items: {
href: string;
href?: string;
title: string;
icon: React.ReactNode;
}[];
}
handleOpenNewFolderModal?: () => void;
handleChangeFolder?: (id: string) => void;
handleEditFolder?: (item: FolderType) => void;
handleDeleteFolder?: (item: FolderType) => void;
className?: string;
};
export default function SidebarNav({
className,
items,
handleChangeFolder,
handleEditFolder,
handleDeleteFolder,
...props
}: SidebarNavProps) {
const location = useLocation();
const pathname = location.pathname;
const loadingFolders = useFolderStore((state) => state.loading);
const folders = useFolderStore((state) => state.folders);
const pathValues = ["folder", "components", "flows", "all"];
const isFolderPath = pathValues.some((value) => pathname.includes(value));
return (
<nav
className={cn(
"flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
className
)}
{...props}
>
{items.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
buttonVariants({ variant: "ghost" }),
pathname === item.href
? "border border-border bg-muted hover:bg-muted"
: "border border-transparent hover:border-border hover:bg-transparent",
"justify-start gap-2"
)}
>
{item.icon}
{item.title}
</Link>
))}
<nav className={cn(className)} {...props}>
<HorizontalScrollFadeComponent>
<SideBarButtonsComponent items={items} pathname={pathname} />
{!loadingFolders && folders?.length > 0 && isFolderPath && (
<SideBarFoldersButtonsComponent
folders={folders}
pathname={pathname}
handleChangeFolder={handleChangeFolder}
handleEditFolder={handleEditFolder}
handleDeleteFolder={handleDeleteFolder}
/>
)}
</HorizontalScrollFadeComponent>
</nav>
);
}

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

@ -5,7 +5,7 @@ export const StoreGuard = ({ children }) => {
const hasStore = useStoreStore((state) => state.hasStore);
if (!hasStore) {
return <Navigate to="/flows" replace />;
return <Navigate to="/all" replace />;
}
return children;

View file

@ -0,0 +1,7 @@
export default function StringReader({
string,
}: {
string: string;
}): JSX.Element {
return <span className="truncate">{string}</span>;
}

View file

@ -0,0 +1,60 @@
import { CustomCellRendererProps } from "ag-grid-react";
import { cn, isTimeStampString } from "../../utils/utils";
import ArrayReader from "../arrayReaderComponent";
import DateReader from "../dateReaderComponent";
import NumberReader from "../numberReader";
import ObjectRender from "../objectRender";
import StringReader from "../stringReaderComponent";
import { Label } from "../ui/label";
import { Badge } from "../ui/badge";
export default function TableAutoCellRender({
value,
}: CustomCellRendererProps) {
function getCellType() {
switch (typeof value) {
case "object":
if (value === null) {
return String(value);
} else if (Array.isArray(value)) {
return <ArrayReader array={value} />;
} else if (value.definitions) {
// use a custom render defined by the sender
} else {
return <ObjectRender object={value} />;
}
break;
case "string":
if (isTimeStampString(value)) {
return <DateReader date={value} />;
}
//TODO: REFACTOR FOR ANY LABEL NOT HARDCODED
else if (value === "success") {
return (
<Badge
variant="outline"
size="sq"
className={cn(
"min-w-min bg-success-background text-success-foreground hover:bg-success-background",
)}
>
{value}
</Badge>
);
} else {
return <StringReader string={value} />;
}
break;
case "number":
return <NumberReader number={value} />;
default:
return String(value);
}
}
return (
<div className="group flex h-full w-full items-center align-middle">
{getCellType()}
</div>
);
}

View file

@ -0,0 +1,70 @@
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
import { ElementRef, forwardRef, useCallback } from "react";
import {
DEFAULT_TABLE_ALERT_MSG,
DEFAULT_TABLE_ALERT_TITLE,
} from "../../constants/constants";
import { useDarkStore } from "../../stores/darkStore";
import "../../style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
import { cn } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
interface TableComponentProps extends AgGridReactProps {
columnDefs: NonNullable<AgGridReactProps["columnDefs"]>;
rowData: NonNullable<AgGridReactProps["rowData"]>;
alertTitle?: string;
alertDescription?: string;
}
const TableComponent = forwardRef<
ElementRef<typeof AgGridReact>,
TableComponentProps
>(
(
{
alertTitle = DEFAULT_TABLE_ALERT_TITLE,
alertDescription = DEFAULT_TABLE_ALERT_MSG,
...props
},
ref,
) => {
const dark = useDarkStore((state) => state.dark);
if (props.rowData.length === 0) {
return (
<div className="flex h-full w-full items-center justify-center rounded-md border">
<Alert variant={"default"} className="w-fit">
<ForwardedIconComponent
name="AlertCircle"
className="h-5 w-5 text-primary"
/>
<AlertTitle>{alertTitle}</AlertTitle>
<AlertDescription>{alertDescription}</AlertDescription>
</Alert>
</div>
);
}
return (
<div
className={cn(
dark ? "ag-theme-quartz-dark" : "ag-theme-quartz",
"ag-theme-shadcn flex h-full flex-col",
)} // applying the grid theme
>
<AgGridReact
{...props}
className={cn(props.className, "custom-scroll")}
defaultColDef={{
minWidth: 100,
}}
ref={ref}
/>
</div>
);
},
);
export default TableComponent;

View file

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { cn } from "../../utils/utils";
import HorizontalScrollFadeComponent from "../horizontalScrollFadeComponent";
import { Badge } from "../ui/badge";
export function TagsSelector({
@ -24,86 +24,37 @@ export function TagsSelector({
setSelectedTags(newArray);
};
const scrollContainerRef = useRef<HTMLDivElement>(null);
const fadeContainerRef = useRef<HTMLDivElement>(null);
const [divWidth, setDivWidth] = useState<number>(0);
useEffect(() => {
const handleResize = () => {
if (scrollContainerRef.current) {
setDivWidth(scrollContainerRef.current.clientWidth);
}
};
window.addEventListener("resize", handleResize);
handleResize(); // call the function at start to get the initial width
return () => window.removeEventListener("resize", handleResize);
}, []);
useEffect(() => {
const handleScroll = () => {
if (!scrollContainerRef.current || !fadeContainerRef.current) return;
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
const atStart = scrollLeft === 0;
const atEnd = scrollLeft === scrollWidth - clientWidth;
const isScrollable = scrollWidth > clientWidth;
fadeContainerRef.current.classList.toggle(
"fade-left",
isScrollable && !atStart
);
fadeContainerRef.current.classList.toggle(
"fade-right",
isScrollable && !atEnd
);
};
const scrollContainer = scrollContainerRef.current;
if (scrollContainer) {
scrollContainer.addEventListener("scroll", handleScroll);
// Delay the initial scroll event dispatch to ensure correct calculation
scrollContainer.dispatchEvent(new Event("scroll"));
return () => scrollContainer.removeEventListener("scroll", handleScroll);
}
}, [divWidth, loadingTags]); // Depend on divWidth
return (
<div ref={fadeContainerRef} className="fade-container">
<div
ref={scrollContainerRef}
className="scroll-container flex min-w-min gap-2"
>
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={disabled}
className={
disabled
? "cursor-not-allowed"
: " overflow-hidden whitespace-nowrap"
}
onClick={() => {
updateTags(tag.name);
}}
<HorizontalScrollFadeComponent isFolder={false}>
{!loadingTags &&
tags.map((tag, idx) => (
<button
disabled={disabled}
className={
disabled
? "cursor-not-allowed"
: " overflow-hidden whitespace-nowrap"
}
onClick={() => {
updateTags(tag.name);
}}
key={idx}
data-testid={`tag-selector-${tag.name}`}
>
<Badge
key={idx}
variant="outline"
size="sq"
className={cn(
selectedTags.some((category) => category === tag.name)
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
: "",
)}
>
<Badge
key={idx}
variant="outline"
size="sq"
className={cn(
selectedTags.some((category) => category === tag.name)
? "min-w-min bg-beta-foreground text-background hover:bg-beta-foreground"
: ""
)}
>
{tag.name}
</Badge>
</button>
))}
</div>
</div>
{tag.name}
</Badge>
</button>
))}
</HorizontalScrollFadeComponent>
);
}

View file

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { editTextModalTitle } from "../../constants/constants";
import { EDIT_TEXT_MODAL_TITLE } from "../../constants/constants";
import { TypeModal } from "../../constants/enums";
import GenericModal from "../../modals/genericModal";
import { TextAreaComponentType } from "../../types/components";
@ -16,7 +16,7 @@ export default function TextAreaComponent({
// Clear text area
useEffect(() => {
if (disabled && value !== "") {
onChange("");
onChange("", true);
}
}, [disabled]);
@ -38,7 +38,7 @@ export default function TextAreaComponent({
<GenericModal
type={TypeModal.TEXT}
buttonText="Finish Editing"
modalTitle={editTextModalTitle}
modalTitle={EDIT_TEXT_MODAL_TITLE}
value={value}
setValue={(value: string) => {
onChange(value);
@ -46,10 +46,11 @@ export default function TextAreaComponent({
>
{!editNode && (
<IconComponent
strokeWidth={1.5}
id={id}
name="ExternalLink"
className={
"icons-parameters-comp" +
"icons-parameters-comp w-[1.35rem]" +
(disabled ? " text-ring" : " hover:text-accent-foreground")
}
/>

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

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