Merge remote-tracking branch 'origin/dev' into feature/output_dropdown

This commit is contained in:
anovazzi1 2024-05-24 00:17:41 -03:00
commit bd4f4c8724
318 changed files with 14679 additions and 9306 deletions

View file

@ -9,6 +9,7 @@
"version": "0.1.2",
"dependencies": {
"@headlessui/react": "^1.7.17",
"@hookform/resolvers": "^3.3.4",
"@million/lint": "^0.0.73",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
@ -55,6 +56,7 @@
"react-cookie": "^4.1.1",
"react-dom": "^18.2.21",
"react-error-boundary": "^4.0.11",
"react-hook-form": "^7.51.4",
"react-icons": "^5.0.1",
"react-laag": "^2.0.5",
"react-markdown": "^8.0.7",
@ -73,10 +75,11 @@
"uuid": "^9.0.0",
"vite-plugin-svgr": "^3.2.0",
"web-vitals": "^2.1.4",
"zod": "^3.23.7",
"zustand": "^4.4.7"
},
"devDependencies": {
"@playwright/test": "^1.43.1",
"@playwright/test": "^1.44.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9",
@ -150,6 +153,26 @@
"nun": "bin/nun.mjs"
}
},
"node_modules/@axiomhq/js": {
"version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@axiomhq/js/-/js-1.0.0-rc.3.tgz",
"integrity": "sha512-Zm10TczcMLounWqC42nMkXQ7XKLqjzLrd5ia022oBKDUZqAFVg2y9d1quQVNV4FlXyg9MKDdfMjpKQRmzEGaog==",
"dependencies": {
"fetch-retry": "^6.0.0",
"uuid": "^8.3.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@axiomhq/js/node_modules/uuid": {
"version": "8.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/@babel/code-frame": {
"version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
@ -429,6 +452,40 @@
"node": ">=6.9.0"
}
},
"node_modules/@clack/core": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz",
"integrity": "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==",
"dependencies": {
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
}
},
"node_modules/@clack/prompts": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz",
"integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==",
"bundleDependencies": [
"is-unicode-supported"
],
"dependencies": {
"@clack/core": "^0.3.3",
"is-unicode-supported": "*",
"picocolors": "^1.0.0",
"sisteransi": "^1.0.5"
}
},
"node_modules/@clack/prompts/node_modules/is-unicode-supported": {
"version": "1.3.0",
"inBundle": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
@ -868,9 +925,9 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.1.tgz",
"integrity": "sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==",
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.2.tgz",
"integrity": "sha512-+2XpQV9LLZeanU4ZevzRnGFg2neDeKHgFLjP6YLW+tly0IvrhqT4u8enLGjLH3qeh85g19xY5rsAusfwTdn5lg==",
"dependencies": {
"@floating-ui/utils": "^0.2.0"
}
@ -885,9 +942,9 @@
}
},
"node_modules/@floating-ui/react-dom": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.0.9.tgz",
"integrity": "sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.0.tgz",
"integrity": "sha512-lNzj5EQmEKn5FFKc04+zasr09h/uX8RtJRNj5gUXsSQIXHVWTVh+hVAg1vOMCexkX8EgvemMvIFpQfkosnVNyA==",
"dependencies": {
"@floating-ui/dom": "^1.0.0"
},
@ -917,6 +974,14 @@
"react-dom": "^16 || ^17 || ^18"
}
},
"node_modules/@hookform/resolvers": {
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.4.2.tgz",
"integrity": "sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==",
"peerDependencies": {
"react-hook-form": "^7.0.0"
}
},
"node_modules/@humanwhocodes/config-array": {
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
@ -999,6 +1064,17 @@
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
@ -1034,6 +1110,22 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@ -1109,6 +1201,25 @@
"node": ">=10"
}
},
"node_modules/@million/install": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@million/install/-/install-0.0.3.tgz",
"integrity": "sha512-yK8NgP+73+Tby/bmQ12B96bJ7RjsLazLtFgBed1Fg1WfTCCpHTILq79yQMSD/OWgm1tt1NYV4ELaTRM6wZOeAg==",
"dependencies": {
"@antfu/ni": "^0.21.12",
"@axiomhq/js": "1.0.0-rc.3",
"@babel/core": "^7.24.5",
"@babel/types": "^7.23.6",
"@clack/prompts": "^0.7.0",
"cli-high": "^0.4.1",
"diff": "^5.1.0",
"posthog-node": "^3.6.3",
"xycolors": "^0.1.1"
},
"bin": {
"install": "bin/index.js"
}
},
"node_modules/@million/lint": {
"version": "0.0.73",
"resolved": "https://registry.npmjs.org/@million/lint/-/lint-0.0.73.tgz",
@ -3232,14 +3343,14 @@
}
},
"node_modules/@swc/core": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.6.tgz",
"integrity": "sha512-0UC0NkgWoqd9fkHPn1NTkTsQucW8iaA1fujK2OLGp40Zg5Vr7nrwBlqruX9expVMggS4rv/3vZSAGzRm80VQ/g==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.7.tgz",
"integrity": "sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"@swc/counter": "^0.1.2",
"@swc/types": "^0.1.5"
"@swc/types": "0.1.7"
},
"engines": {
"node": ">=10"
@ -3249,16 +3360,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.5.6",
"@swc/core-darwin-x64": "1.5.6",
"@swc/core-linux-arm-gnueabihf": "1.5.6",
"@swc/core-linux-arm64-gnu": "1.5.6",
"@swc/core-linux-arm64-musl": "1.5.6",
"@swc/core-linux-x64-gnu": "1.5.6",
"@swc/core-linux-x64-musl": "1.5.6",
"@swc/core-win32-arm64-msvc": "1.5.6",
"@swc/core-win32-ia32-msvc": "1.5.6",
"@swc/core-win32-x64-msvc": "1.5.6"
"@swc/core-darwin-arm64": "1.5.7",
"@swc/core-darwin-x64": "1.5.7",
"@swc/core-linux-arm-gnueabihf": "1.5.7",
"@swc/core-linux-arm64-gnu": "1.5.7",
"@swc/core-linux-arm64-musl": "1.5.7",
"@swc/core-linux-x64-gnu": "1.5.7",
"@swc/core-linux-x64-musl": "1.5.7",
"@swc/core-win32-arm64-msvc": "1.5.7",
"@swc/core-win32-ia32-msvc": "1.5.7",
"@swc/core-win32-x64-msvc": "1.5.7"
},
"peerDependencies": {
"@swc/helpers": "^0.5.0"
@ -3270,9 +3381,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.6.tgz",
"integrity": "sha512-U4szqU03cvZOTXug5o+HQbbg16ZGwANtr7LELjw4hmWBqSTcbBVXgocyBMm1L4ngFIsqJc33D+urnnaae/NfMg==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.5.7.tgz",
"integrity": "sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ==",
"cpu": [
"arm64"
],
@ -3286,9 +3397,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.6.tgz",
"integrity": "sha512-k4jymBHYkbfGw8DT4bLtVsAckpp2dblyImQHRPScpvDyS4jUo4174mgy/dEnFmZVLTbuZAY876zBQyH+eJ3p4A==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.5.7.tgz",
"integrity": "sha512-RpUyu2GsviwTc2qVajPL0l8nf2vKj5wzO3WkLSHAHEJbiUZk83NJrZd1RVbEknIMO7+Uyjh54hEh8R26jSByaw==",
"cpu": [
"x64"
],
@ -3302,9 +3413,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.6.tgz",
"integrity": "sha512-ijwGEdP18vS8YmvHUIfKYDFQ5mQ1GtCxhJp+IcJlrBJE+/eSJVvEVF5WwXFQ+Hzj6tr/OOub8UcRNUaQokjh3A==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.5.7.tgz",
"integrity": "sha512-cTZWTnCXLABOuvWiv6nQQM0hP6ZWEkzdgDvztgHI/+u/MvtzJBN5lBQ2lue/9sSFYLMqzqff5EHKlFtrJCA9dQ==",
"cpu": [
"arm"
],
@ -3318,9 +3429,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.6.tgz",
"integrity": "sha512-6YD942jsDm64wgGNew4Q1Yh8CRI4QKWTqjzCkvlZCGV6aOw5B663WDSnUEKIoN4egeLcvSiqYYGlKMfbkkfNMw==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.5.7.tgz",
"integrity": "sha512-hoeTJFBiE/IJP30Be7djWF8Q5KVgkbDtjySmvYLg9P94bHg9TJPSQoC72tXx/oXOgXvElDe/GMybru0UxhKx4g==",
"cpu": [
"arm64"
],
@ -3334,9 +3445,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.6.tgz",
"integrity": "sha512-Jntdd/HQAgRSQFUpmXjiU00BG08Yl5G1pgYDBXG2mPfkndGGgapRT8JPsnzfQujh9nOdWMIQDnCGU+mB4NY2Dg==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.5.7.tgz",
"integrity": "sha512-+NDhK+IFTiVK1/o7EXdCeF2hEzCiaRSrb9zD7X2Z7inwWlxAntcSuzZW7Y6BRqGQH89KA91qYgwbnjgTQ22PiQ==",
"cpu": [
"arm64"
],
@ -3350,9 +3461,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.6.tgz",
"integrity": "sha512-cdaAyAZJvYCx0u9uadUVe4VVJOEC6GeVO5uTGo/vybCsKFn2Z8WVjGVTeHbxdp7pH2II9MRTySIv1eCCq70SOQ==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.5.7.tgz",
"integrity": "sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg==",
"cpu": [
"x64"
],
@ -3366,9 +3477,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.6.tgz",
"integrity": "sha512-mGIs4d6/Hv/5EbP2d+2YNmIj9U5U/6CiPZsTyahQcEl+vJjNsmwDJoLex36LhxoGIUmVbbgk6cHj5DoHWNl0bQ==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.5.7.tgz",
"integrity": "sha512-0VN9Y5EAPBESmSPPsCJzplZHV26akC0sIgd3Hc/7S/1GkSMoeuVL+V9vt+F/cCuzr4VidzSkqftdP3qEIsXSpg==",
"cpu": [
"x64"
],
@ -3382,9 +3493,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.6.tgz",
"integrity": "sha512-ViiS2pUXy/J9RZWJXQJMuDKid0lqH9fu2piFi3IHnMWyyWPla5qHYijkeODwrdBsjc30NAHjV6jfka5SZduqiw==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.5.7.tgz",
"integrity": "sha512-RtoNnstBwy5VloNCvmvYNApkTmuCe4sNcoYWpmY7C1+bPR+6SOo8im1G6/FpNem8AR5fcZCmXHWQ+EUmRWJyuA==",
"cpu": [
"arm64"
],
@ -3398,9 +3509,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.6.tgz",
"integrity": "sha512-Ju5z65Nda8oYjt2Z4CGbuBLxM+98TcewLogutI69rBo5S70xunadp4ANIHcr9fZlTu/IX9mxZmUpOaHtb+/TfA==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.5.7.tgz",
"integrity": "sha512-Xm0TfvcmmspvQg1s4+USL3x8D+YPAfX2JHygvxAnCJ0EHun8cm2zvfNBcsTlnwYb0ybFWXXY129aq1wgFC9TpQ==",
"cpu": [
"ia32"
],
@ -3414,9 +3525,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.6.tgz",
"integrity": "sha512-pkqJsdrgxyFYJA+g9a3zbl+AoxYoBtv+Rji1Fw4+Sq7CsSMWboayWNN5fKRiyi3u3nG8kxqyGi51PsKy2G4Vpg==",
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.5.7.tgz",
"integrity": "sha512-tp43WfJLCsKLQKBmjmY/0vv1slVywR5Q4qKjF5OIY8QijaEW7/8VwPyUyVoJZEnDgv9jKtUTG5PzqtIYPZGnyg==",
"cpu": [
"x64"
],
@ -3436,9 +3547,9 @@
"dev": true
},
"node_modules/@swc/types": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.6.tgz",
"integrity": "sha512-/JLo/l2JsT/LRd80C3HfbmVpxOAJ11FO2RCEslFrgzLltoP9j8XIbsyDcfCt2WWyX+CM96rBoNM+IToAkFOugg==",
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.7.tgz",
"integrity": "sha512-scHWahbHF0eyj3JsxG9CFJgFdFNaVQCNAimBlT6PzS3n/HptxqREjsm4OH6AN3lYcffZYSPxXW8ua2BEHp0lJQ==",
"dev": true,
"dependencies": {
"@swc/counter": "^0.1.3"
@ -4230,9 +4341,9 @@
}
},
"node_modules/@types/lodash": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz",
"integrity": "sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==",
"version": "4.17.4",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.4.tgz",
"integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==",
"dev": true
},
"node_modules/@types/mathjax": {
@ -4318,12 +4429,12 @@
"dev": true
},
"node_modules/@vitejs/plugin-react-swc": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.6.0.tgz",
"integrity": "sha512-XFRbsGgpGxGzEV5i5+vRiro1bwcIaZDIdBRP16qwm+jP68ue/S8FJTBEgOeojtVDYrbSua3XFp71kC8VJE6v+g==",
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-3.7.0.tgz",
"integrity": "sha512-yrknSb3Dci6svCd/qhHqhFPDSw0QtjumcqdKMoNNzmOl5lMXTTiqzjWtG4Qask2HdvvzaNgSunbQGet8/GrKdA==",
"dev": true,
"dependencies": {
"@swc/core": "^1.3.107"
"@swc/core": "^1.5.7"
},
"peerDependencies": {
"vite": "^4 || ^5"
@ -4342,9 +4453,9 @@
"optional": true
},
"node_modules/ace-builds": {
"version": "1.33.2",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.33.2.tgz",
"integrity": "sha512-uDqCe+XDIdnADaDrA8o+x+qAfbM6uqyDQ43QcE6qC7zBPTvQSMOSPcXW+HvjZhEc2YbVYSaxXJX1qQKPgYqi5w=="
"version": "1.34.0",
"resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.34.0.tgz",
"integrity": "sha512-ZQqoV76wl4guDE5zvEnxCDrvy9gzLAwu7eD4ikYj/Gdlb4+qRLbb+aOFVnweZZRsHh089V0WVaw2NMNuiiEdTw=="
},
"node_modules/acorn": {
"version": "8.11.3",
@ -4384,16 +4495,16 @@
}
},
"node_modules/ag-grid-community": {
"version": "31.3.1",
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.3.1.tgz",
"integrity": "sha512-kKnNxY8UaVoF0aUSdtzK7oGr48Wj+VrdDY5l2p9+HdF0cAo/jBEasuUYR85QbkumNyilI6UbFpO6IyCrjNQ6Iw=="
"version": "31.3.2",
"resolved": "https://registry.npmjs.org/ag-grid-community/-/ag-grid-community-31.3.2.tgz",
"integrity": "sha512-GxqFRD0OcjaVRE1gwLgoP0oERNPH8Lk8wKJ1txulsxysEQ5dZWHhiIoXXSiHjvOCVMkK/F5qzY6HNrn6VeDMTQ=="
},
"node_modules/ag-grid-react": {
"version": "31.3.1",
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-31.3.1.tgz",
"integrity": "sha512-MP5PhFeRhe1gWNx866Sr1C+IrJcrJ0yj8pM/ls+Be/GNmSyIcmGb0Gwbl19bOSdfL5btTkNmGY/RbwnIO4GoMQ==",
"version": "31.3.2",
"resolved": "https://registry.npmjs.org/ag-grid-react/-/ag-grid-react-31.3.2.tgz",
"integrity": "sha512-SFHN05bsXp901rIT00Fa6iQLCtyavoJiKaXEDUtAU5LMu+GTkjs/FPQBQ8754omgdDFr4NsS3Ri6QbqBne3rug==",
"dependencies": {
"ag-grid-community": "31.3.1",
"ag-grid-community": "31.3.2",
"prop-types": "^15.8.1"
},
"peerDependencies": {
@ -4508,6 +4619,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz",
"integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==",
"deprecated": "This package is no longer supported.",
"optional": true,
"dependencies": {
"delegates": "^1.0.0",
@ -4621,9 +4733,9 @@
}
},
"node_modules/axios": {
"version": "1.6.8",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
"integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==",
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
"integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@ -4810,11 +4922,11 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -4963,9 +5075,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001618",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001618.tgz",
"integrity": "sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==",
"version": "1.0.30001621",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001621.tgz",
"integrity": "sha512-+NLXZiviFFKX0fk8Piwv3PfLPGtRqJeq2TiNoUff/qB5KJgwecJTvCXDpmlyP/eCI/GUEmp/h/y5j0yckiiZrA==",
"funding": [
{
"type": "opencollective",
@ -5118,6 +5230,24 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-high": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/cli-high/-/cli-high-0.4.1.tgz",
"integrity": "sha512-fPbkILfCVG6mfxbsJbJZQsKE5JmfgJNlfBxap52XHXjLwPDhs/OMFVFrNR7bqiavZt2r5A80+wiRHzJyzWqwEQ==",
"hasInstallScript": true,
"dependencies": {
"@clack/prompts": "^0.7.0",
"sugar-high": "^0.6.1",
"xycolors": "^0.1.1",
"yargs": "^17.7.2"
},
"bin": {
"cli-high": "bin/index.js"
},
"funding": {
"url": "https://github.com/sponsors/xinyao27"
}
},
"node_modules/cli-spinners": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
@ -5134,6 +5264,19 @@
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
@ -5787,9 +5930,9 @@
}
},
"node_modules/dompurify": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.3.tgz",
"integrity": "sha512-5sOWYSNPaxz6o2MUPvtyxTTqR4D3L77pr5rUQoWgD5ROQtVIZQgJkXbo1DLlK3vj11YGw5+LnF4SYti4gZmwng=="
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.4.tgz",
"integrity": "sha512-2gnshi6OshmuKil8rMZuQCGiUF3cUxHY3NGDzUAdUx/NPEe5DVnO8BDoAQouvgwnx0R/+a6jUn36Z0FSdq8vww=="
},
"node_modules/dot-case": {
"version": "3.0.4",
@ -5817,9 +5960,9 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.767",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.767.tgz",
"integrity": "sha512-nzzHfmQqBss7CE3apQHkHjXW77+8w3ubGCIoEijKCJebPufREaFETgGXWTkh32t259F3Kcq+R8MZdFdOJROgYw=="
"version": "1.4.778",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.778.tgz",
"integrity": "sha512-C6q/xcUJf/2yODRxAVCfIk4j3y3LMsD0ehiE2RQNV2cxc8XU62gR6vvYh3+etSUzlgTfil+qDHI1vubpdf0TOA=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@ -6554,6 +6697,11 @@
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fetch-retry": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz",
"integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag=="
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@ -6618,9 +6766,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@ -6779,9 +6927,9 @@
}
},
"node_modules/framer-motion": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.0.tgz",
"integrity": "sha512-LRfLVPEwtO9IXJCAsWvtj3XZxrdZDcTxNNkZEq30aQ8p7/wimfUkDy67TDWdtzPiyKDkqOHDhaQC6XVrQ4Fh7A==",
"version": "11.2.6",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.6.tgz",
"integrity": "sha512-XUrjjBt57e5YoHQtjwc3eNchFBuHvIgN/cS8SC4oIaAn2J/0+bLanUxXizidJKZVeHJam/JrmMnPRjYMglVn5g==",
"dependencies": {
"tslib": "^2.4.0"
},
@ -6885,6 +7033,7 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz",
"integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==",
"deprecated": "This package is no longer supported.",
"optional": true,
"dependencies": {
"aproba": "^1.0.3 || ^2.0.0",
@ -6909,6 +7058,14 @@
"node": ">=6.9.0"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
@ -7885,9 +8042,9 @@
}
},
"node_modules/jackspeak": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz",
"integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz",
"integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==",
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@ -9309,11 +9466,11 @@
]
},
"node_modules/micromatch": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"dependencies": {
"braces": "^3.0.2",
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@ -9321,12 +9478,13 @@
}
},
"node_modules/million": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/million/-/million-3.0.6.tgz",
"integrity": "sha512-OLjRVASGOZdyZw2ctBSSOu5kb9PaxafqkueqVvw0iQtUUnTLVRk1EmtqcNAtJWCIm8wn+WGRpDbnp+5Hi8//Kg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/million/-/million-3.1.1.tgz",
"integrity": "sha512-vmI3lyA3IN4QKiB0/M3uDef3lZZvgVUokWtLkc5NvxEnykY+TdSR6xatMMNDJmzMHTneayIOpc/eQu4d9Z/r2w==",
"dependencies": {
"@babel/core": "^7.23.7",
"@babel/types": "^7.23.6",
"@million/install": "^0.0.3",
"@rollup/pluginutils": "^5.1.0",
"kleur": "^4.1.5",
"undici": "^6.3.0",
@ -9650,6 +9808,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
"integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==",
"deprecated": "This package is no longer supported.",
"optional": true,
"dependencies": {
"are-we-there-yet": "^2.0.0",
@ -10268,9 +10427,9 @@
}
},
"node_modules/postcss-nested/node_modules/postcss-selector-parser": {
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
"integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
"integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -10810,6 +10969,21 @@
"react": ">=16.13.1"
}
},
"node_modules/react-hook-form": {
"version": "7.51.5",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.5.tgz",
"integrity": "sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==",
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-icons": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
@ -11256,6 +11430,14 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/requires-port": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@ -12130,12 +12312,12 @@
}
},
"node_modules/sucrase/node_modules/glob": {
"version": "10.3.15",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.15.tgz",
"integrity": "sha512-0c6RlJt1TICLyvJYIApxb8GsXoai0KUP7AxKKAtsYXdgJR1mGEUa7DgwShbdk1nly0PYoZj01xd4hzbq3fsjpw==",
"version": "10.3.16",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.16.tgz",
"integrity": "sha512-JDKXl1DiuuHJ6fVS2FXjownaavciiHNUU4mOvV/B793RLh05vZL1rcPnCSaOgv1hDT6RDlY7AB7ZUvFYAtPgAw==",
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^2.3.6",
"jackspeak": "^3.1.2",
"minimatch": "^9.0.1",
"minipass": "^7.0.4",
"path-scurry": "^1.11.0"
@ -12158,6 +12340,11 @@
"node": ">=16 || 14 >=14.17"
}
},
"node_modules/sugar-high": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/sugar-high/-/sugar-high-0.6.1.tgz",
"integrity": "sha512-kg1qMW7WwJcueXIlHkChL/p2EWY3gf8rQmP6n5nUq2TWVqatqDTMLvViS9WgAjgyTKH5/3/b8sRwWPOOAo1zMA=="
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@ -12253,9 +12440,9 @@
}
},
"node_modules/tailwindcss/node_modules/postcss-selector-parser": {
"version": "6.0.16",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz",
"integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==",
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
"integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
@ -12520,9 +12707,9 @@
}
},
"node_modules/undici": {
"version": "6.16.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.16.1.tgz",
"integrity": "sha512-NeNiTT7ixpeiL1qOIU/xTVpHpVP0svmI6PwoCKaMGaI5AsHOaRdwqU/f7Fi9eyU4u03nd5U/BC8wmRMnS9nqoA==",
"version": "6.18.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.18.1.tgz",
"integrity": "sha512-/0BWqR8rJNRysS5lqVmfc7eeOErcOP4tZpATVjJOojjHZ71gSYVAtFhEmadcIjwMIUehh5NFyKGsXCnXIajtbA==",
"engines": {
"node": ">=18.17"
}
@ -13462,16 +13649,16 @@
}
},
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
"strip-ansi": "^7.0.1"
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=12"
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
@ -13524,62 +13711,35 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/wrap-ansi/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/ansi-styles": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=12"
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/wrap-ansi/node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
},
"node_modules/wrap-ansi/node_modules/string-width": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"node_modules/wrap-ansi/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
"color-name": "~1.1.4"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node": ">=7.0.0"
}
},
"node_modules/wrap-ansi/node_modules/strip-ansi": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
"node_modules/wrap-ansi/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/wrappy": {
"version": "1.0.2",
@ -13636,6 +13796,23 @@
"node": ">=0.4"
}
},
"node_modules/xycolors": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/xycolors/-/xycolors-0.1.1.tgz",
"integrity": "sha512-BbRKWpz/87nNH4lXp6TbBFUT0QipzmJI7ksQpSpBb3ny8mGJgkiKk36bIr8VqfyTEhasEBsfbp/Cum37fIHnjA==",
"hasInstallScript": true,
"funding": {
"url": "https://github.com/sponsors/xinyao27"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"engines": {
"node": ">=10"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@ -13652,6 +13829,31 @@
"node": ">= 14"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"engines": {
"node": ">=12"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",

View file

@ -1,134 +1,137 @@
{
"name": "langflow",
"version": "0.1.2",
"private": true,
"dependencies": {
"@headlessui/react": "^1.7.17",
"@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",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@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": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.32.0",
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"ace-builds": "^1.24.1",
"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",
"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.21",
"react-error-boundary": "^4.0.11",
"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",
"react18-json-view": "^0.2.3",
"reactflow": "^11.9.2",
"rehype-mathjax": "^4.0.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"shadcn-ui": "^0.2.3",
"short-unique-id": "^4.4.4",
"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",
"zustand": "^4.4.7"
},
"scripts": {
"dev:docker": "vite --host 0.0.0.0",
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"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",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://127.0.0.1:7860",
"devDependencies": {
"@playwright/test": "^1.43.1",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.197",
"@types/node": "^16.18.46",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.2",
"@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"
}
"name": "langflow",
"version": "0.1.2",
"private": true,
"dependencies": {
"@headlessui/react": "^1.7.17",
"@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",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-form": "^0.0.3",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@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": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.6",
"@tabler/icons-react": "^2.32.0",
"@tailwindcss/forms": "^0.5.6",
"@tailwindcss/line-clamp": "^0.4.4",
"@types/axios": "^0.14.0",
"ace-builds": "^1.24.1",
"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",
"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.21",
"react-error-boundary": "^4.0.11",
"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",
"react18-json-view": "^0.2.3",
"reactflow": "^11.9.2",
"rehype-mathjax": "^4.0.3",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"shadcn-ui": "^0.2.3",
"short-unique-id": "^4.4.4",
"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": {
"dev:docker": "vite --host 0.0.0.0",
"start": "vite",
"build": "vite build",
"serve": "vite preview",
"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",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"proxy": "http://127.0.0.1:7860",
"devDependencies": {
"@playwright/test": "^1.44.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.80",
"@tailwindcss/typography": "^0.5.9",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/lodash": "^4.14.197",
"@types/node": "^16.18.46",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/uuid": "^9.0.2",
"@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

@ -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;
@ -111,3 +137,30 @@ body {
.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

@ -15,19 +15,23 @@ import {
} from "./constants/constants";
import { AuthContext } from "./contexts/authContext";
import { autoLogin, getGlobalVariables, getHealth } from "./controllers/API";
import useTrackLastVisitedPath from "./hooks/use-track-last-visited-path";
import Router from "./routes";
import useAlertStore from "./stores/alertStore";
import { useDarkStore } from "./stores/darkStore";
import useFlowsManagerStore from "./stores/flowsManagerStore";
import { useGlobalVariablesStore } from "./stores/globalVariables";
import { 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
(state) => state.removeFromTempNotificationList,
);
const tempNotificationList = useAlertStore(
(state) => state.tempNotificationList
(state) => state.tempNotificationList,
);
const [fetchError, setFetchError] = useState(false);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
@ -45,15 +49,15 @@ export default function App() {
const refreshVersion = useDarkStore((state) => state.refreshVersion);
const refreshStars = useDarkStore((state) => state.refreshStars);
const setGlobalVariables = useGlobalVariablesStore(
(state) => state.setGlobalVariables
);
const setUnavailableFields = useGlobalVariablesStore(
(state) => state.setUnavaliableFields
(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(() => {
@ -76,7 +80,7 @@ export default function App() {
setUserData(user);
setAutoLogin(true);
setLoading(false);
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
fetchAllData();
}
})
.catch(async (error) => {
@ -84,7 +88,7 @@ export default function App() {
setAutoLogin(false);
if (isAuthenticated && !isLoginPage) {
getUser();
await Promise.all([refreshStars(), refreshVersion(), fetchData()]);
fetchAllData();
} else {
setLoading(false);
useFlowsManagerStore.setState({ isLoading: false });
@ -92,18 +96,25 @@ export default function App() {
}
});
/*
Abort the request as it isn't needed anymore, the component being
/*
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 getFoldersApi();
await getTypes();
await refreshFlows();
const res = await getGlobalVariables();
@ -157,7 +168,7 @@ export default function App() {
setFetchError(false);
//This condition is necessary to avoid infinite loop on starter page when the application is not healthy
if (isLoading === true && window.location.pathname === "/") {
navigate("/flows");
navigate("/all");
window.location.reload();
}
};
@ -184,7 +195,7 @@ export default function App() {
></FetchErrorComponent>
}
{isLoading ? (
{isLoading || loadingFolders ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
@ -200,7 +211,7 @@ export default function App() {
<div className="flex flex-col-reverse" style={{ zIndex: 999 }}>
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "error" && (
{alert.type === "error" ? (
<ErrorAlert
key={alert.id}
title={alert.title}
@ -208,26 +219,12 @@ export default function App() {
id={alert.id}
removeAlert={removeAlert}
/>
)}
</div>
))}
</div>
<div className="z-40 flex flex-col-reverse">
{tempNotificationList.map((alert) => (
<div key={alert.id}>
{alert.type === "notice" ? (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
) : (
alert.type === "success" && (
<SuccessAlert
alert.type === "notice" && (
<NoticeAlert
key={alert.id}
title={alert.title}
link={alert.link}
id={alert.id}
removeAlert={removeAlert}
/>
@ -236,6 +233,20 @@ export default function App() {
</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,141 +1,162 @@
import { saveAs } from "file-saver";
import OpenSeadragon from "openseadragon";
import { useEffect, useRef, useState } from "react";
import ForwardedIconComponent from "../genericIconComponent";
import useFlowStore from "../../stores/flowStore";
import OpenSeadragon from 'openseadragon';
import { Separator } from "../ui/separator";
import { saveAs } from 'file-saver'
import useAlertStore from "../../stores/alertStore";
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 }) {
export default function ImageViewer({ image }) {
const viewerRef = useRef(null);
const [errorDownloading, setErrordownloading] = useState(false)
const setErrorList = useAlertStore(state => state.setErrorData);
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");
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]);
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),
);
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)
});
// 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]);
return (
image === "" ? (
<div className="w-full h-full bg-muted rounded-md flex align-center justify-center flex-col gap-5 border border-border">
<div className="flex gap-2 align-center justify-center ">
<ForwardedIconComponent
name="Image"
/>
{IMGViewErrorTitle}
</div>
<div className="flex align-center justify-center">
<div className="langflow-chat-desc flex align-center justify-center">
<div className="langflow-chat-desc-span">
{IMGViewErrorMSG}
</div>
</div>
</div>
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>
) : (
<>
<div className="w-full flex align-center justify-center my-2 mb-4">
<div className="shadow-round-btn-shadow hover:shadow-round-btn-shadow flex items-center justify-center rounded-sm border bg-muted shadow-md transition-all w-[50%]">
<button id="zoom-in-button" className="relative inline-flex w-full items-center justify-center px-3 py-3 text-sm font-semibold transition-all w-full transition-all duration-500 ease-in-out ease-in-out hover:bg-hover">
<ForwardedIconComponent
name="ZoomIn"
className={"text-secondary-foreground w-5 h-5"}
/>
</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={"text-secondary-foreground w-5 h-5"}
/>
</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={"text-secondary-foreground w-5 h-5"}
/>
</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={"text-secondary-foreground w-5 h-5"}
/>
</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={"text-secondary-foreground w-5 h-5"}
/>
</button>
</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>
<div id="canvas" ref={viewerRef} className={`w-full h-[90%] `} />
</>
)
);
}
<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

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

@ -2,16 +2,16 @@ import { useState } from "react";
import { registerGlobalVariable } from "../../controllers/API";
import BaseModal from "../../modals/baseModal";
import useAlertStore from "../../stores/alertStore";
import { useGlobalVariablesStore } from "../../stores/globalVariables";
import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables";
import { useTypesStore } from "../../stores/typesStore";
import { ResponseErrorDetailAPI } from "../../types/api";
import { sortByName } from "../../utils/utils";
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
@ -130,8 +130,8 @@ export default function AddNewVariableButton({ children }): JSX.Element {
<InputComponent
setSelectedOptions={(value) => setFields(value)}
selectedOptions={fields}
password={false}
options={availableFields()}
password={false}
placeholder="Choose a field for the variable..."
id={"apply-to-fields"}
></InputComponent>

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,4 +1,6 @@
import { useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import { Control } from "react-hook-form";
import { getComponent, postLikeComponent } from "../../controllers/API";
import IOModal from "../../modals/IOModal";
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
@ -6,11 +8,12 @@ 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, {
getInputsAndOutputs,
} from "../../utils/storeUtils";
import { cn, convertTestName } from "../../utils/utils";
import { cn } from "../../utils/utils";
import IconComponent from "../genericIconComponent";
import ShadTooltip from "../shadTooltipComponent";
import { Button } from "../ui/button";
@ -21,8 +24,11 @@ import {
CardHeader,
CardTitle,
} from "../ui/card";
import { Checkbox } from "../ui/checkbox";
import { FormControl, FormField } from "../ui/form";
import Loading from "../ui/loading";
import { FlowType } from "../../types/flow";
import { convertTestName } from "./utils/convert-test-name";
import DragCardComponent from "./components/dragCardComponent";
export default function CollectionCardComponent({
data,
@ -32,6 +38,8 @@ export default function CollectionCardComponent({
onClick,
onDelete,
playground,
control,
is_component,
}: {
data: storeComponent;
authorized?: boolean;
@ -40,6 +48,8 @@ export default function CollectionCardComponent({
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);
@ -69,6 +79,10 @@ export default function CollectionCardComponent({
);
const [loadingPlayground, setLoadingPlayground] = useState(false);
const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards,
);
const name = data.is_component ? "Component" : "Flow";
async function getFlowData() {
@ -82,7 +96,6 @@ export default function CollectionCardComponent({
return false;
}
const { inputs, outputs } = getInputsAndOutputs(flow?.data?.nodes ?? []);
console.log(inputs, outputs);
return inputs.length > 0 || outputs.length > 0;
}
@ -177,36 +190,59 @@ export default function CollectionCardComponent({
}
}
const isSelectedCard =
selectedFlowsComponentsCards?.includes(data?.id) ?? false;
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-center justify-between gap-3 text-xl">
<CardTitle className="flex w-full items-start justify-between gap-3 text-xl">
<IconComponent
className={cn(
"flex-shrink-0",
"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">{data.name}</div>
<div className="w-full truncate pr-3">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex gap-3">
<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">
@ -249,19 +285,29 @@ export default function CollectionCardComponent({
</div>
)}
{onDelete && data?.metadata === undefined && (
<button
className="z-50"
{control && (
<div
className="flex"
onClick={(e) => {
e.stopPropagation();
setOpenDelete(true);
}}
>
<IconComponent
name="Trash2"
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
<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>
)}
/>
</button>
</div>
)}
</CardTitle>
</div>
@ -368,11 +414,7 @@ export default function CollectionCardComponent({
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<DeleteConfirmationModal onConfirm={onDelete}>
<Button
variant="ghost"
size="icon"
@ -540,6 +582,7 @@ export default function CollectionCardComponent({
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

@ -15,8 +15,8 @@ export default function FlowToolbar(): JSX.Element {
const hasIO = useFlowStore((state) => state.hasIO);
const hasStore = useStoreStore((state) => state.hasStore);
const validApiKey = useStoreStore((state) => state.validApiKey);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const hasApiKey = useStoreStore((state) => state.hasApiKey);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {

View file

@ -26,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";
@ -43,7 +45,6 @@ import KeypairListComponent from "../keypairListComponent";
import ShadTooltip from "../shadTooltipComponent";
import { Label } from "../ui/label";
import { Switch } from "../ui/switch";
import ExportModal from "../../modals/exportModal";
export default function CodeTabsComponent({
flow,
@ -54,6 +55,7 @@ export default function CodeTabsComponent({
tweaks,
setActiveTweaks,
activeTweaks,
allowExport = false,
}: codeTabsPropsType) {
const [isCopied, setIsCopied] = useState<Boolean>(false);
const [data, setData] = useState(flow ? flow["data"]!["nodes"] : null);
@ -87,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}`;
@ -141,39 +147,43 @@ export default function CodeTabsComponent({
)}
<div className="float-right mx-1 mb-1 mt-2 flex gap-2">
<div
className={
Number(activeTab) > 2
? "hidden"
: "relative top-[2.5px] flex gap-2"
}
>
<Switch
style={{
transform: `scaleX(${0.7}) scaleY(${0.7})`,
}}
id="tweaks-switch"
onCheckedChange={setActiveTweaks}
autoFocus={false}
/>
<Label
{tweaks && (
<div
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")
Number(activeTab) > 2
? "hidden"
: "relative top-[2.5px] flex gap-2"
}
htmlFor="tweaks-switch"
>
Tweaks
</Label>
</div>
<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
<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>
</ExportModal>
)}
{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 && (
<>
@ -276,107 +286,71 @@ export default function CodeTabsComponent({
</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
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
}
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
@ -410,11 +384,68 @@ export default function CodeTabsComponent({
);
}}
/>
)}
</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
@ -446,9 +477,14 @@ export default function CodeTabsComponent({
disabled={false}
/>
</div>
) : node.data.node.template[
templateField
].type === "file" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"file"
}
>
<div className="mx-auto">
<InputFileComponent
editNode={true}
@ -473,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}
@ -518,12 +559,17 @@ export default function CodeTabsComponent({
}}
/>
</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}
@ -565,9 +611,14 @@ 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}
@ -610,9 +661,14 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "prompt" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"prompt"
}
>
<div className="mx-auto">
<PromptAreaComponent
readonly={true}
@ -651,9 +707,14 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "code" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"code"
}
>
<div className="mx-auto">
<CodeAreaComponent
disabled={false}
@ -692,9 +753,14 @@ export default function CodeTabsComponent({
}}
/>
</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}
@ -712,6 +778,10 @@ export default function CodeTabsComponent({
.template[
templateField
].value,
type(
node,
templateField,
),
)
}
duplicateKey={
@ -755,9 +825,14 @@ export default function CodeTabsComponent({
}
/>
</div>
) : node.data.node.template[
templateField
].type === "NestedDict" ? (
</Case>
<Case
condition={
type(node, templateField) ===
"NestedDict"
}
>
<div className="mx-auto">
<DictComponent
disabled={false}
@ -765,7 +840,7 @@ export default function CodeTabsComponent({
value={
node.data.node!.template[
templateField
].value.toString() === "{}"
].value?.toString() === "{}"
? {
// yourkey: "value",
}
@ -795,13 +870,16 @@ export default function CodeTabsComponent({
}}
/>
</div>
) : node.data.node.template[
templateField
].type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</Case>
<Case
condition={
type(node, templateField) ===
"Any"
}
>
<>-</>
</Case>
</div>
</TableCell>
</TableRow>

View file

@ -1,7 +1,6 @@
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) => ({

View file

@ -1,7 +1,6 @@
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 { AgGridReact } from "ag-grid-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import {
CSVError,
CSVNoDataError,
@ -11,6 +10,7 @@ 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";
@ -54,8 +54,6 @@ function CsvOutputComponent({
const [colDefs, setColDefs] = useState([]);
const [status, setStatus] = useState("loading");
var currentRowHeight: number;
var minRowHeight = 25;
const defaultColDef = useMemo(() => {
return {
width: 200,
@ -82,48 +80,6 @@ function CsvOutputComponent({
}
}, [separator]);
const getRowHeight = useCallback(() => {
return currentRowHeight;
}, []);
const onGridReady = useCallback((params: any) => {
minRowHeight = params.api.getSizesForCurrentTheme().rowHeight;
currentRowHeight = minRowHeight;
}, []);
const updateRowHeight = (params: { api: any }) => {
const bodyViewport = document.querySelector(".ag-body-viewport");
if (!bodyViewport) {
return;
}
var gridHeight = bodyViewport.clientHeight;
var renderedRowCount = params.api.getDisplayedRowCount();
if (renderedRowCount * minRowHeight >= gridHeight) {
if (currentRowHeight !== minRowHeight) {
currentRowHeight = minRowHeight;
params.api.resetRowHeights();
}
} else {
currentRowHeight = Math.floor(gridHeight / renderedRowCount);
params.api.resetRowHeights();
}
};
const onFirstDataRendered = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
const onGridSizeChanged = useCallback(
(params: any) => {
updateRowHeight(params);
},
[updateRowHeight],
);
return (
<div className=" h-full rounded-md border bg-muted">
{status === "nodata" && (
@ -158,14 +114,10 @@ function CsvOutputComponent({
className={`${dark ? "ag-theme-balham-dark" : "ag-theme-balham"}`}
style={{ height: "100%", width: "100%" }}
>
<AgGridReact
<TableComponent
rowData={rowData}
columnDefs={colDefs}
defaultColDef={defaultColDef}
getRowHeight={getRowHeight}
onGridReady={onGridReady}
onFirstDataRendered={onFirstDataRendered}
onGridSizeChanged={onGridSizeChanged}
scrollbarWidth={8}
overlayNoRowsTemplate="No data available"
/>

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

@ -59,7 +59,7 @@ export default function Dropdown({
? "dropdown-component-outline"
: "dropdown-component-false-outline",
"w-full justify-between font-normal",
editNode ? "input-edit-node" : "py-2"
editNode ? "input-edit-node" : "py-2",
)}
>
<span data-testid={`value-dropdown-` + id}>
@ -78,6 +78,8 @@ export default function Dropdown({
</PopoverTrigger>
)}
<PopoverContentDropdown
side="bottom"
avoidCollisions={!!children}
className="nocopy nowheel nopan nodelete nodrag noundo p-0"
style={
children
@ -105,7 +107,7 @@ export default function Dropdown({
name="Check"
className={cn(
"ml-auto h-4 w-4 text-primary",
value === option ? "opacity-100" : "opacity-0"
value === option ? "opacity-100" : "opacity-0",
)}
/>
</CommandItem>

View file

@ -7,7 +7,7 @@ import Loading from "../ui/loading";
import { useEffect, useState } from "react";
const ForwardedIconComponent = memo(
export const ForwardedIconComponent = memo(
forwardRef(
(
{
@ -18,7 +18,7 @@ const ForwardedIconComponent = memo(
strokeWidth,
id = "",
}: IconComponentProps,
ref
ref,
) => {
const [showFallback, setShowFallback] = useState(false);
@ -65,8 +65,8 @@ const ForwardedIconComponent = memo(
/>
</Suspense>
);
}
)
},
),
);
export default ForwardedIconComponent;

View file

@ -8,10 +8,10 @@ import {
} from "../../../ui/dropdown-menu";
import { useNavigate } from "react-router-dom";
import { Node } from "reactflow";
import { UPLOAD_ERROR_ALERT } from "../../../../constants/alerts_constants";
import { SAVED_HOVER } from "../../../../constants/constants";
import ExportModal from "../../../../modals/exportModal";
import FlowLogsModal from "../../../../modals/flowLogsModal";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
@ -21,11 +21,7 @@ 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);
@ -34,7 +30,7 @@ export const MenuBar = ({
const redo = useFlowsManagerStore((state) => state.redo);
const saveLoading = useFlowsManagerStore((state) => state.saveLoading);
const [openSettings, setOpenSettings] = useState(false);
const nodes = useFlowStore((state) => state.nodes);
const [openLogs, setOpenLogs] = useState(false);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const navigate = useNavigate();
const isBuilding = useFlowStore((state) => state.isBuilding);
@ -70,14 +66,6 @@ export const MenuBar = ({
return currentFlow ? (
<div className="round-button-div">
<button
onClick={() => {
removeFunction(nodes);
navigate("/");
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</button>
<div className="header-menu-bar">
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -123,6 +111,18 @@ 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={() => {
@ -132,7 +132,7 @@ export const MenuBar = ({
title: UPLOAD_ERROR_ALERT,
list: [error],
});
}
},
);
}}
>
@ -194,6 +194,7 @@ export const MenuBar = ({
open={openSettings}
setOpen={setOpenSettings}
></FlowSettingsModal>
<FlowLogsModal open={openLogs} setOpen={setOpenLogs}></FlowLogsModal>
</div>
{(currentFlow.updated_at || saveLoading) && (
<ShadTooltip
@ -213,7 +214,7 @@ export const MenuBar = ({
name={isBuilding || saveLoading ? "Loader2" : "CheckCircle2"}
className={cn(
"h-4 w-4",
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle"
isBuilding || saveLoading ? "animate-spin" : "animate-wiggle",
)}
/>
{printByBuildStatus()}

View file

@ -3,14 +3,17 @@ 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";
@ -29,6 +32,7 @@ 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);
@ -40,60 +44,81 @@ export default function Header(): JSX.Element {
const setDark = useDarkStore((state) => state.setDark);
const stars = useDarkStore((state) => state.stars);
async function checkForChanges(nodes: Node[]): Promise<void> {
const routeHistory = useLocationStore((state) => state.routeHistory);
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(nodes)}>
<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(nodes);
}}
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(nodes);
}}
onClick={checkForChanges}
data-testid="button-store"
>
<IconComponent name="Store" className="h-4 w-4" />

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

@ -1,20 +1,12 @@
import * as Form from "@radix-ui/react-form";
import { PopoverAnchor } from "@radix-ui/react-popover";
import { useEffect, useRef, useState } from "react";
import useAlertStore from "../../stores/alertStore";
import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames, 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";
import CustomInputPopover from "./components/popover";
import CustomInputPopoverObject from "./components/popoverObject";
export default function InputComponent({
autoFocus = false,
@ -39,13 +31,15 @@ export default function InputComponent({
optionsPlaceholder = "Search options...",
optionsButton,
optionButton,
objectOptions,
isObjectOption = false,
name,
onChangeFolderName,
}: InputComponentType): JSX.Element {
const setErrorData = useAlertStore.getState().setErrorData;
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
const [showOptions, setShowOptions] = useState<boolean>(false);
// Clear component state
useEffect(() => {
if (disabled && value && onChange && value !== "") {
onChange("", true);
@ -61,6 +55,7 @@ export default function InputComponent({
{isForm ? (
<Form.Control asChild>
<Input
name={name}
id={"form-" + id}
ref={refInput}
onBlur={onInputLostFocus}
@ -80,6 +75,9 @@ export default function InputComponent({
)}
placeholder={password && editNode ? "Key" : placeholder}
onChange={(e) => {
if (onChangeFolderName) {
return onChangeFolderName(e);
}
onChange && onChange(e.target.value);
}}
onCopy={(e) => {
@ -93,181 +91,61 @@ export default function InputComponent({
</Form.Control>
) : (
<>
<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={(e) => {
// if the user copies a password from another input
// it might come as ••••••••••• it causes errors
// in ascii encoding, so we need to handle it
if (password) {
// check if all chars are •
if (
e.target.value.split("").every((char) => char === "•") &&
e.target.value !== ""
) {
setErrorData({
title: `Invalid characters: ${e.target.value}`,
list: [
"It seems you are trying to paste a password. Make sure the value is visible before copying from another field.",
],
});
}
}
onChange && onChange(e.target.value);
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter")
refInput.current?.blur();
}}
data-testid={editNode ? id + "-edit" : id}
/>
</PopoverAnchor>
<PopoverContentWithoutPortal
className="nocopy nopan nodelete nodrag noundo p-0"
style={{ minWidth: refInput?.current?.clientWidth ?? "200px" }}
side="bottom"
align="center"
>
<Command
filter={(value, search) => {
if (
value.toLowerCase().includes(search.toLowerCase()) ||
value.includes("doNotFilter-")
)
return 1; // ensures items arent filtered
return 0;
}}
>
<CommandInput placeholder={optionsPlaceholder} />
<CommandList>
<CommandGroup defaultChecked={false}>
{options.map((option, id) => (
<CommandItem
className="group"
key={option + id}
value={option}
onSelect={(currentValue) => {
setSelectedOption &&
setSelectedOption(
currentValue === selectedOption
? ""
: currentValue,
);
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>
<div
data-testid={"popover-anchor-" + id}
className={cn(
"pointer-events-auto absolute inset-y-0 h-full w-full cursor-pointer",
((selectedOption !== "" || !onChange) && setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)
? ""
: "hidden",
)}
onClick={
((selectedOption !== "" || !onChange) && setSelectedOption) ||
((selectedOptions?.length !== 0 || !onChange) &&
setSelectedOptions)
? (e) => {
setShowOptions((old) => !old);
e.preventDefault();
e.stopPropagation();
}
: () => {}
}
></div>
{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}
/>
)}
</>
)}
@ -279,8 +157,10 @@ export default function InputComponent({
)}
>
<button
onClick={() => {
onClick={(e) => {
setShowOptions(!showOptions);
e.preventDefault();
e.stopPropagation();
}}
className={cn(
selectedOption !== ""

View file

@ -2,7 +2,7 @@ import { useEffect } from "react";
import { deleteGlobalVariable } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/deleteConfirmationModal";
import useAlertStore from "../../stores/alertStore";
import { useGlobalVariablesStore } from "../../stores/globalVariables";
import { useGlobalVariablesStore } from "../../stores/globalVariablesStore/globalVariables";
import { InputGlobalComponentType } from "../../types/components";
import { cn } from "../../utils/utils";
import AddNewVariableButton from "../addNewVariableButtonComponent/addNewVariableButton";
@ -34,6 +34,7 @@ export default function InputGlobalComponent({
useEffect(() => {
if (data.node?.template[name])
if (
globalVariablesEntries &&
!globalVariablesEntries.includes(data.node?.template[name].value) &&
data.node?.template[name].load_from_db
) {
@ -138,6 +139,7 @@ export default function InputGlobalComponent({
)}
selectedOption={
data?.node?.template[name].load_from_db &&
globalVariablesEntries &&
globalVariablesEntries.includes(data?.node?.template[name].value ?? "")
? data?.node?.template[name].value
: ""

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

@ -1,23 +1,19 @@
import { CHAT_FIRST_INITIAL_TEXT, CHAT_SECOND_INITIAL_TEXT, PDFCheckFlow, PDFLoadErrorTitle } from "../../../constants/constants";
import { PDFCheckFlow, PDFLoadErrorTitle } from "../../../constants/constants";
import IconComponent from "../../genericIconComponent";
export default function Error(): JSX.Element {
return (
<div className="flex flex-col items-center justify-center h-full w-full 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>
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,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,48 +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
data-testid={`sidebar-nav-${item.title}`}
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

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

@ -1,27 +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 } from "ag-grid-react";
import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react";
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>,
ComponentPropsWithoutRef<typeof AgGridReact>
>(({ ...props }, ref) => {
const dark = useDarkStore((state) => state.dark);
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 ref={ref} {...props} />
</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,87 +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}
data-testid={`tag-selector-${tag.name}`}
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

@ -0,0 +1,58 @@
import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../utils/utils";
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
},
);
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
));
Alert.displayName = "Alert";
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
));
AlertTitle.displayName = "AlertTitle";
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
));
AlertDescription.displayName = "AlertDescription";
export { Alert, AlertTitle, AlertDescription };

View file

@ -13,14 +13,14 @@ const Checkbox = React.forwardRef<
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<IconComponent name="Check" className="h-4 w-4" />
<IconComponent name="Check" className="h-4 w-4 stroke-2" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));
@ -37,7 +37,7 @@ const CheckBoxDiv = ({
className={cn(
className,
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
checked ? "bg-primary text-primary-foreground" : ""
checked ? "bg-primary text-primary-foreground" : "",
)}
>
{checked && (

View file

@ -0,0 +1,55 @@
"use client";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "@radix-ui/react-icons";
import * as React from "react";
import { cn } from "../../utils/utils";
const Accordion = AccordionPrimitive.Root;
const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item ref={ref} className={cn("", className)} {...props} />
));
AccordionItem.displayName = "AccordionItem";
const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger asChild ref={ref} {...props}>
<div
className={cn(
" flex flex-1 cursor-pointer items-center justify-between border-[1px] py-2 text-sm font-medium data-[state=closed]:rounded-md data-[state=open]:rounded-t-md data-[state=open]:border-b-0 data-[state=open]:bg-muted [&[data-state=open]>svg]:rotate-180",
className,
)}
>
{children}
<ChevronDownIcon className="h-4 w-4 font-bold text-primary transition-transform duration-200" />
</div>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className={cn(
"data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden border-[1px] text-sm data-[state=open]:rounded-b-md data-[state=open]:border-t-0 data-[state=open]:bg-muted",
className,
)}
{...props}
>
<div className="pt-0">{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
export { Accordion, AccordionContent, AccordionItem, AccordionTrigger };

View file

@ -0,0 +1,176 @@
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { cn } from "../../utils/utils";
import { Label } from "./label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue,
);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext);
const itemContext = React.useContext(FormItemContext);
const { getFieldState, formState } = useFormContext();
const fieldState = getFieldState(fieldContext.name, formState);
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>");
}
const { id } = itemContext;
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
};
};
type FormItemContextValue = {
id: string;
};
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue,
);
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId();
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField();
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } =
useFormField();
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
);
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField();
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
useFormField,
};

View file

@ -95,6 +95,12 @@ export const EXPORT_DIALOG_SUBTITLE = "Export flow as JSON file.";
*/
export const SETTINGS_DIALOG_SUBTITLE = "Edit details about your project.";
/**
* The base text for subtitle of Flow Logs (Menubar)
* @constant
*/
export const LOGS_DIALOG_SUBTITLE = "Check out information about your flow.";
/**
* The base text for subtitle of Code Dialog (Toolbar)
* @constant
@ -125,7 +131,6 @@ export const CODE_PROMPT_DIALOG_SUBTITLE =
export const CODE_DICT_DIALOG_SUBTITLE =
"Edit your dictionary. This dialog allows you to create your own customized dictionary. You can add as many key-value pairs as you want. While in edit mode, you can enter ({}) or ([]), and this will result in adding a new object or array.";
/**
* The base text for subtitle of Prompt Dialog
* @constant
@ -533,6 +538,8 @@ export const NOUNS: string[] = [
*/
export const USER_PROJECTS_HEADER = "My Collection";
export const DEFAULT_FOLDER = "My Projects";
/**
* Header text for admin page
* @constant
@ -727,6 +734,8 @@ export const OUTPUT_TYPES = new Set([
"JsonOutput",
"KeyPairOutput",
"StringListOutput",
"RecordsOutput",
"TableOutput",
]);
export const CHAT_FIRST_INITIAL_TEXT =
@ -746,8 +755,8 @@ export const EDIT_TEXT_MODAL_TITLE = "Edit Text";
export const EDIT_TEXT_PLACEHOLDER = "Type message here.";
export const INPUT_HANDLER_HOVER = "Avaliable input components:";
export const OUTPUT_HANDLER_HOVER = "Avaliable output components:";
export const TEXT_INPUT_MODAL_TITLE = "Text Inputs";
export const OUTPUTS_MODAL_TITLE = "Text Outputs";
export const TEXT_INPUT_MODAL_TITLE = "Inputs";
export const OUTPUTS_MODAL_TITLE = "Outputs";
export const LANGFLOW_CHAT_TITLE = "Langflow Chat";
export const CHAT_INPUT_PLACEHOLDER =
"No chat input variables found. Click to run your flow.";
@ -791,4 +800,19 @@ export const NATIVE_CATEGORIES = [
"agents",
];
export const AUTHORIZED_DUPLICATE_REQUESTS = [
"/health",
"/flows",
"/logout",
"/refresh",
"/login",
"/auto_login",
];
export const SAVE_DEBOUNCE_TIME = 300;
export const DEFAULT_TABLE_ALERT_MSG = `Oops! It seems there's no data to display right now. Please check back later.`;
export const DEFAULT_TABLE_ALERT_TITLE = "No Data Available";
export const LOCATIONS_TO_RETURN = ["/flow/", "/settings/"];

View file

@ -2,6 +2,7 @@ import axios, { AxiosError, AxiosInstance } from "axios";
import { useContext, useEffect } from "react";
import { Cookies } from "react-cookie";
import { renewAccessToken } from ".";
import { AUTHORIZED_DUPLICATE_REQUESTS } from "../../constants/constants";
import { BuildStatus } from "../../constants/enums";
import { AuthContext } from "../../contexts/authContext";
import useAlertStore from "../../stores/alertStore";
@ -22,7 +23,10 @@ function ApiInterceptor() {
const interceptor = api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
if (error.response?.status === 403 || error.response?.status === 401) {
if (
error?.response?.status === 403 ||
error?.response?.status === 401
) {
if (!autoLogin) {
if (error?.config?.url?.includes("github")) {
return Promise.reject(error);
@ -44,7 +48,7 @@ function ApiInterceptor() {
}
await clearBuildVerticesState(error);
return Promise.reject(error);
}
},
);
const isAuthorizedURL = (url) => {
@ -61,10 +65,10 @@ function ApiInterceptor() {
const parsedURL = new URL(url);
const isDomainAllowed = authorizedDomains.some(
(domain) => parsedURL.origin === new URL(domain).origin
(domain) => parsedURL.origin === new URL(domain).origin,
);
const isEndpointAllowed = authorizedEndpoints.some((endpoint) =>
parsedURL.pathname.includes(endpoint)
parsedURL.pathname.includes(endpoint),
);
return isDomainAllowed || isEndpointAllowed;
@ -77,6 +81,28 @@ function ApiInterceptor() {
// Request interceptor to add access token to every request
const requestInterceptor = api.interceptors.request.use(
(config) => {
const lastUrl = localStorage.getItem("lastUrlCalled");
const lastMethodCalled = localStorage.getItem("lastMethodCalled");
const isContained = AUTHORIZED_DUPLICATE_REQUESTS.some((request) =>
config?.url!.includes(request),
);
if (
config?.url === lastUrl &&
!isContained &&
lastMethodCalled === config.method
) {
return Promise.reject("Duplicate request");
}
localStorage.setItem("lastUrlCalled", config.url ?? "");
localStorage.setItem("lastMethodCalled", config.method ?? "");
localStorage.setItem(
"lastRequestData",
JSON.stringify(config.data) ?? "",
);
const accessToken = cookies.get("access_token_lf");
if (accessToken && !isAuthorizedURL(config?.url)) {
config.headers["Authorization"] = `Bearer ${accessToken}`;
@ -86,7 +112,7 @@ function ApiInterceptor() {
},
(error) => {
return Promise.reject(error);
}
},
);
return () => {
@ -118,7 +144,7 @@ function ApiInterceptor() {
if (error?.config?.headers) {
delete error.config.headers["Authorization"];
error.config.headers["Authorization"] = `Bearer ${cookies.get(
"access_token_lf"
"access_token_lf",
)}`;
const response = await axios.request(error.config);
return response;

View file

@ -1,5 +1,6 @@
import { ColDef, ColGroupDef } from "ag-grid-community";
import { AxiosRequestConfig, AxiosResponse } from "axios";
import { Edge, ReactFlowJsonObject,Node } from "reactflow";
import { Edge, Node, ReactFlowJsonObject } from "reactflow";
import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import {
@ -18,6 +19,7 @@ import { UserInputType } from "../../types/components";
import { FlowStyleType, FlowType } from "../../types/flow";
import { StoreComponentResponse } from "../../types/store";
import { FlowPoolType } from "../../types/zustand/flow";
import { extractColumnsFromRows } from "../../utils/utils";
import {
APIClassType,
BuildStatusTypeAPI,
@ -59,7 +61,7 @@ export async function sendAll(data: sendAllProps) {
}
export async function postValidateCode(
code: string
code: string,
): Promise<AxiosResponse<errorsTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/code`, { code });
}
@ -74,7 +76,7 @@ export async function postValidateCode(
export async function postValidatePrompt(
name: string,
template: string,
frontend_node: APIClassType
frontend_node: APIClassType,
): Promise<AxiosResponse<PromptTypeAPI>> {
return api.post(`${BASE_URL_API}validate/prompt`, {
name,
@ -119,6 +121,7 @@ export async function saveFlowToDatabase(newFlow: {
description: string;
style?: FlowStyleType;
is_component?: boolean;
folder_id?: string;
}): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}flows/`, {
@ -126,6 +129,7 @@ export async function saveFlowToDatabase(newFlow: {
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
folder_id: newFlow.folder_id === "" ? null : newFlow.folder_id,
});
if (response.status !== 201) {
@ -145,13 +149,14 @@ export async function saveFlowToDatabase(newFlow: {
* @throws Will throw an error if the update fails.
*/
export async function updateFlowInDatabase(
updatedFlow: FlowType
updatedFlow: FlowType,
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}flows/${updatedFlow.id}`, {
name: updatedFlow.name,
data: updatedFlow.data,
description: updatedFlow.description,
folder_id: updatedFlow.folder_id === "" ? null : updatedFlow.folder_id,
});
if (response?.status !== 200) {
@ -321,7 +326,7 @@ export async function getHealth() {
*
*/
export async function getBuildStatus(
flowId: string
flowId: string,
): Promise<AxiosResponse<BuildStatusTypeAPI>> {
return await api.get(`${BASE_URL_API}build/${flowId}/status`);
}
@ -334,7 +339,7 @@ export async function getBuildStatus(
*
*/
export async function postBuildInit(
flow: FlowType
flow: FlowType,
): Promise<AxiosResponse<InitTypeAPI>> {
return await api.post(`${BASE_URL_API}build/init/${flow.id}`, flow);
}
@ -350,7 +355,7 @@ export async function postBuildInit(
*/
export async function uploadFile(
file: File,
id: string
id: string,
): Promise<AxiosResponse<UploadFileTypeAPI>> {
const formData = new FormData();
formData.append("file", file);
@ -359,7 +364,7 @@ export async function uploadFile(
export async function postCustomComponent(
code: string,
apiClass: APIClassType
apiClass: APIClassType,
): Promise<AxiosResponse<APIClassType>> {
// let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
@ -372,7 +377,7 @@ export async function postCustomComponentUpdate(
code: string,
template: APITemplateType,
field: string,
field_value: any
field_value: any,
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
@ -394,7 +399,7 @@ export async function onLogin(user: LoginType) {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
},
);
if (response.status === 200) {
@ -456,11 +461,11 @@ export async function addUser(user: UserInputType): Promise<Array<Users>> {
export async function getUsersPage(
skip: number,
limit: number
limit: number,
): Promise<Array<Users>> {
try {
const res = await api.get(
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`
`${BASE_URL_API}users/?skip=${skip}&limit=${limit}`,
);
if (res.status === 200) {
return res.data;
@ -497,7 +502,7 @@ export async function resetPassword(user_id: string, user: resetPasswordType) {
try {
const res = await api.patch(
`${BASE_URL_API}users/${user_id}/reset-password`,
user
user,
);
if (res.status === 200) {
return res.data;
@ -571,7 +576,7 @@ export async function saveFlowStore(
last_tested_version?: string;
},
tags: string[],
publicFlow = false
publicFlow = false,
): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}store/components/`, {
@ -700,7 +705,7 @@ export async function postStoreComponents(component: Component) {
export async function getComponent(component_id: string) {
try {
const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`
`${BASE_URL_API}store/components/${component_id}`,
);
if (res.status === 200) {
return res.data;
@ -715,7 +720,7 @@ export async function searchComponent(
page?: number | null,
limit?: number | null,
status?: string | null,
tags?: string[]
tags?: string[],
): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
@ -827,7 +832,7 @@ export async function updateFlowStore(
},
tags: string[],
publicFlow = false,
id: string
id: string,
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
@ -911,7 +916,7 @@ export async function deleteGlobalVariable(id: string) {
export async function updateGlobalVariable(
name: string,
value: string,
id: string
id: string,
) {
try {
const response = api.patch(`${BASE_URL_API}variables/${id}`, {
@ -929,36 +934,40 @@ export async function getVerticesOrder(
flowId: string,
startNodeId?: string | null,
stopNodeId?: string | null,
nodes?:Node[],
Edges?:Edge[]
nodes?: Node[],
Edges?: Edge[],
): Promise<AxiosResponse<VerticesOrderTypeAPI>> {
// nodeId is optional and is a query parameter
// if nodeId is not provided, the API will return all vertices
const config:AxiosRequestConfig<any> = {};
const config: AxiosRequestConfig<any> = {};
if (stopNodeId) {
config["params"] = { stop_component_id: stopNodeId };
} else if (startNodeId) {
config["params"] = { start_component_id: startNodeId };
}
const data = {
data:{}
data: {},
};
if (nodes && Edges) {
data["data"]["nodes"] = nodes;
data["data"]["edges"] = Edges;
}
if(nodes && Edges){
data["data"]["nodes"] = nodes
data["data"]["edges"] = Edges
}
return await api.post(`${BASE_URL_API}build/${flowId}/vertices`,data, config);
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices`,
data,
config,
);
}
export async function postBuildVertex(
flowId: string,
vertexId: string,
input_value: string
input_value: string,
): Promise<AxiosResponse<VertexBuildTypeAPI>> {
// input_value is optional and is a query parameter
return await api.post(
`${BASE_URL_API}build/${flowId}/vertices/${vertexId}`,
input_value ? { inputs: { input_value: input_value } } : undefined
input_value ? { inputs: { input_value: input_value } } : undefined,
);
}
@ -982,9 +991,47 @@ export async function getFlowPool({
}
export async function deleteFlowPool(
flowId: string
flowId: string,
): Promise<AxiosResponse<any>> {
const config = {};
config["params"] = { flow_id: flowId };
return await api.delete(`${BASE_URL_API}monitor/builds`, config);
}
export async function multipleDeleteFlowsComponents(
flowIds: string[],
): Promise<AxiosResponse<any>> {
return await api.post(`${BASE_URL_API}flows/multiple_delete/`, {
flow_ids: flowIds,
});
}
export async function getTransactionTable(
id: string,
mode: "intersection" | "union",
params = {},
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
config["params"] = { flow_id: id };
if (params) {
config["params"] = { ...config["params"], ...params };
}
const rows = await api.get(`${BASE_URL_API}monitor/transactions`, config);
const columns = extractColumnsFromRows(rows.data, mode);
return { rows: rows.data, columns };
}
export async function getMessagesTable(
id: string,
mode: "intersection" | "union",
params = {},
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
config["params"] = { flow_id: id };
if (params) {
config["params"] = { ...config["params"], ...params };
}
const rows = await api.get(`${BASE_URL_API}monitor/messages`, config);
const columns = extractColumnsFromRows(rows.data, mode);
return { rows: rows.data, columns };
}

View file

@ -1,5 +1,5 @@
import { cloneDeep } from "lodash";
import React, { ReactNode, useEffect, useRef, useState } from "react";
import { ReactNode, useEffect, useRef, useState } from "react";
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
import CodeAreaComponent from "../../../../components/codeAreaComponent";
import DictComponent from "../../../../components/dictComponent";
@ -26,20 +26,15 @@ import {
} from "../../../../components/ui/dropdown-menu";
import { RefreshButton } from "../../../../components/ui/refreshButton";
import {
INPUT_HANDLER_HOVER,
LANGFLOW_SUPPORTED_TYPES,
OUTPUT_HANDLER_HOVER,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { Case } from "../../../../shared/components/caseComponent";
import useAlertStore from "../../../../stores/alertStore";
import useFlowStore from "../../../../stores/flowStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useTypesStore } from "../../../../stores/typesStore";
import {
APIClassType,
ResponseErrorDetailAPI,
ResponseErrorTypeAPI,
} from "../../../../types/api";
import { APIClassType } from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components";
import {
debouncedHandleUpdateValues,
@ -52,12 +47,13 @@ import {
isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { nodeColors } from "../../../../utils/styleUtils";
import { classNames, cn, groupByFamily } from "../../../../utils/utils";
import useFetchDataOnMount from "../../../hooks/use-fetch-data-on-mount";
import useHandleOnNewValue from "../../../hooks/use-handle-new-value";
import useHandleNodeClass from "../../../hooks/use-handle-node-class";
import useHandleRefreshButtonPress from "../../../hooks/use-handle-refresh-buttons";
import TooltipRenderComponent from "../tooltipRenderComponent";
export default function ParameterComponent({
left,
@ -83,175 +79,69 @@ export default function ParameterComponent({
const nodes = useFlowStore((state) => state.nodes);
const edges = useFlowStore((state) => state.edges);
const setNode = useFlowStore((state) => state.setNode);
const myData = useTypesStore((state) => state.data);
const takeSnapshot = useFlowsManagerStore((state) => state.takeSnapshot);
const [isLoading, setIsLoading] = useState(false);
const updateNodeInternals = useUpdateNodeInternals();
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const flow = currentFlow?.data?.nodes ?? null;
const groupedEdge = useRef(null);
const setFilterEdge = useFlowStore((state) => state.setFilterEdge);
const { handleOnNewValue: handleOnNewValueHook } = useHandleOnNewValue(
data,
name,
takeSnapshot,
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
);
const { handleNodeClass: handleNodeClassHook } = useHandleNodeClass(
data,
name,
takeSnapshot,
setNode,
updateNodeInternals,
renderTooltips,
);
const { handleRefreshButtonPress: handleRefreshButtonPressHook } =
useHandleRefreshButtonPress(setIsLoading, setNode, renderTooltips);
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 handleRefreshButtonPress = async (name, data) => {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
handleRefreshButtonPressHook(name, data);
};
useEffect(() => {
async function fetchData() {
if (
(data.node?.template[name]?.real_time_refresh ||
data.node?.template[name]?.refresh_button) &&
// options can be undefined but not an empty array
(data.node?.template[name]?.options?.length ?? 0) === 0
) {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
}
}
fetchData();
}, []);
useFetchDataOnMount(
data,
name,
handleUpdateValues,
setNode,
renderTooltips,
setIsLoading,
);
const handleOnNewValue = async (
newValue: string | string[] | boolean | Object[],
skipSnapshot: boolean | undefined = false,
): Promise<void> => {
const nodeTemplate = data.node!.template[name];
const currentValue = nodeTemplate.value;
if (currentValue !== newValue && !skipSnapshot) {
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
currentValue !== newValue;
const typeToDebounce = nodeTemplate.type;
nodeTemplate.value = newValue;
let newTemplate;
if (shouldUpdate) {
setIsLoading(true);
try {
if (["int"].includes(typeToDebounce)) {
newTemplate = await handleUpdateValues(name, data);
} else {
newTemplate = await debouncedHandleUpdateValues(name, data);
}
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
});
}
setIsLoading(false);
}
setNode(data.id, (oldNode) => {
const newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else {
newNode.data.node.template[name].value = newValue;
}
return newNode;
});
renderTooltips();
handleOnNewValueHook(newValue, skipSnapshot);
};
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();
handleNodeClassHook(newNodeClass, code);
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
// @ts-ignore
infoHtml.current = (
@ -310,88 +200,7 @@ export default function ParameterComponent({
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}
data-testid={`available-${left ? "input" : "output"}-${
item.family
}`}
>
{index === 0 && (
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</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"
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
>
{nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span
className="text-xs"
data-testid={`tooltip-${item?.display_name}`}
>
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index ===
item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span
className="text-xs"
data-testid={`tooltip-${item?.type}`}
>
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
);
return <TooltipRenderComponent index={index} item={item} left={left} />;
});
} else {
//@ts-ignore
@ -400,6 +209,7 @@ export default function ParameterComponent({
);
}
}
// If optionalHandle is an empty list, then it is not an optional handle
if (optionalHandle && optionalHandle.length === 0) {
optionalHandle = null;
@ -408,6 +218,7 @@ export default function ParameterComponent({
useEffect(() => {
renderTooltips();
}, [tooltipTitle, flow]);
return !showNode ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
@ -473,11 +284,12 @@ export default function ParameterComponent({
(left ? "" : " justify-end")
}
>
{!left && data.node?.frozen && (
<Case condition={left && data.node?.frozen}>
<div className="pr-1">
<IconComponent className="h-5 w-5 text-ice" name={"Snowflake"} />
</div>
)}
</Case>
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
{renderTitle()}
@ -519,45 +331,37 @@ export default function ParameterComponent({
}`}
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
key={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
key={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
id={scapedJSONStringfy(proxy ? { ...id, proxy } : id)}
isValidConnection={(connection) =>
isValidConnection(connection, nodes, edges)
}
className={classNames(
left ? "-ml-0.5 " : "-mr-0.5 ",
left ? "-ml-0.5" : "-mr-0.5",
"h-3 w-3 rounded-full border-2 bg-background",
)}
style={{
borderColor: color ?? nodeColors.unknown,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
style={{ borderColor: color ?? nodeColors.unknown }}
onClick={() => setFilterEdge(groupedEdge.current)}
/>
</ShadTooltip>
</div>
</Button>
)}
{left === true &&
type === "str" &&
!data.node?.template[name].options ? (
<Case
condition={
left === true &&
type === "str" &&
!data.node?.template[name]?.options
}
>
<div className="w-full">
{data.node?.template[name].list ? (
<Case condition={data.node?.template[name]?.list}>
<div
className={
// Commenting this out until we have a better
// way to display
// (data.node?.template[name].refresh ? "w-5/6 " : "") +
// (data.node?.template[name]?.refresh ? "w-5/6 " : "") +
"flex-grow"
}
>
@ -565,39 +369,27 @@ export default function ParameterComponent({
componentName={name}
disabled={disabled}
value={
!data.node.template[name].value ||
data.node.template[name].value === ""
!data.node!.template[name]?.value ||
data.node!.template[name]?.value === ""
? [""]
: data.node.template[name].value
: data.node!.template[name]?.value
}
onChange={handleOnNewValue}
/>
{/* {data.node?.template[name].refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
/>
</div>
)} */}
</div>
) : data.node?.template[name].multiline ? (
</Case>
<Case condition={data.node?.template[name]?.multiline}>
<div className="mt-2 flex w-full flex-col ">
<div className="flex-grow">
<TextAreaComponent
disabled={disabled}
value={data.node.template[name].value ?? ""}
value={data.node!.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + data.node.template[name].name}
data-testid={"textarea-" + data.node.template[name].name}
id={"textarea-" + data.node!.template[name]?.name}
data-testid={"textarea-" + data.node!.template[name]?.name}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="flex-grow">
<RefreshButton
isLoading={isLoading}
@ -605,7 +397,7 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
data.node?.template[name]?.refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons mt-1"
@ -615,12 +407,18 @@ export default function ParameterComponent({
</div>
)}
</div>
) : (
</Case>
<Case
condition={
!data.node?.template[name]?.multiline &&
!data.node?.template[name]?.list
}
>
<div className="mt-2 flex w-full items-center">
<div
className={
"flex-grow " +
(data.node?.template[name].refresh_button ? "w-5/6" : "")
(data.node?.template[name]?.refresh_button ? "w-5/6" : "")
}
>
<InputGlobalComponent
@ -640,7 +438,7 @@ export default function ParameterComponent({
data={data}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
@ -648,7 +446,7 @@ export default function ParameterComponent({
name={name}
data={data}
button_text={
data.node?.template[name].refresh_button_text ??
data.node?.template[name]?.refresh_button_text ??
"Refresh"
}
className="extra-side-bar-buttons ml-2 mt-1"
@ -658,52 +456,61 @@ export default function ParameterComponent({
</div>
)}
</div>
)}
</Case>
</div>
) : left === true && type === "bool" ? (
</Case>
<Case condition={left === true && type === "bool"}>
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + name}
disabled={disabled}
enabled={data.node?.template[name].value ?? false}
enabled={data.node?.template[name]?.value ?? false}
setEnabled={handleOnNewValue}
size="large"
editNode={false}
/>
</div>
) : left === true && type === "float" ? (
</Case>
<Case condition={left === true && type === "float"}>
<div className="mt-2 w-full">
<FloatComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
rangeSpec={data.node?.template[name]?.rangeSpec}
onChange={handleOnNewValue}
/>
</div>
) : left === true &&
type === "str" &&
(data.node?.template[name].options ||
data.node?.template[name]?.real_time_refresh) ? (
// TODO: Improve CSS
</Case>
<Case
condition={
left === true &&
type === "str" &&
(data.node?.template[name]?.options ||
data.node?.template[name]?.real_time_refresh)
}
>
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
disabled={disabled}
isLoading={isLoading}
options={data.node.template[name].options}
options={data.node!.template[name]?.options}
onSelect={handleOnNewValue}
value={data.node.template[name].value}
value={data.node!.template[name]?.value}
id={"dropdown-" + name}
/>
</div>
{data.node?.template[name].refresh_button && (
{data.node?.template[name]?.refresh_button && (
<div className="w-1/6">
<RefreshButton
isLoading={isLoading}
disabled={disabled}
name={name}
data={data}
button_text={data.node?.template[name].refresh_button_text}
button_text={data.node?.template[name]?.refresh_button_text}
className="extra-side-bar-buttons ml-2 mt-1"
handleUpdateValues={handleRefreshButtonPress}
id={"refresh-button-" + name}
@ -711,46 +518,54 @@ export default function ParameterComponent({
</div>
)}
</div>
) : left === true && type === "code" ? (
</Case>
<Case condition={left === true && type === "code"}>
<div className="mt-2 w-full">
<CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
data.node?.flow && data.node.template[name]?.dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
dynamic={data.node?.template[name]?.dynamic ?? false}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + name}
/>
</div>
) : left === true && type === "file" ? (
</Case>
<Case condition={left === true && type === "file"}>
<div className="mt-2 w-full">
<InputFileComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
fileTypes={data.node?.template[name].fileTypes}
fileTypes={data.node?.template[name]?.fileTypes}
onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath;
}}
></InputFileComponent>
</div>
) : left === true && type === "int" ? (
</Case>
<Case condition={left === true && type === "int"}>
<div className="mt-2 w-full">
<IntComponent
rangeSpec={data.node?.template[name].rangeSpec}
rangeSpec={data.node?.template[name]?.rangeSpec}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + name}
/>
</div>
) : left === true && type === "prompt" ? (
</Case>
<Case condition={left === true && type === "prompt"}>
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={data.node?.flow ? true : false}
@ -758,57 +573,59 @@ export default function ParameterComponent({
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
value={data.node?.template[name]?.value ?? ""}
onChange={handleOnNewValue}
id={"prompt-input-" + name}
data-testid={"prompt-input-" + name}
/>
</div>
) : left === true && type === "NestedDict" ? (
</Case>
<Case condition={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() === "{}"
!data.node!.template[name]?.value ||
data.node!.template[name]?.value?.toString() === "{}"
? {
// yourkey: "value",
}
: data.node!.template[name].value
: data.node!.template[name]?.value
}
onChange={handleOnNewValue}
id="div-dict-input"
/>
</div>
) : left === true && type === "dict" ? (
</Case>
<Case condition={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
data.node!.template[name]?.value?.length === 0 ||
!data.node!.template[name]?.value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value)
: convertObjToArray(data.node!.template[name]?.value, type!)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers = convertValuesToNumbers(newValue);
setErrorDuplicateKey(hasDuplicateKeys(valueToNumbers));
// if data.node?.template[name].list is true, then the value is an array of objects
// if data.node?.template[name]?.list is true, then the value is an array of objects
// else we need to get the first object of the array
if (data.node?.template[name].list) {
if (data.node?.template[name]?.list) {
handleOnNewValue(valueToNumbers);
} else handleOnNewValue(valueToNumbers[0]);
}}
isList={data.node?.template[name].list ?? false}
isList={data.node?.template[name]?.list ?? false}
/>
</div>
) : (
<></>
)}
</Case>
</>
</div>
);

View file

@ -0,0 +1,91 @@
import React from "react";
import {
INPUT_HANDLER_HOVER,
OUTPUT_HANDLER_HOVER,
} from "../../../../constants/constants";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { classNames } from "../../../../utils/utils";
const TooltipRenderComponent = ({ item, index, left }) => {
const Icon = nodeIconsLucide[item.family] ?? nodeIconsLucide["unknown"];
return (
<div
key={index}
data-testid={`available-${left ? "input" : "output"}-${item.family}`}
>
{index === 0 && (
<span>{left ? INPUT_HANDLER_HOVER : OUTPUT_HANDLER_HOVER}</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"
data-testid={`tooltip-${nodeNames[item.family] ?? "Other"}`}
>
{nodeNames[item.family] ?? "Other"}{" "}
{item?.display_name && item?.display_name?.length > 0 ? (
<span
className="text-xs"
data-testid={`tooltip-${item?.display_name}`}
>
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span className="text-xs" data-testid={`tooltip-${item?.type}`}>
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + name}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
);
};
export default TooltipRenderComponent;

View file

@ -27,8 +27,10 @@ import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, cn, getFieldTitle, sortFields } from "../../utils/utils";
import { classNames, cn } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
import getFieldTitle from "../utils/get-field-title";
import sortFields from "../utils/sort-fields";
export default function GenericNode({
data,
@ -303,7 +305,7 @@ export default function GenericNode({
) => {
let isInvalid = validationStatus && !validationStatus.valid;
if (buildStatus === BuildStatus.INACTIVE && isInvalid) {
if (buildStatus === BuildStatus.INACTIVE) {
// INACTIVE should have its own class
return "inactive-status";
}
@ -329,6 +331,7 @@ export default function GenericNode({
buildStatus,
validationStatus,
);
const baseBorderClass = getBaseBorderClass(selected);
const nodeSizeClass = getNodeSizeClass(showNode);
return classNames(
@ -383,7 +386,6 @@ export default function GenericNode({
isOutdated,
selected,
]);
return (
<>
{memoizedNodeToolbarComponent}

View file

@ -0,0 +1,54 @@
import { cloneDeep } from "lodash";
import { useEffect } from "react";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api";
const useFetchDataOnMount = (
data,
name,
handleUpdateValues,
setNode,
renderTooltips,
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
useEffect(() => {
async function fetchData() {
if (
(data.node?.template[name]?.real_time_refresh ||
data.node?.template[name]?.refresh_button) &&
// options can be undefined but not an empty array
(data.node?.template[name]?.options?.length ?? 0) === 0
) {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
}
}
fetchData();
}, []); // Empty dependency array ensures that this effect runs only once, on mount
};
export default useFetchDataOnMount;

View file

@ -0,0 +1,75 @@
import { cloneDeep } from "lodash";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorTypeAPI } from "../../types/api";
const useHandleOnNewValue = (
data,
name,
takeSnapshot,
handleUpdateValues,
debouncedHandleUpdateValues,
setNode,
renderTooltips,
isLoading,
setIsLoading,
) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleOnNewValue = async (newValue, skipSnapshot = false) => {
const nodeTemplate = data.node!.template[name];
const currentValue = nodeTemplate.value;
if (currentValue !== newValue && !skipSnapshot) {
takeSnapshot();
}
const shouldUpdate =
data.node?.template[name].real_time_refresh &&
!data.node?.template[name].refresh_button &&
currentValue !== newValue;
const typeToDebounce = nodeTemplate.type;
nodeTemplate.value = newValue;
let newTemplate;
if (shouldUpdate) {
setIsLoading(true);
try {
if (["int"].includes(typeToDebounce)) {
newTemplate = await handleUpdateValues(name, data);
} else {
newTemplate = await debouncedHandleUpdateValues(name, data);
}
} catch (error) {
let responseError = error as ResponseErrorTypeAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail.error ?? "Unknown error"],
});
}
setIsLoading(false);
}
setNode(data.id, (oldNode) => {
const newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
if (data.node?.template[name].real_time_refresh && newTemplate) {
newNode.data.node.template = newTemplate;
} else {
newNode.data.node.template[name].value = newValue;
}
return newNode;
});
renderTooltips();
};
return { handleOnNewValue };
};
export default useHandleOnNewValue;

View file

@ -0,0 +1,40 @@
import { cloneDeep } from "lodash";
const useHandleNodeClass = (
data,
name,
takeSnapshot,
setNode,
updateNodeInternals,
renderTooltips,
) => {
const handleNodeClass = (newNodeClass, code) => {
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();
};
return { handleNodeClass };
};
export default useHandleNodeClass;

View file

@ -0,0 +1,39 @@
import { cloneDeep } from "lodash";
import useAlertStore from "../../stores/alertStore";
import { ResponseErrorDetailAPI } from "../../types/api";
import { handleUpdateValues } from "../../utils/parameterUtils";
const useHandleRefreshButtonPress = (setIsLoading, setNode, renderTooltips) => {
const setErrorData = useAlertStore((state) => state.setErrorData);
const handleRefreshButtonPress = async (name, data) => {
setIsLoading(true);
try {
let newTemplate = await handleUpdateValues(name, data);
if (newTemplate) {
setNode(data.id, (oldNode) => {
let newNode = cloneDeep(oldNode);
newNode.data = {
...newNode.data,
};
newNode.data.node.template = newTemplate;
return newNode;
});
}
} catch (error) {
let responseError = error as ResponseErrorDetailAPI;
setErrorData({
title: "Error while updating the Component",
list: [responseError.response.data.detail ?? "Unknown error"],
});
}
setIsLoading(false);
renderTooltips();
};
return { handleRefreshButtonPress };
};
export default useHandleRefreshButtonPress;

View file

@ -0,0 +1,10 @@
import { APITemplateType } from "../../types/api";
export default function getFieldTitle(
template: APITemplateType,
templateField: string,
): string {
return template[templateField].display_name
? template[templateField].display_name!
: template[templateField].name ?? templateField;
}

View file

@ -0,0 +1,40 @@
import { priorityFields } from "../../constants/constants";
export default function sortFields(a, b, fieldOrder) {
// Early return for empty fields
if (!a && !b) return 0;
if (!a) return 1;
if (!b) return -1;
// Normalize the case to ensure case-insensitive comparison
const normalizedFieldA = a.toLowerCase();
const normalizedFieldB = b.toLowerCase();
const aIsPriority = priorityFields.has(normalizedFieldA);
const bIsPriority = priorityFields.has(normalizedFieldB);
// Sort by priority
if (aIsPriority && !bIsPriority) return -1;
if (!aIsPriority && bIsPriority) return 1;
// Check if either field is in the fieldOrder array
const indexOfA = fieldOrder.indexOf(normalizedFieldA);
const indexOfB = fieldOrder.indexOf(normalizedFieldB);
// If both fields are in fieldOrder, sort by their order in the array
if (indexOfA !== -1 && indexOfB !== -1) {
return indexOfA - indexOfB;
}
// If only one of the fields is in fieldOrder, that field comes first
if (indexOfA !== -1) {
return -1;
}
if (indexOfB !== -1) {
return 1;
}
// Default case for fields not in priorityFields and not found in fieldOrder
// You might want to sort them alphabetically or in another specific manner
return a.localeCompare(b);
}

View file

@ -0,0 +1,14 @@
import { useEffect } from "react";
import { useLocation } from "react-router-dom";
import { useLocationStore } from "../stores/locationStore";
function useTrackLastVisitedPath() {
const location = useLocation();
const setHistory = useLocationStore((state) => state.setRouteHistory);
useEffect(() => {
setHistory(location.pathname);
}, [location]);
}
export default useTrackLastVisitedPath;

View file

@ -0,0 +1,17 @@
const SvgCouchbaseIcon = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
preserveAspectRatio="xMidYMid"
viewBox="0 0 256 256"
{...props}
>
<path
fill="#ED2226"
d="M128 0C57.426 0 0 57.233 0 128c0 70.574 57.233 128 128 128 70.574 0 128-57.233 128-128S198.574 0 128 0zm86.429 150.429c0 7.734-4.447 14.502-13.148 16.048-15.082 2.707-46.792 4.254-73.281 4.254-26.49 0-58.2-1.547-73.281-4.254-8.7-1.546-13.148-8.314-13.148-16.048v-49.885c0-7.734 5.994-14.888 13.148-16.049 4.447-.773 14.888-1.546 23.01-1.546 3.093 0 5.606 2.32 5.606 5.994v34.997l44.858-.967 44.858.967V88.943c0-3.674 2.514-5.994 5.608-5.994 8.12 0 18.562.773 23.009 1.546 7.347 1.16 13.148 8.315 13.148 16.049-.387 16.435-.387 33.257-.387 49.885z"
/>
</svg>
);
export default SvgCouchbaseIcon;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="2500" height="2500" preserveAspectRatio="xMidYMid" viewBox="0 0 256 256" id="couchbase"><path fill="#ED2226" d="M128 0C57.426 0 0 57.233 0 128c0 70.574 57.233 128 128 128 70.574 0 128-57.233 128-128S198.574 0 128 0zm86.429 150.429c0 7.734-4.447 14.502-13.148 16.048-15.082 2.707-46.792 4.254-73.281 4.254-26.49 0-58.2-1.547-73.281-4.254-8.7-1.546-13.148-8.314-13.148-16.048v-49.885c0-7.734 5.994-14.888 13.148-16.049 4.447-.773 14.888-1.546 23.01-1.546 3.093 0 5.606 2.32 5.606 5.994v34.997l44.858-.967 44.858.967V88.943c0-3.674 2.514-5.994 5.608-5.994 8.12 0 18.562.773 23.009 1.546 7.347 1.16 13.148 8.315 13.148 16.049-.387 16.435-.387 33.257-.387 49.885z"></path></svg>

After

Width:  |  Height:  |  Size: 720 B

View file

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

View file

@ -56,7 +56,11 @@ export const GradientGroup = (props) => {
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent name="Combine" stroke="url(#grad3)" {...props} />
<ForwardedIconComponent
name="Combine"
stroke={`${props.disabled ? "#64748B" : "url(#grad3)"}`}
{...props}
/>
</>
);
};

View file

@ -0,0 +1,140 @@
import { useEffect, useState } from "react";
import InputComponent from "../../../components/inputComponent";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "../../../components/ui/form";
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { Textarea } from "../../../components/ui/textarea";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
type FolderFormsProps = {
control: any;
setValue: any;
folderToEdit: any;
};
export const FolderForms = ({
control,
setValue,
folderToEdit,
}: FolderFormsProps): JSX.Element => {
const flows = useFlowsManagerStore((state) => state.flows);
const [selectedComponents, setSelectedComponents] = useState<string[]>([]);
const [selectedFlows, setSelectedFlows] = useState<string[]>([]);
const componentsList = flows
.filter((flow) => flow.is_component && flow.folder_id !== null)
.map((flow) => ({ id: flow.id, name: flow.name }));
const flowsList = flows
.filter((flow) => !flow.is_component && flow.folder_id !== null)
.map((flow) => ({ id: flow.id, name: flow.name }));
useEffect(() => {
setValue("components", selectedComponents);
setValue("flows", selectedFlows);
}, [selectedComponents, selectedFlows]);
useEffect(() => {
if (folderToEdit) {
setValue("name", folderToEdit.name);
setValue("description", folderToEdit.description);
console.log(folderToEdit);
// setSelectedComponents(folderToEdit.components);
// setSelectedFlows(folderToEdit.flows);
return;
}
setValue("name", "");
setValue("description", "");
setSelectedComponents([]);
setSelectedFlows([]);
}, [folderToEdit]);
return (
<>
<div className="flex h-full w-full flex-col gap-4 align-middle">
<Label>Folder Name</Label>
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
value={field.value}
onChange={field.onChange}
placeholder="Insert a name for the folder..."
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Label>Description (optional) </Label>
<FormField
control={control}
name="description"
render={({ field }) => (
<FormControl>
<Textarea
value={field.value}
onChange={field.onChange}
placeholder="Insert a description for the folder..."
></Textarea>
</FormControl>
)}
/>
<Label>Add Flows</Label>
<FormField
control={control}
name="flows"
render={() => (
<FormControl>
<InputComponent
isObjectOption
password={false}
objectOptions={flowsList}
placeholder="Choose a flow to add..."
id="input-flow"
setSelectedOptions={(value: any) => setSelectedFlows(value)}
selectedOptions={selectedFlows}
></InputComponent>
</FormControl>
)}
/>
<Label>Add Components</Label>
<FormField
control={control}
name="components"
render={() => (
<FormControl>
<InputComponent
isObjectOption
password={false}
objectOptions={componentsList}
placeholder="Choose a component to add..."
id="input-component"
setSelectedOptions={(value) => setSelectedComponents(value)}
selectedOptions={selectedComponents}
></InputComponent>
</FormControl>
)}
/>
</div>
</>
);
};
export default FolderForms;

View file

@ -0,0 +1,10 @@
import { z } from "zod";
export const FolderFormsSchema = z.object({
name: z.string().min(1, {
message: "Name must be at least 1 characters.",
}),
description: z.string().optional(),
components: z.array(z.string()),
flows: z.array(z.string()),
});

View file

@ -0,0 +1,53 @@
import { addFolder, updateFolder } from "../../../pages/MainPage/services";
import useAlertStore from "../../../stores/alertStore";
import { useFolderStore } from "../../../stores/foldersStore";
const useFolderSubmit = (setOpen, folderToEdit) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const onSubmit = (data) => {
if (folderToEdit) {
updateFolder(data, folderToEdit?.id!).then(
() => {
setSuccessData({
title: "Folder updated successfully.",
});
getFoldersApi(true);
setOpen(false);
},
(reason) => {
if (reason) {
setErrorData({
title: `Error updating folder.`,
});
console.error(reason);
} else {
getFoldersApi(true);
setOpen(false);
}
},
);
} else {
addFolder(data).then(
() => {
setSuccessData({
title: "Folder created successfully.",
});
getFoldersApi(true);
setOpen(false);
},
() => {
setErrorData({
title: `Error creating folder.`,
});
},
);
}
};
return { onSubmit, open, setOpen };
};
export default useFolderSubmit;

View file

@ -0,0 +1,79 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import ForwardedIconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Form } from "../../components/ui/form";
import { useFolderStore } from "../../stores/foldersStore";
import BaseModal from "../baseModal";
import FolderForms from "./component";
import { FolderFormsSchema } from "./entities";
import useFolderSubmit from "./hooks/submit-folder";
type FoldersModalProps = {
open: boolean;
setOpen: (open: boolean) => void;
};
export default function FoldersModal({
open,
setOpen,
}: FoldersModalProps): JSX.Element {
const form = useForm<z.infer<typeof FolderFormsSchema>>({
resolver: zodResolver(FolderFormsSchema),
defaultValues: {
name: "",
description: "",
components: [],
flows: [],
},
mode: "all",
});
const folderToEdit = useFolderStore((state) => state.folderToEdit);
const { onSubmit: onSubmitFolder } = useFolderSubmit(setOpen, folderToEdit);
const onSubmit = (data) => {
onSubmitFolder(data);
};
return (
<>
<BaseModal size="x-small" open={open} setOpen={setOpen}>
<BaseModal.Header
description={`${folderToEdit ? "Edit a folder" : "Add a new folder"}`}
>
<span className="pr-2" data-testid="modal-title">
{folderToEdit ? "Edit" : "New"} Folder
</span>
<ForwardedIconComponent
name="Plus"
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FolderForms
folderToEdit={folderToEdit}
control={form.control}
setValue={form.setValue}
/>
<Button
className="float-right mt-6"
type="submit"
disabled={!form.formState.isValid}
>
{folderToEdit ? "Edit" : "Save"} Folder
</Button>
</form>
</Form>
</div>
</BaseModal.Content>
</BaseModal>
</>
);
}

View file

@ -4,6 +4,7 @@ import ImageViewer from "../../../../components/ImageViewer";
import CsvOutputComponent from "../../../../components/csvOutputComponent";
import InputListComponent from "../../../../components/inputListComponent";
import PdfViewer from "../../../../components/pdfViewer";
import RecordsOutputComponent from "../../../../components/recordsOutputComponent";
import {
Select,
SelectContent,
@ -47,6 +48,7 @@ export default function IOFieldView({
}
}
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
function handleOutputType() {
@ -204,7 +206,7 @@ export default function IOFieldView({
<SelectItem key={separator} value={separator}>
{separator}
</SelectItem>
)
),
)}
</SelectGroup>
</SelectContent>
@ -280,6 +282,15 @@ export default function IOFieldView({
/>
</>
);
case "RecordsOutput":
return (
<div className={left ? "h-56" : "h-full"}>
<RecordsOutputComponent
flowPool={flowPoolNode}
pagination={!left}
/>
</div>
);
default:
return (

View file

@ -114,19 +114,19 @@ export default function ChatMessage({
<div
className={classNames(
"form-modal-chat-position",
chat.isSend ? "" : " "
chat.isSend ? "" : " ",
)}
>
<div
className={classNames(
"mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3"
"mr-3 mt-1 flex w-24 flex-col items-center gap-1 overflow-hidden px-3 pb-3",
)}
>
<div className="flex flex-col items-center gap-1">
<div
className={cn(
"relative flex h-8 w-8 items-center justify-center overflow-hidden rounded-md p-5 text-2xl",
!chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon"
!chat.isSend ? "bg-chat-bot-icon" : "bg-chat-user-icon",
)}
>
<img
@ -210,12 +210,12 @@ dark:prose-invert"
children[0] = (children[0] as string).replace(
"`▍`",
"▍"
"▍",
);
}
const match = /language-(\w+)/.exec(
className || ""
className || "",
);
return !inline ? (
@ -230,7 +230,7 @@ dark:prose-invert"
language: (match && match[1]) || "",
code: String(children).replace(
/\n$/,
""
"",
),
},
]}
@ -248,7 +248,7 @@ dark:prose-invert"
{chatMessage}
</Markdown>
),
[chat.message, chatMessage]
[chat.message, chatMessage],
)}
</div>
{chat.files && (
@ -306,7 +306,7 @@ dark:prose-invert"
parts.push(
<span className="chat-message-highlight">
{chat.message[match[1]]}
</span>
</span>,
);
}
@ -324,7 +324,7 @@ dark:prose-invert"
</>
) : (
<span
className="prose text-primary word-break-break-word dark:prose-invert"
className="prose text-primary word-break-break-word dark:prose-invert"
data-testid={
"chat-message-" + chat.sender_name + "-" + chatMessage
}

View file

@ -0,0 +1,26 @@
/**
* Function to get the curl code for the API
* @param {string} flowId - The id of the flow
* @param {boolean} isAuth - If the API is authenticated
* @returns {string} - The curl code
*/
export default function getCurlCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `curl -X POST \\
${window.location.protocol}//${
window.location.host
}/api/v1/run/${flowId}?stream=false \\
-H 'Content-Type: application/json'\\${
!isAuth ? `\n -H 'x-api-key: <your api key>'\\` : ""
}
-d '{"input_value": "message",
"output_type": "chat",
"input_type": "chat",
"tweaks": ${JSON.stringify(tweaksObject, null, 2)}'
`;
}

View file

@ -0,0 +1,58 @@
/**
* Function to get the python code for the API
* @param {string} flowId - The id of the flow
* @param {boolean} isAuth - If the API is authenticated
* @param {any[]} tweak - The tweaks
* @returns {string} - The python code
*/
export default function getPythonApiCode(
flowId: string,
isAuth: boolean,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `import requests
from typing import Optional
BASE_API_URL = "${window.location.protocol}//${window.location.host}/api/v1/run"
FLOW_ID = "${flowId}"
# You can tweak the flow by adding a tweaks dictionary
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
def run_flow(message: str,
flow_id: str,
output_type: str = "chat",
input_type: str = "chat",
tweaks: Optional[dict] = None,
api_key: Optional[str] = None) -> dict:
"""
Run a flow with a given message and optional tweaks.
:param message: The message to send to the flow
:param flow_id: The ID of the flow to run
:param tweaks: Optional tweaks to customize the flow
:return: The JSON response from the flow
"""
api_url = f"{BASE_API_URL}/{flow_id}"
payload = {
"input_value": message,
"output_type": output_type,
"input_type": input_type,
}
headers = None
if tweaks:
payload["tweaks"] = tweaks
if api_key:
headers = {"x-api-key": api_key}
response = requests.post(api_url, json=payload, headers=headers)
return response.json()
# Setup any tweaks you want to apply to the flow
message = "message"
${!isAuth ? `api_key = "<your api key>"` : ""}
print(run_flow(message=message, flow_id=FLOW_ID, tweaks=TWEAKS${
!isAuth ? `, api_key=api_key` : ""
}))`;
}

View file

@ -0,0 +1,20 @@
/**
* Function to get the python code for the API
* @param {string} flow - The current flow
* @param {any[]} tweak - The tweaks
* @returns {string} - The python code
*/
export default function getPythonCode(
flowName: string,
tweaksBuildedObject,
): string {
const tweaksObject = tweaksBuildedObject[0];
return `from langflow.load import run_flow_from_json
TWEAKS = ${JSON.stringify(tweaksObject, null, 2)}
result = run_flow_from_json(flow="${flowName}.json",
input_value="message",
fallback_to_env_vars=True, # False by default
tweaks=TWEAKS)`;
}

View file

@ -0,0 +1,24 @@
/**
* Function to get the widget code for the API
* @param {string} flow - The current flow.
* @returns {string} - The widget code
*/
export default function getWidgetCode(
flowId: string,
flowName: string,
isAuth: boolean,
): string {
return `<script src="https://cdn.jsdelivr.net/gh/langflow-ai/langflow-embedded-chat@1.0_alpha/dist/build/static/js/bundle.min.js"></script>
<langflow-chat
window_title="${flowName}"
flow_id="${flowId}"
host_url="http://localhost:7860"${
!isAuth
? `
api_key="..."`
: ""
}
></langflow-chat>`;
}

View file

@ -0,0 +1,78 @@
export default function tabsArray(codes: string[], method: number) {
if (!method) return;
if (method === 0) {
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
];
}
return [
{
name: "cURL",
mode: "bash",
image: "https://curl.se/logo/curl-symbol-transparent.png",
language: "sh",
code: codes[0],
},
{
name: "Python API",
mode: "python",
image:
"https://images.squarespace-cdn.com/content/v1/5df3d8c5d2be5962e4f87890/1628015119369-OY4TV3XJJ53ECO0W2OLQ/Python+API+Training+Logo.png?format=1000w",
language: "py",
code: codes[1],
},
{
name: "Python Code",
mode: "python",
language: "py",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
code: codes[2],
},
{
name: "Chat Widget HTML",
description:
"Insert this code anywhere in your &lt;body&gt; tag. To use with react and other libs, check our <a class='link-color' href='https://langflow.org/guidelines/widget'>documentation</a>.",
mode: "html",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[3],
},
{
name: "Tweaks",
mode: "python",
image: "https://cdn-icons-png.flaticon.com/512/5968/5968350.png",
language: "py",
code: codes[4],
},
];
}

View file

@ -13,13 +13,6 @@ import { useTweaksStore } from "../../../stores/tweaksStore";
import { TemplateVariableType } from "../../../types/api";
import { uniqueTweakType } from "../../../types/components";
import { FlowType } from "../../../types/flow/index";
import {
getCurlCode,
getPythonApiCode,
getPythonCode,
getWidgetCode,
tabsArray,
} from "../../../utils/utils";
import BaseModal from "../../baseModal";
import { buildContent } from "../utils/build-content";
import { buildTweaks } from "../utils/build-tweaks";
@ -27,6 +20,11 @@ import { checkCanBuildTweakObject } from "../utils/check-can-build-tweak-object"
import { getChangesType } from "../utils/get-changes-types";
import { getNodesWithDefaultValue } from "../utils/get-nodes-with-default-value";
import { getValue } from "../utils/get-value";
import getPythonApiCode from "../utils/get-python-api-code";
import getCurlCode from "../utils/get-curl-code";
import getPythonCode from "../utils/get-python-code";
import getWidgetCode from "../utils/get-widget-code";
import tabsArray from "../utils/tabs-array";
const ApiModal = forwardRef(
(
@ -201,6 +199,7 @@ const ApiModal = forwardRef(
}}
activeTweaks={activeTweaks}
setActiveTweaks={setActiveTweaks}
allowExport
/>
</BaseModal.Content>
</BaseModal>

View file

@ -25,15 +25,21 @@ type TriggerProps = {
children: ReactNode;
asChild?: boolean;
disable?: boolean;
className?: string;
};
const Content: React.FC<ContentProps> = ({ children }) => {
return <div className="flex h-full w-full flex-col">{children}</div>;
};
const Trigger: React.FC<TriggerProps> = ({ children, asChild, disable }) => {
const Trigger: React.FC<TriggerProps> = ({
children,
asChild,
disable,
className,
}) => {
return (
<DialogTrigger
className={asChild ? "" : "w-full"}
className={asChild ? "" : cn("w-full", className)}
hidden={children ? false : true}
disabled={disable}
asChild={asChild}
@ -113,7 +119,7 @@ function BaseModal({
switch (size) {
case "x-small":
minWidth = "min-w-[20vw]";
height = " ";
height = "h-full";
break;
case "smaller":
minWidth = "min-w-[40vw]";
@ -129,6 +135,7 @@ function BaseModal({
break;
case "small-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "medium":
minWidth = "min-w-[60vw]";
@ -136,6 +143,8 @@ function BaseModal({
break;
case "medium-h-full":
minWidth = "min-w-[60vw]";
height = "h-full";
break;
case "large":
minWidth = "min-w-[85vw]";
@ -162,6 +171,7 @@ function BaseModal({
case "large-h-full":
minWidth = "min-w-[80vw]";
height = "h-full";
break;
default:
minWidth = "min-w-[80vw]";
@ -186,7 +196,7 @@ function BaseModal({
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
className={`flex flex-col ${height} w-full transition-all duration-300`}
>
{ContentChild}
</div>
@ -203,7 +213,7 @@ function BaseModal({
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
className={`flex flex-col ${height} w-full transition-all duration-300`}
>
{ContentChild}
</div>

View file

@ -17,6 +17,7 @@ export default function DeleteConfirmationModal({
asChild,
open,
setOpen,
note = "",
}: {
children: JSX.Element;
onConfirm: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
@ -24,6 +25,7 @@ export default function DeleteConfirmationModal({
asChild?: boolean;
open?: boolean;
setOpen?: (open: boolean) => void;
note?: string;
}) {
return (
<Dialog open={open} onOpenChange={setOpen}>
@ -43,7 +45,14 @@ export default function DeleteConfirmationModal({
</DialogTitle>
</DialogHeader>
<span>
Confirm deletion of {description ?? "component"}?<br></br>
Are you sure you want to delete the selected{" "}
{description ?? "component"}?<br></br>
{note && (
<>
{note}
<br></br>
</>
)}
Note: This action is irreversible.
</span>
<DialogFooter>

View file

@ -18,6 +18,10 @@ export default function DictAreaModal({
children,
onChange,
value,
}: {
children: JSX.Element;
onChange?: (value: Object) => void;
value: Object;
}): JSX.Element {
const [open, setOpen] = useState(false);
const isDark = useDarkStore((state) => state.dark);
@ -29,9 +33,13 @@ export default function DictAreaModal({
return (
<BaseModal size="medium-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={CODE_DICT_DIALOG_SUBTITLE}>
<span className="pr-2">Edit Dictionary</span>
<BaseModal.Trigger className="h-full">{children}</BaseModal.Trigger>
<BaseModal.Header
description={onChange ? CODE_DICT_DIALOG_SUBTITLE : null}
>
<span className="pr-2">
{onChange ? "Edit Dictionary" : "View Dictionary"}
</span>
<IconComponent
name="BookMarked"
className="h-6 w-6 pl-1 text-primary "
@ -44,7 +52,7 @@ export default function DictAreaModal({
theme="vscode"
dark={isDark}
className={!isDark ? "json-view-white" : "json-view-dark"}
editable
editable={!!onChange}
enableClipboard
onEdit={(edit) => {
ref.current = edit["src"];
@ -54,19 +62,21 @@ export default function DictAreaModal({
}}
src={ref.current}
/>
<div className="flex h-fit w-full justify-end">
<Button
data-testid="save-dict-button"
className="mt-3"
type="submit"
onClick={() => {
onChange(ref.current);
setOpen(false);
}}
>
Save
</Button>
</div>
{onChange && (
<div className="flex h-fit w-full justify-end">
<Button
data-testid="save-dict-button"
className="mt-3"
type="submit"
onClick={() => {
onChange(ref.current);
setOpen(false);
}}
>
Save
</Button>
</div>
)}
</div>
</BaseModal.Content>
</BaseModal>

View file

@ -28,6 +28,7 @@ import {
LANGFLOW_SUPPORTED_TYPES,
limitScrollFieldsModal,
} from "../../constants/constants";
import { Case } from "../../shared/components/caseComponent";
import useFlowStore from "../../stores/flowStore";
import { NodeDataType } from "../../types/flow";
import {
@ -52,7 +53,7 @@ const EditNodeModal = forwardRef(
open: boolean;
setOpen: (open: boolean) => void;
},
ref
ref,
) => {
const [myData, setMyData] = useState(data);
@ -84,6 +85,10 @@ const EditNodeModal = forwardRef(
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const type = (templateParam) => {
return myData.node?.template[templateParam].type;
};
return (
<BaseModal
key={data.id}
@ -116,7 +121,7 @@ const EditNodeModal = forwardRef(
"edit-node-modal-box",
nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll"
: ""
: "",
)}
>
{nodeLength > 0 && (
@ -138,8 +143,8 @@ const EditNodeModal = forwardRef(
templateParam.charAt(0) !== "_" &&
myData.node?.template[templateParam].show &&
LANGFLOW_SUPPORTED_TYPES.has(
myData.node.template[templateParam].type
)
myData.node!.template[templateParam].type,
),
)
.map((templateParam, index) => {
let id = {
@ -161,8 +166,8 @@ const EditNodeModal = forwardRef(
myData.node?.template[templateParam]
.proxy,
}
: id
)
: id,
),
) ?? false;
return (
<TableRow
@ -170,8 +175,7 @@ const EditNodeModal = forwardRef(
className={
"h-10 " +
((templateParam === "code" &&
myData.node?.template[templateParam].type ===
"code") ||
type(templateParam) === "code") ||
(templateParam.includes("code") &&
myData.node?.template[templateParam].proxy)
? " hidden "
@ -190,7 +194,7 @@ const EditNodeModal = forwardRef(
<span>
{myData.node?.template[templateParam]
.display_name
? myData.node.template[templateParam]
? myData.node!.template[templateParam]
.display_name
: myData.node?.template[templateParam]
.name}
@ -198,58 +202,62 @@ const EditNodeModal = forwardRef(
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.node?.template[templateParam].type ===
"str" &&
!myData.node.template[templateParam].options ? (
<Case
condition={
type(templateParam) === "str" &&
!myData.node!.template[templateParam]
.options
}
>
<div className="mx-auto">
{myData.node.template[templateParam]
{myData.node!.template[templateParam]
?.list ? (
<InputListComponent
componentName={templateParam}
editNode={true}
disabled={disabled}
value={
!myData.node.template[templateParam]
!myData.node!.template[templateParam]
.value ||
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value === ""
? [""]
: myData.node.template[
: myData.node!.template[
templateParam
].value
}
onChange={(value: string[]) => {
handleOnNewValue(
value,
templateParam
templateParam,
);
}}
/>
) : myData.node.template[templateParam]
) : myData.node!.template[templateParam]
.multiline ? (
<TextAreaComponent
id={
"textarea-edit-" +
myData.node.template[templateParam]
myData.node!.template[templateParam]
.name
}
data-testid={
"textarea-edit-" +
myData.node.template[templateParam]
myData.node!.template[templateParam]
.name
}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(
value: string | string[]
value: string | string[],
) => {
handleOnNewValue(
value,
templateParam
templateParam,
);
}}
/>
@ -274,8 +282,13 @@ const EditNodeModal = forwardRef(
/>
)}
</div>
) : myData.node?.template[templateParam]
.type === "NestedDict" ? (
</Case>
<Case
condition={
type(templateParam) === "NestedDict"
}
>
<div className=" w-full">
<DictComponent
disabled={disabled}
@ -296,21 +309,24 @@ const EditNodeModal = forwardRef(
].value = newValue;
handleOnNewValue(
newValue,
templateParam
templateParam,
);
}}
id="editnode-div-dict-input"
/>
</div>
) : myData.node?.template[templateParam]
.type === "dict" ? (
</Case>
<Case
condition={type(templateParam) === "dict"}
>
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
myData.node!.template[templateParam].value
?.length > 1
? "my-3"
: ""
: "",
)}
>
<KeypairListComponent
@ -325,7 +341,8 @@ const EditNodeModal = forwardRef(
: convertObjToArray(
myData.node!.template[
templateParam
].value
].value,
type(templateParam)!,
)
}
duplicateKey={errorDuplicateKey}
@ -336,11 +353,11 @@ const EditNodeModal = forwardRef(
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers)
hasDuplicateKeys(valueToNumbers),
);
handleOnNewValue(
valueToNumbers,
templateParam
templateParam,
);
}}
isList={
@ -349,32 +366,39 @@ const EditNodeModal = forwardRef(
}
/>
</div>
) : myData.node?.template[templateParam]
.type === "bool" ? (
</Case>
<Case
condition={type(templateParam) === "bool"}
>
<div className="ml-auto">
{" "}
<ToggleShadComponent
id={
"toggle-edit-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
disabled={disabled}
enabled={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value
}
setEnabled={(isEnabled) => {
handleOnNewValue(
isEnabled,
templateParam
templateParam,
);
}}
size="small"
editNode={true}
/>
</div>
) : myData.node?.template[templateParam]
.type === "float" ? (
</Case>
<Case
condition={type(templateParam) === "float"}
>
<div className="mx-auto">
<FloatComponent
disabled={disabled}
@ -384,7 +408,7 @@ const EditNodeModal = forwardRef(
.rangeSpec
}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value) => {
@ -392,32 +416,38 @@ const EditNodeModal = forwardRef(
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "str" &&
myData.node.template[templateParam]
.options ? (
</Case>
<Case
condition={
type(templateParam) === "str" &&
myData.node!.template[templateParam].options
}
>
<div className="mx-auto">
<Dropdown
editNode={true}
options={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.options
}
onSelect={(value) =>
handleOnNewValue(value, templateParam)
}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? "Choose an option"
}
id={
"dropdown-edit-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
></Dropdown>
</div>
) : myData.node?.template[templateParam]
.type === "int" ? (
</Case>
<Case condition={type(templateParam) === "int"}>
<div className="mx-auto">
<IntComponent
rangeSpec={
@ -426,12 +456,13 @@ const EditNodeModal = forwardRef(
}
id={
"edit-int-input-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value) => {
@ -439,21 +470,24 @@ const EditNodeModal = forwardRef(
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "file" ? (
</Case>
<Case
condition={type(templateParam) === "file"}
>
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.fileTypes
}
onFileChange={(filePath: string) => {
@ -463,8 +497,11 @@ const EditNodeModal = forwardRef(
}}
></InputFileComponent>
</div>
) : myData.node?.template[templateParam]
.type === "prompt" ? (
</Case>
<Case
condition={type(templateParam) === "prompt"}
>
<div className="mx-auto">
<PromptAreaComponent
readonly={
@ -478,7 +515,7 @@ const EditNodeModal = forwardRef(
myData.node = nodeClass;
}}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
@ -486,21 +523,26 @@ const EditNodeModal = forwardRef(
}}
id={
"prompt-area-edit-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
data-testid={
"modal-prompt-input-" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
/>
</div>
) : myData.node?.template[templateParam]
.type === "code" ? (
</Case>
<Case
condition={type(templateParam) === "code"}
>
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.node?.flow &&
myData.node.template[templateParam]
myData.node!.template[templateParam]
.dynamic
? true
: false
@ -516,7 +558,7 @@ const EditNodeModal = forwardRef(
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
myData.node!.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
@ -524,16 +566,16 @@ const EditNodeModal = forwardRef(
}}
id={
"code-area-edit" +
myData.node.template[templateParam].name
myData.node!.template[templateParam]
.name
}
/>
</div>
) : myData.node?.template[templateParam]
.type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</Case>
<Case condition={type(templateParam) === "Any"}>
<>-</>
</Case>
</TableCell>
<TableCell className="p-0 text-right">
<div className="items-center text-center">
@ -588,7 +630,7 @@ const EditNodeModal = forwardRef(
</BaseModal.Footer>
</BaseModal>
);
}
},
);
export default EditNodeModal;

View file

@ -0,0 +1,129 @@
import { useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Tabs, TabsList, TabsTrigger } from "../../components/ui/tabs";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import { FlowSettingsPropsType } from "../../types/components";
import { FlowType, NodeDataType } from "../../types/flow";
import BaseModal from "../baseModal";
import TableComponent from "../../components/tableComponent";
import { getMessagesTable, getTransactionTable } from "../../controllers/API";
import {
ColDef,
ColGroupDef,
SizeColumnsToFitGridStrategy,
} from "ag-grid-community";
import useAlertStore from "../../stores/alertStore";
import useFlowStore from "../../stores/flowStore";
export default function FlowLogsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
const saveFlow = useFlowsManagerStore((state) => state.saveFlow);
const nodes = useFlowStore((state) => state.nodes);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const currentFlowId = useFlowsManagerStore((state) => state.currentFlowId);
const flows = useFlowsManagerStore((state) => state.flows);
const setNoticeData = useAlertStore((state) => state.setNoticeData);
useEffect(() => {
setName(currentFlow!.name);
setDescription(currentFlow!.description);
}, [currentFlow!.name, currentFlow!.description, open]);
const [name, setName] = useState(currentFlow!.name);
const [description, setDescription] = useState(currentFlow!.description);
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
const [rows, setRows] = useState<any>([]);
const [activeTab, setActiveTab] = useState("Executions");
const noticed = useRef(false);
function handleClick(): void {
currentFlow!.name = name;
currentFlow!.description = description;
saveFlow(currentFlow!);
setOpen(false);
}
useEffect(() => {
if (activeTab === "Executions") {
getTransactionTable(currentFlowId, "union").then((data) => {
const { columns, rows } = data;
setColumns(columns.map((col) => ({ ...col, editable: true })));
setRows(rows);
});
} else if (activeTab === "Messages") {
getMessagesTable(currentFlowId, "union").then((data) => {
const { columns, rows } = data;
setColumns(columns.map((col) => ({ ...col, editable: true })));
setRows(rows);
});
}
if (open && activeTab === "Messages" && !noticed.current) {
const haStream = nodes
.map((nodes) => (nodes.data as NodeDataType).node!.template)
.some((template) => template["stream"] && template["stream"].value);
console.log(
haStream,
nodes.map((nodes) => (nodes.data as NodeDataType).node!.template),
);
if (haStream) {
setNoticeData({
title: "Streamed messages will not appear in this table.",
});
noticed.current = true;
}
}
if (!open) {
noticed.current = false;
}
}, [open, activeTab]);
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
const tempNameList: string[] = [];
flows.forEach((flow: FlowType) => {
if ((flow.is_component ?? false) === false) tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== currentFlow!.name));
}, [flows]);
return (
<BaseModal open={open} setOpen={setOpen} size="large">
<BaseModal.Header description="Inspect component executions and monitor sent messages in the playground.">
<div className="flex w-full justify-between">
<div className="flex h-fit w-32 items-center">
<span className="pr-2">Logs</span>
<IconComponent name="ScrollText" className="mr-2 h-4 w-4 " />
</div>
<div className="flex h-fit w-32 items-center"></div>
</div>
</BaseModal.Header>
<BaseModal.Content>
<Tabs
value={activeTab}
onValueChange={setActiveTab}
className={
"text-center; inset-0 m-0 mb-2 flex flex-col self-center overflow-hidden rounded-md border bg-muted pb-1"
}
>
<TabsList>
<TabsTrigger value={"Executions"}>Executions</TabsTrigger>
<TabsTrigger value={"Messages"}>Messages</TabsTrigger>
</TabsList>
</Tabs>
<TableComponent
readOnlyEdit
className="h-max-full h-full w-full"
pagination={rows.length === 0 ? false : true}
columnDefs={columns}
autoSizeStrategy={{ type: "fitGridWidth" }}
rowData={rows}
headerHeight={rows.length === 0 ? 0 : undefined}
></TableComponent>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -0,0 +1,148 @@
import { useEffect, useState } from "react";
import InputComponent from "../../../components/inputComponent";
import {
FormControl,
FormField,
FormItem,
FormMessage,
} from "../../../components/ui/form";
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import { Textarea } from "../../../components/ui/textarea";
import useFlowsManagerStore from "../../../stores/flowsManagerStore";
type FolderFormsProps = {
control: any;
setValue: any;
folderToEdit: any;
};
export const FolderForms = ({
control,
setValue,
folderToEdit,
}: FolderFormsProps): JSX.Element => {
const flows = useFlowsManagerStore((state) => state.flows);
const [selectedComponents, setSelectedComponents] = useState<string[]>([]);
const [selectedFlows, setSelectedFlows] = useState<string[]>([]);
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const componentsList = flows
.filter((flow) => flow.is_component && flow.folder_id !== null)
.map((flow) => ({ id: flow.id, name: flow.name }));
const flowsList = flows
.filter((flow) => !flow.is_component && flow.folder_id !== null)
.map((flow) => ({ id: flow.id, name: flow.name }));
const componentsOnFolder = allFlows
.filter((flow) => flow.is_component && flow.folder_id === folderToEdit?.id)
.map((flow) => flow.id);
const flowsOnFolder = allFlows
.filter((flow) => !flow.is_component && flow.folder_id === folderToEdit?.id)
.map((flow) => flow.id);
useEffect(() => {
setValue("components", selectedComponents);
setValue("flows", selectedFlows);
}, [selectedComponents, selectedFlows]);
useEffect(() => {
if (folderToEdit) {
setValue("name", folderToEdit.name);
setValue("description", folderToEdit.description);
setSelectedComponents(componentsOnFolder);
setSelectedFlows(flowsOnFolder);
return;
}
setValue("name", "");
setValue("description", "");
setSelectedComponents([]);
setSelectedFlows([]);
}, [folderToEdit]);
return (
<>
<div className="flex h-full w-full flex-col gap-4 align-middle">
<Label>Folder Name</Label>
<FormField
control={control}
name="name"
render={({ field }) => (
<FormItem>
<FormControl>
<Input
value={field.value}
onChange={field.onChange}
placeholder="Insert a name for the folder..."
></Input>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Label>Description (optional) </Label>
<FormField
control={control}
name="description"
render={({ field }) => (
<FormControl>
<Textarea
value={field.value}
onChange={field.onChange}
placeholder="Insert a description for the folder..."
></Textarea>
</FormControl>
)}
/>
{/*
<Label>Add Flows</Label>
<FormField
control={control}
name="flows"
render={() => (
<FormControl>
<InputComponent
isObjectOption
password={false}
objectOptions={flowsList}
placeholder="Choose a flow to add..."
id="input-flow"
setSelectedOptions={(value: any) => setSelectedFlows(value)}
selectedOptions={selectedFlows}
></InputComponent>
</FormControl>
)}
/>
<Label>Add Components</Label>
<FormField
control={control}
name="components"
render={() => (
<FormControl>
<InputComponent
isObjectOption
password={false}
objectOptions={componentsList}
placeholder="Choose a component to add..."
id="input-component"
setSelectedOptions={(value) => setSelectedComponents(value)}
selectedOptions={selectedComponents}
></InputComponent>
</FormControl>
)}
/> */}
</div>
</>
);
};
export default FolderForms;

View file

@ -0,0 +1,10 @@
import { z } from "zod";
export const FolderFormsSchema = z.object({
name: z.string().min(1, {
message: "Name must be at least 1 characters.",
}),
description: z.string().optional(),
components: z.array(z.string()),
flows: z.array(z.string()),
});

View file

@ -0,0 +1,60 @@
import { useNavigate } from "react-router-dom";
import { addFolder, updateFolder } from "../../../pages/MainPage/services";
import useAlertStore from "../../../stores/alertStore";
import { useFolderStore } from "../../../stores/foldersStore";
const useFolderSubmit = (setOpen, folderToEdit) => {
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const navigate = useNavigate();
const onSubmit = (data) => {
if (folderToEdit) {
updateFolder(data, folderToEdit?.id!).then(
() => {
setSuccessData({
title: "Folder updated successfully.",
});
getFoldersApi(true);
setOpen(false);
navigate(`all/folder/${folderToEdit.id}`, {
state: { folderId: folderToEdit.id },
});
},
//TODO: LOOK THIS ERRO MORE CAREFULLY
(reason) => {
if (reason) {
setErrorData({
title: `Error updating folder.`,
});
console.error(reason);
} else {
getFoldersApi(true);
setOpen(false);
}
},
);
} else {
addFolder(data).then(
(res) => {
setSuccessData({
title: "Folder created successfully.",
});
getFoldersApi(true);
setOpen(false);
navigate(`all/folder/${res.id}`, { state: { folderId: res.id } });
},
() => {
setErrorData({
title: `Error creating folder.`,
});
},
);
}
};
return { onSubmit, open, setOpen };
};
export default useFolderSubmit;

View file

@ -0,0 +1,79 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import ForwardedIconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Form } from "../../components/ui/form";
import { useFolderStore } from "../../stores/foldersStore";
import BaseModal from "../baseModal";
import FolderForms from "./component";
import { FolderFormsSchema } from "./entities";
import useFolderSubmit from "./hooks/submit-folder";
type FoldersModalProps = {
open: boolean;
setOpen: (open: boolean) => void;
};
export default function FoldersModal({
open,
setOpen,
}: FoldersModalProps): JSX.Element {
const form = useForm<z.infer<typeof FolderFormsSchema>>({
resolver: zodResolver(FolderFormsSchema),
defaultValues: {
name: "",
description: "",
components: [],
flows: [],
},
mode: "all",
});
const folderToEdit = useFolderStore((state) => state.folderToEdit);
const { onSubmit: onSubmitFolder } = useFolderSubmit(setOpen, folderToEdit);
const onSubmit = (data) => {
onSubmitFolder(data);
};
return (
<>
<BaseModal size="x-small" open={open} setOpen={setOpen}>
<BaseModal.Header
description={`${folderToEdit ? "Edit a folder" : "Add a new folder"}`}
>
<span className="pr-2" data-testid="modal-title">
{folderToEdit ? "Edit" : "New"} Folder
</span>
<ForwardedIconComponent
name={folderToEdit ? "Pencil" : "Plus"}
className="h-6 w-6 pl-1 text-primary "
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FolderForms
folderToEdit={folderToEdit}
control={form.control}
setValue={form.setValue}
/>
<Button
className="float-right mt-6"
type="submit"
disabled={!form.formState.isValid}
>
{folderToEdit ? "Edit" : "Save"} Folder
</Button>
</form>
</Form>
</div>
</BaseModal.Content>
</BaseModal>
</>
);
}

View file

@ -24,8 +24,9 @@ import { postValidatePrompt } from "../../controllers/API";
import useAlertStore from "../../stores/alertStore";
import { genericModalPropsType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames, varHighlightHTML } from "../../utils/utils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal";
import varHighlightHTML from "./utils/var-highlight-html";
export default function GenericModal({
field_name = "",
@ -82,7 +83,7 @@ export default function GenericModal({
}
const filteredWordsHighlight = matches.filter(
(word) => !invalid_chars.includes(word)
(word) => !invalid_chars.includes(word),
);
setWordsHighlight(filteredWordsHighlight);
@ -133,7 +134,7 @@ export default function GenericModal({
// to the first key of the custom_fields object
if (field_name === "") {
field_name = Array.isArray(
apiReturn.data?.frontend_node?.custom_fields?.[""]
apiReturn.data?.frontend_node?.custom_fields?.[""],
)
? apiReturn.data?.frontend_node?.custom_fields?.[""][0] ?? ""
: apiReturn.data?.frontend_node?.custom_fields?.[""] ?? "";
@ -209,7 +210,7 @@ export default function GenericModal({
<div
className={classNames(
!isEdit ? "rounded-lg border" : "",
"flex h-full w-full"
"flex h-full w-full",
)}
>
{type === TypeModal.PROMPT && isEdit && !readonly ? (

View file

@ -0,0 +1,6 @@
import { IVarHighlightType } from "../../../types/components";
export default function varHighlightHTML({ name }: IVarHighlightType): string {
const html = `<span class="font-semibold chat-message-highlight">{${name}}</span>`;
return html;
}

View file

@ -1,4 +1,4 @@
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import {
Card,
CardContent,
@ -6,15 +6,21 @@ import {
CardTitle,
} from "../../../../components/ui/card";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
export default function NewFlowCardComponent() {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const navigate = useNavigate();
const location = useLocation();
const folderId = location?.state?.folderId;
const setFolderUrl = useFolderStore((state) => state.setFolderUrl);
return (
<Card
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
setFolderUrl(folderId ?? "");
navigate(`/flow/${id}${folderId ? `/folder/${folderId}` : ""}`);
});
}}
className="h-64 w-80 cursor-pointer bg-background pt-4"

View file

@ -1,4 +1,4 @@
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
/// <reference types="vite-plugin-svgr/client" />
//@ts-ignore
import { ReactComponent as TransferFiles } from "../../../../assets/undraw_transfer_files_re_a2a9.svg";
@ -22,6 +22,7 @@ import {
CardTitle,
} from "../../../../components/ui/card";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { UndrawCardComponentProps } from "../../../../types/components";
import { updateIds } from "../../../../utils/reactflowUtils";
@ -30,6 +31,9 @@ export default function UndrawCardComponent({
}: UndrawCardComponentProps): JSX.Element {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const navigate = useNavigate();
const location = useLocation();
const folderId = location?.state?.folderId;
const setFolderUrl = useFolderStore((state) => state.setFolderUrl);
function selectImage() {
switch (flow.name) {
@ -111,7 +115,8 @@ export default function UndrawCardComponent({
onClick={() => {
updateIds(flow.data!);
addFlow(true, flow).then((id) => {
navigate("/flow/" + id);
setFolderUrl(folderId ?? "");
navigate(`/flow/${id}${folderId ? `/folder/${folderId}` : ""}`);
});
}}
className="h-64 w-80 cursor-pointer bg-background pt-4"

View file

@ -10,7 +10,7 @@ export default function NewFlowModal({
}: newFlowModalPropsType): JSX.Element {
const examples = useFlowsManagerStore((state) => state.examples);
examples.forEach((example) => {
examples?.forEach((example) => {
if (example.name === "Blog Writter") {
example.name = "Blog Writer";
}
@ -22,24 +22,17 @@ export default function NewFlowModal({
<span className="pr-2" data-testid="modal-title">
Get Started
</span>
{/* <IconComponent
name="Group"
className="h-6 w-6 stroke-2 text-primary "
aria-hidden="true"
/> */}
</BaseModal.Header>
<BaseModal.Content>
<div className=" mb-5 grid h-fit w-full grid-cols-3 gap-4 overflow-auto pb-6 custom-scroll">
<NewFlowCardComponent />
{/* {examples.map((example, idx) => {
return <UndrawCardComponent key={idx} flow={example} />;
})} */}
{examples.find((e) => e.name == "Basic Prompting (Hello, World)") && (
<UndrawCardComponent
key={1}
key={0}
flow={
examples.find(
(e) => e.name == "Basic Prompting (Hello, World)"
(e) => e.name == "Basic Prompting (Hello, World)",
)!
}
/>
@ -52,19 +45,19 @@ export default function NewFlowModal({
)}
{examples.find((e) => e.name == "Document QA") && (
<UndrawCardComponent
key={1}
key={2}
flow={examples.find((e) => e.name == "Document QA")!}
/>
)}
{examples.find((e) => e.name == "Blog Writer") && (
<UndrawCardComponent
key={1}
key={3}
flow={examples.find((e) => e.name == "Blog Writer")!}
/>
)}
{examples.find((e) => e.name == "Vector Store RAG") && (
<UndrawCardComponent
key={1}
key={4}
flow={examples.find((e) => e.name == "Vector Store RAG")!}
/>
)}

View file

@ -22,10 +22,10 @@ import {
removeFileNameFromComponents,
removeGlobalVariableFromComponents,
} from "../../utils/reactflowUtils";
import { getTagsIds } from "../../utils/storeUtils";
import BaseModal from "../baseModal";
import ConfirmationModal from "../confirmationModal";
import ExportModal from "../exportModal";
import getTagsIds from "./utils/get-tags-ids";
export default function ShareModal({
component,
@ -129,14 +129,14 @@ export default function ShareModal({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
}
},
);
else
updateFlowStore(
flow!,
getTagsIds(selectedTags, tags),
sharePublic,
unavaliableNames.find((e) => e.name === name)!.id
unavaliableNames.find((e) => e.name === name)!.id,
).then(successShare, (err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
@ -287,7 +287,7 @@ export default function ShareModal({
className={is_component ? "w-40" : "w-28"}
onClick={() => {
const isNameAvailable = !unavaliableNames.some(
(element) => element.name === name
(element) => element.name === name,
);
if (isNameAvailable) {

View file

@ -0,0 +1,8 @@
export default function getTagsIds(
tags: string[],
tagListId: { name: string; id: string }[],
) {
return tags
.map((tag) => tagListId.find((tagObj) => tagObj.name === tag))!
.map((tag) => tag!.id);
}

View file

@ -34,9 +34,10 @@ import {
updateIds,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import isWrappedWithClass from "./utils/is-wrapped-with-class";
import getRandomName from "./utils/get-random-name";
const nodeTypes = {
genericNode: GenericNode,
@ -483,6 +484,7 @@ export default function Page({
></Controls>
)}
<SelectionMenu
lastSelection={lastSelection}
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {

View file

@ -0,0 +1,35 @@
import { ADJECTIVES, NOUNS } from "../../../../../flow_constants";
import { getRandomElement } from "../../../../../utils/reactflowUtils";
import { toTitleCase } from "../../../../../utils/utils";
export default function getRandomName(
retry: number = 0,
noSpace: boolean = false,
maxRetries: number = 3,
): string {
const left: string[] = ADJECTIVES;
const right: string[] = NOUNS;
const lv = getRandomElement(left);
const rv = getRandomElement(right);
// Condition to avoid "boring wozniak"
if (lv === "boring" && rv === "wozniak") {
if (retry < maxRetries) {
return getRandomName(retry + 1, noSpace, maxRetries);
} else {
console.warn("Max retries reached, returning as is");
}
}
// Append a suffix if retrying and noSpace is true
if (retry > 0 && noSpace) {
const retrySuffix = Math.floor(Math.random() * 10);
return `${lv}_${rv}${retrySuffix}`;
}
// Construct the final name
let final_name = noSpace ? `${lv}_${rv}` : `${lv} ${rv}`;
// Return title case final name
return toTitleCase(final_name);
}

View file

@ -0,0 +1,4 @@
const isWrappedWithClass = (event: any, className: string | undefined) =>
event.target.closest(`.${className}`);
export default isWrappedWithClass;

View file

@ -1,11 +1,31 @@
import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import { GradientGroup } from "../../../../icons/GradientSparkles";
export default function SelectionMenu({ onClick, nodes, isVisible }) {
import useFlowStore from "../../../../stores/flowStore";
import { validateSelection } from "../../../../utils/reactflowUtils";
export default function SelectionMenu({
onClick,
nodes,
isVisible,
lastSelection,
}) {
const edges = useFlowStore((state) => state.edges);
const [disable, setDisable] = useState<boolean>(
lastSelection && edges.length > 0
? validateSelection(lastSelection!, edges).length > 0
: false,
);
const [isOpen, setIsOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const [lastNodes, setLastNodes] = useState(nodes);
useEffect(() => {
if (isOpen) {
return setDisable(validateSelection(lastSelection!, edges).length > 0);
}
setDisable(false);
}, [isOpen, setIsOpen]);
// nodes get saved to not be gone after the toolbar closes
useEffect(() => {
setLastNodes(nodes);
@ -42,13 +62,19 @@ export default function SelectionMenu({ onClick, nodes, isVisible }) {
}
>
<button
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
className={`${
disable
? "flex h-full w-full cursor-not-allowed items-center justify-between text-sm text-muted-foreground"
: "flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
}`}
onClick={onClick}
disabled={disable}
>
<GradientGroup
strokeWidth={1.5}
size={22}
className="text-primary"
disabled={disable}
/>
Group
</button>

View file

@ -19,15 +19,12 @@ import {
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import {
classNames,
removeCountFromString,
sensitiveSort,
} from "../../../../utils/utils";
import { classNames, removeCountFromString } from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent";
import ParentDisclosureComponent from "../ParentDisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent";
import { sortKeys } from "./utils";
import sensitiveSort from "./utils/sensitive-sort";
export default function ExtraSidebar(): JSX.Element {
const data = useTypesStore((state) => state.data);
@ -45,7 +42,7 @@ export default function ExtraSidebar(): JSX.Element {
const [search, setSearch] = useState("");
function onDragStart(
event: React.DragEvent<any>,
data: { type: string; node?: APIClassType }
data: { type: string; node?: APIClassType },
): void {
//start drag event
var crt = event.currentTarget.cloneNode(true);
@ -71,7 +68,7 @@ export default function ExtraSidebar(): JSX.Element {
let keys = Object.keys(data[d]).filter(
(nd) =>
nd.toLowerCase().includes(e.toLowerCase()) ||
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase())
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase()),
);
keys.forEach((element) => {
ret[d][element] = data[d][element];
@ -138,7 +135,7 @@ export default function ExtraSidebar(): JSX.Element {
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
filtered.includes(nd),
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
@ -175,7 +172,7 @@ export default function ExtraSidebar(): JSX.Element {
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
filtered.includes(nd),
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
@ -204,7 +201,7 @@ export default function ExtraSidebar(): JSX.Element {
"extra-side-bar-buttons gap-[4px] text-sm font-semibold",
!hasApiKey || !validApiKey || !hasStore
? "button-disable cursor-default text-muted-foreground"
: ""
: "",
)}
>
<IconComponent
@ -213,14 +210,14 @@ export default function ExtraSidebar(): JSX.Element {
"-m-0.5 -ml-1 h-6 w-6",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: ""
: "",
)}
/>
Share
</button>
</ShareModal>
),
[hasApiKey, validApiKey, currentFlow, hasStore]
[hasApiKey, validApiKey, currentFlow, hasStore],
);
const ExportMemo = useMemo(
@ -231,7 +228,7 @@ export default function ExtraSidebar(): JSX.Element {
</button>
</ExportModal>
),
[]
[],
);
const getIcon = useMemo(() => {
@ -315,8 +312,8 @@ export default function ExtraSidebar(): JSX.Element {
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
dataFilter[SBSectionName][b].display_name,
),
)
.map((SBItemName: string, index) => (
<ShadTooltip
@ -360,7 +357,7 @@ export default function ExtraSidebar(): JSX.Element {
</>
) : (
<div key={index}></div>
)
),
)}{" "}
<ParentDisclosureComponent
openDisc={false}
@ -397,8 +394,8 @@ export default function ExtraSidebar(): JSX.Element {
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
dataFilter[SBSectionName][b].display_name,
),
)
.map((SBItemName: string, index) => (
<ShadTooltip
@ -472,7 +469,7 @@ export default function ExtraSidebar(): JSX.Element {
</>
) : (
<div key={index}></div>
)
),
)}
</ParentDisclosureComponent>
</div>

View file

@ -0,0 +1,24 @@
export default function sensitiveSort(a: string, b: string): number {
// Extract the name and number from each string using regular expressions
const regex = /(.+) \((\w+)\)/;
const matchA = a.match(regex);
const matchB = b.match(regex);
if (matchA && matchB) {
// Compare the names alphabetically
const nameA = matchA[1];
const nameB = matchB[1];
if (nameA !== nameB) {
return nameA.localeCompare(nameB);
}
// If the names are the same, compare the numbers numerically
const numberA = parseInt(matchA[2]);
const numberB = parseInt(matchB[2]);
return numberA - numberB;
} else {
// Handle cases where one or both strings do not match the expected pattern
// Simple strings are treated as pure alphabetical comparisons
return a.localeCompare(b);
}
}

View file

@ -1,27 +1,37 @@
import { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useNavigate, useParams } from "react-router-dom";
import FlowToolbar from "../../components/chatComponent";
import Header from "../../components/headerComponent";
import { useDarkStore } from "../../stores/darkStore";
import useFlowStore from "../../stores/flowStore";
import useFlowsManagerStore from "../../stores/flowsManagerStore";
import Page from "./components/PageComponent";
import ExtraSidebar from "./components/extraSidebarComponent";
import useFlowStore from "../../stores/flowStore";
export default function FlowPage({ view }: { view?: boolean }): JSX.Element {
const setCurrentFlowId = useFlowsManagerStore(
(state) => state.setCurrentFlowId
(state) => state.setCurrentFlowId,
);
const version = useDarkStore((state) => state.version);
const setOnFlowPage = useFlowStore((state) => state.setOnFlowPage);
const currentFlow = useFlowsManagerStore((state) => state.currentFlow);
const { id } = useParams();
const navigate = useNavigate();
const flows = useFlowsManagerStore((state) => state.flows);
// Set flow tab id
useEffect(() => {
const isAnExistingFlow = flows.some((flow) => flow.id === id);
if (!isAnExistingFlow) {
navigate("/all");
return;
}
setCurrentFlowId(id!);
setOnFlowPage(true);
return () => {
setOnFlowPage(false);
};

View file

@ -1,207 +0,0 @@
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import CollectionCardComponent from "../../../../components/cardComponent";
import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent from "../../../../components/genericIconComponent";
import PaginatorComponent from "../../../../components/paginatorComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button";
import {
CONSOLE_ERROR_MSG,
UPLOAD_ALERT_LIST,
WRONG_FILE_ERROR_ALERT,
} from "../../../../constants/alerts_constants";
import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { FlowType } from "../../../../types/flow";
export default function ComponentsComponent({
is_component = true,
}: {
is_component?: boolean;
}) {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const removeFlow = useFlowsManagerStore((state) => state.removeFlow);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const flows = useFlowsManagerStore((state) => state.flows);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [pageSize, setPageSize] = useState(20);
const [pageIndex, setPageIndex] = useState(1);
const navigate = useNavigate();
const all: FlowType[] = flows
.filter((f) => (f.is_component ?? false) === is_component)
.sort((a, b) => {
if (a?.updated_at && b?.updated_at) {
return (
new Date(b?.updated_at!).getTime() -
new Date(a?.updated_at!).getTime()
);
} else if (a?.updated_at && !b?.updated_at) {
return 1;
} else if (!a?.updated_at && b?.updated_at) {
return -1;
} else {
return (
new Date(b?.date_created!).getTime() -
new Date(a?.date_created!).getTime()
);
}
});
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
const data: FlowType[] = all.slice(start, end);
const name = is_component ? "Component" : "Flow";
const onFileDrop = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
if (e.dataTransfer.files.item(0).type === "application/json") {
uploadFlow({
newProject: true,
file: e.dataTransfer.files.item(0)!,
isComponent: is_component,
})
.then(() => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
})
.catch((error) => {
setErrorData({
title: CONSOLE_ERROR_MSG,
list: [error],
});
});
} else {
setErrorData({
title: WRONG_FILE_ERROR_ALERT,
list: [UPLOAD_ALERT_LIST],
});
}
}
};
function resetFilter() {
setPageIndex(1);
setPageSize(20);
}
return (
<CardsWrapComponent
onFileDrop={onFileDrop}
dragMessage={`Drag your ${name} here`}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4">
{!isLoading && data.length === 0 ? (
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex-max-width h-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4">
Flows and components can be created using Langflow.
</div>
<div className="align-center flex w-full justify-center gap-1">
<span>New?</span>
<span className="transition-colors hover:text-muted-foreground">
<button
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
className="underline"
>
Start Here
</button>
.
</span>
<span className="animate-pulse">🚀</span>
</div>
</div>
</div>
</div>
) : (
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
{isLoading === false && data?.length > 0 ? (
data?.map((item, idx) => (
<CollectionCardComponent
onDelete={() => {
removeFlow(item.id);
setSuccessData({
title: `${
item.is_component ? "Component" : "Flow"
} deleted successfully!`,
});
resetFilter();
}}
key={idx}
data={{ is_component: item.is_component ?? false, ...item }}
disabled={isLoading}
data-testid={"edit-flow-button-" + item.id + "-" + idx}
button={
!is_component ? (
<Link to={"/flow/" + item.id}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap "
data-testid={
"edit-flow-button-" + item.id + "-" + idx
}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
) : (
<></>
)
}
onClick={
!is_component
? () => {
navigate("/flow/" + item.id);
}
: undefined
}
playground={!is_component}
/>
))
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
)}
</div>
{!isLoading && data.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
rowsCount={[10, 20, 50, 100]}
totalRowsCount={
flows.filter((f) => (f.is_component ?? false) === is_component)
.length
}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</CardsWrapComponent>
);
}

View file

@ -0,0 +1,303 @@
import { cloneDeep } from "lodash";
import { useEffect, useMemo, useState } from "react";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { Link, useLocation, useNavigate } from "react-router-dom";
import CollectionCardComponent from "../../../../components/cardComponent";
import CardsWrapComponent from "../../../../components/cardsWrapComponent";
import IconComponent from "../../../../components/genericIconComponent";
import PaginatorComponent from "../../../../components/paginatorComponent";
import { SkeletonCardComponent } from "../../../../components/skeletonCardComponent";
import { Button } from "../../../../components/ui/button";
import DeleteConfirmationModal from "../../../../modals/deleteConfirmationModal";
import useAlertStore from "../../../../stores/alertStore";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
import { useFolderStore } from "../../../../stores/foldersStore";
import { FlowType } from "../../../../types/flow";
import useFileDrop from "../../hooks/use-on-file-drop";
import { getNameByType } from "../../utils/get-name-by-type";
import { sortFlows } from "../../utils/sort-flows";
import EmptyComponent from "../emptyComponent";
import HeaderComponent from "../headerComponent";
export default function ComponentsComponent({
type = "all",
}: {
type?: string;
}) {
const uploadFlow = useFlowsManagerStore((state) => state.uploadFlow);
const removeFlow = useFlowsManagerStore((state) => state.removeFlow);
const isLoading = useFlowsManagerStore((state) => state.isLoading);
const setAllFlows = useFlowsManagerStore((state) => state.setAllFlows);
const allFlows = useFlowsManagerStore((state) => state.allFlows);
const flowsFromFolder = useFolderStore(
(state) => state.selectedFolder?.flows,
);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const setErrorData = useAlertStore((state) => state.setErrorData);
const [openDelete, setOpenDelete] = useState(false);
const searchFlowsComponents = useFlowsManagerStore(
(state) => state.searchFlowsComponents,
);
const setSelectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.setSelectedFlowsComponentsCards,
);
const selectedFlowsComponentsCards = useFlowsManagerStore(
(state) => state.selectedFlowsComponentsCards,
);
const [handleFileDrop] = useFileDrop(uploadFlow, type)!;
const [pageSize, setPageSize] = useState(20);
const [pageIndex, setPageIndex] = useState(1);
const navigate = useNavigate();
const location = useLocation();
const all: FlowType[] = sortFlows(allFlows, type);
const start = (pageIndex - 1) * pageSize;
const end = start + pageSize;
const data: FlowType[] = all?.slice(start, end);
const name = getNameByType(type);
const folderId = location?.state?.folderId;
const getFolderById = useFolderStore((state) => state.getFolderById);
const myCollectionId = useFolderStore((state) => state.myCollectionId);
const getFoldersApi = useFolderStore((state) => state.getFoldersApi);
const setFolderUrl = useFolderStore((state) => state.setFolderUrl);
useEffect(() => {
setFolderUrl(folderId ?? "");
setSelectedFlowsComponentsCards([]);
handleSelectAll(false);
getFolderById(folderId ? folderId : myCollectionId);
}, [location]);
useEffect(() => {
const newFlows = cloneDeep(flowsFromFolder!);
const filteredFlows = newFlows?.filter(
(f) =>
f.name.toLowerCase().includes(searchFlowsComponents.toLowerCase()) ||
f.description
.toLowerCase()
.includes(searchFlowsComponents.toLowerCase()),
);
if (searchFlowsComponents === "") {
setAllFlows(flowsFromFolder!);
}
setAllFlows(filteredFlows);
}, [searchFlowsComponents]);
const resetFilter = () => {
setPageIndex(1);
setPageSize(20);
};
const { getValues, control, setValue } = useForm();
const entireFormValues = useWatch({ control });
const methods = useForm();
const handleSelectAll = (select) => {
const flowsFromFolderIds = flowsFromFolder?.map((f) => f.id);
if (select) {
Object.keys(getValues()).forEach((key) => {
if (!flowsFromFolderIds?.includes(key)) return;
setValue(key, true);
});
return;
}
Object.keys(getValues()).forEach((key) => {
setValue(key, false);
});
};
const handleSelectOptionsChange = () => {
const hasSelected = selectedFlowsComponentsCards?.length > 0;
if (!hasSelected) {
setErrorData({
title: "No items selected",
list: ["Please select items to delete"],
});
return;
}
setOpenDelete(true);
};
const handleDeleteMultiple = () => {
removeFlow(selectedFlowsComponentsCards)
.then(() => {
resetFilter();
getFoldersApi(true);
if (!folderId || folderId === myCollectionId) {
getFolderById(folderId ? folderId : myCollectionId);
}
setSuccessData({
title: "Selected items deleted successfully!",
});
})
.catch(() => {
setErrorData({
title: "Error deleting items",
list: ["Please try again"],
});
});
};
useEffect(() => {
if (!entireFormValues || Object.keys(entireFormValues).length === 0) return;
const selectedFlows: string[] = Object.keys(entireFormValues).filter(
(key) => {
if (entireFormValues[key] === true) {
return true;
}
return false;
},
);
setSelectedFlowsComponentsCards(selectedFlows);
}, [entireFormValues]);
const getDescriptionModal = useMemo(() => {
const getTypeLabel = (type) => {
const labels = {
all: "item",
component: "component",
flow: "flow",
};
return labels[type] || "";
};
const getPluralizedLabel = (type) => {
const labels = {
all: "items",
component: "components",
flow: "flows",
};
return labels[type] || "";
};
if (selectedFlowsComponentsCards?.length === 1) {
return getTypeLabel(type);
}
return getPluralizedLabel(type);
}, [selectedFlowsComponentsCards, type]);
const getTotalRowsCount = () => {
if (type === "all") return allFlows?.length;
return allFlows?.filter(
(f) => (f.is_component ?? false) === (type === "component"),
)?.length;
};
return (
<>
{allFlows?.length > 0 && (
<HeaderComponent
handleDelete={handleSelectOptionsChange}
handleSelectAll={handleSelectAll}
disableDelete={!(selectedFlowsComponentsCards?.length > 0)}
/>
)}
<CardsWrapComponent
onFileDrop={handleFileDrop}
dragMessage={`Drag your ${name} here`}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4">
{!isLoading && data?.length === 0 ? (
<EmptyComponent />
) : (
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-2">
{isLoading === false && data?.length > 0 ? (
<>
{data?.map((item) => (
<FormProvider {...methods} key={item.id}>
<form>
<CollectionCardComponent
is_component={type === "component"}
data={{
is_component: item.is_component ?? false,
...item,
}}
disabled={isLoading}
data-testid={"edit-flow-button-" + item.id}
button={
!item.is_component ? (
<Link to={"/flow/" + item.id}>
<Button
tabIndex={-1}
variant="outline"
size="sm"
className="whitespace-nowrap"
data-testid={"edit-flow-button-" + item.id}
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button select-none"
/>
Edit Flow
</Button>
</Link>
) : (
<></>
)
}
onClick={
!item.is_component
? () => {
navigate("/flow/" + item.id);
}
: undefined
}
playground={!item.is_component}
control={control}
/>
</form>
</FormProvider>
))}
</>
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
)}
</div>
{!isLoading && data?.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
rowsCount={[10, 20, 50, 100]}
totalRowsCount={getTotalRowsCount()}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</CardsWrapComponent>
{openDelete && (
<DeleteConfirmationModal
open={openDelete}
setOpen={setOpenDelete}
onConfirm={handleDeleteMultiple}
description={getDescriptionModal}
>
<></>
</DeleteConfirmationModal>
)}
</>
);
}

View file

@ -0,0 +1,41 @@
import { useNavigate } from "react-router-dom";
import useFlowsManagerStore from "../../../../stores/flowsManagerStore";
type EmptyComponentProps = {};
const EmptyComponent = ({}: EmptyComponentProps) => {
const addFlow = useFlowsManagerStore((state) => state.addFlow);
const navigate = useNavigate();
return (
<>
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex-max-width h-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4 text-muted-foreground">
Flows and components can be created using Langflow.
</div>
<div className="align-center flex w-full justify-center gap-1 ">
<span className="text-muted-foreground">New?</span>
<span className="transition-colors hover:text-muted-foreground">
<button
onClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
className="underline"
>
Start Here
</button>
.
</span>
<span className="animate-pulse">🚀</span>
</div>
</div>
</div>
</div>
</>
);
};
export default EmptyComponent;

View file

@ -0,0 +1,85 @@
import { useState } from "react";
import IconComponent from "../../../../components/genericIconComponent";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../../../components/ui/select";
import { Checkbox } from "../../../../components/ui/checkbox";
import { Button } from "../../../../components/ui/button";
import { cn } from "../../../../utils/utils";
import ShadTooltip from "../../../../components/shadTooltipComponent";
type HeaderComponentProps = {
handleSelectAll: (select) => void;
handleDelete: () => void;
disableDelete: boolean;
};
const HeaderComponent = ({
handleSelectAll,
handleDelete,
disableDelete,
}: HeaderComponentProps) => {
const [shouldSelectAll, setShouldSelectAll] = useState(true);
const handleClick = () => {
handleSelectAll(shouldSelectAll);
setShouldSelectAll((prevState) => !prevState);
};
return (
<>
<div className="grid grid-cols-3 pb-5">
<div className="col-auto grid-cols-1 self-center justify-self-start">
<a onClick={handleClick} className="text-sm">
<div className="header-menu-bar-display ">
<div
className="header-menu-flow-name"
data-testid="select_all_collection"
>
<div className="flex items-center space-x-2">
<Checkbox checked={!shouldSelectAll} id="terms" />
<label
onClick={handleClick}
htmlFor="terms"
className="label cursor-pointer text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{shouldSelectAll ? "Select All" : "Unselect All"}
</label>
</div>
</div>
</div>
</a>
</div>
<div className="col-span-2 grid-cols-1 justify-self-end">
<div>
<ShadTooltip
content={
disableDelete ? (
<span>Select items to delete</span>
) : (
<span>Delete selected items</span>
)
}
>
<button onClick={handleDelete} disabled={disableDelete}>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-primary transition-all",
disableDelete ? "" : "hover:text-destructive",
)}
/>
</button>
</ShadTooltip>
</div>
</div>
</div>
</>
);
};
export default HeaderComponent;

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