Merge remote-tracking branch 'origin/dev' into multipart_endpoint
This commit is contained in:
commit
0dd16ee794
202 changed files with 21559 additions and 12347 deletions
17449
src/frontend/package-lock.json
generated
17449
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,83 +1,106 @@
|
|||
{
|
||||
"name": "langflow",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@headlessui/react": "^1.7.10",
|
||||
"@heroicons/react": "^2.0.15",
|
||||
"@mui/material": "^5.11.9",
|
||||
"@tabler/icons-react": "^2.18.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"ace-builds": "^1.16.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.3.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"lodash": "^4.17.21",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.2",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-laag": "^2.0.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tabs": "^6.0.0",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"reactflow": "^11.5.5",
|
||||
"rehype-mathjax": "^4.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:docker": "vite --host 0.0.0.0",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"format": "npx prettier --write \"src/**/*.{js,jsx,ts,tsx,json,md}\""
|
||||
},
|
||||
"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": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^16.18.12",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.5"
|
||||
}
|
||||
"name": "langflow",
|
||||
"version": "0.1.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@headlessui/react": "^1.7.10",
|
||||
"@heroicons/react": "^2.0.15",
|
||||
"@mui/material": "^5.11.9",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-menubar": "^1.0.3",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-switch": "^1.0.3",
|
||||
"@radix-ui/react-tooltip": "^1.0.6",
|
||||
"@tabler/icons-react": "^2.18.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"ace-builds": "^1.16.0",
|
||||
"add": "^2.0.6",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"axios": "^1.3.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"class-variance-authority": "^0.6.0",
|
||||
"clsx": "^1.2.1",
|
||||
"esbuild": "^0.17.18",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.233.0",
|
||||
"react": "^18.2.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-cookie": "^4.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.2",
|
||||
"react-icons": "^4.8.0",
|
||||
"react-laag": "^2.0.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-tabs": "^6.0.0",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"reactflow": "^11.5.5",
|
||||
"rehype-mathjax": "^4.0.2",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"shadcn-ui": "^0.1.3",
|
||||
"short-unique-id": "^4.4.4",
|
||||
"switch": "^0.0.0",
|
||||
"table": "^6.8.1",
|
||||
"tailwind-merge": "^1.13.0",
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"uuid": "^9.0.0",
|
||||
"vite-plugin-svgr": "^3.2.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"dev:docker": "vite --host 0.0.0.0",
|
||||
"start": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"format": "npx prettier --write \"**/*.{js,jsx,ts,tsx,json,md}\""
|
||||
},
|
||||
"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": {
|
||||
"@swc/cli": "^0.1.62",
|
||||
"@swc/core": "^1.3.62",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^16.18.12",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@vitejs/plugin-react-swc": "^3.0.0",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.3.9"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,4 +3,4 @@ module.exports = {
|
|||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import _ from "lodash";
|
|||
import ErrorAlert from "./alerts/error";
|
||||
import NoticeAlert from "./alerts/notice";
|
||||
import SuccessAlert from "./alerts/success";
|
||||
import ExtraSidebar from "./components/ExtraSidebarComponent";
|
||||
import { alertContext } from "./contexts/alertContext";
|
||||
import { locationContext } from "./contexts/locationContext";
|
||||
import TabsManagerComponent from "./pages/FlowPage/components/tabsManagerComponent";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import CrashErrorComponent from "./components/CrashErrorComponent";
|
||||
import { TabsContext } from "./contexts/tabsContext";
|
||||
import { getVersion } from "./controllers/API";
|
||||
import Router from "./routes";
|
||||
import Header from "./components/headerComponent";
|
||||
|
||||
export default function App() {
|
||||
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
||||
|
|
@ -46,15 +47,6 @@ export default function App() {
|
|||
}>
|
||||
>([]);
|
||||
|
||||
// Initialize state variable for the version
|
||||
const [version, setVersion] = useState("");
|
||||
useEffect(() => {
|
||||
fetch("/version")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setVersion(data.version);
|
||||
});
|
||||
}, []);
|
||||
// Use effect hook to update alertsList when a new alert is added
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
|
|
@ -112,7 +104,6 @@ export default function App() {
|
|||
return (
|
||||
//need parent component with width and height
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex grow-0 shrink basis-auto"></div>
|
||||
<ErrorBoundary
|
||||
onReset={() => {
|
||||
window.localStorage.removeItem("tabsData");
|
||||
|
|
@ -122,19 +113,14 @@ export default function App() {
|
|||
}}
|
||||
FallbackComponent={CrashErrorComponent}
|
||||
>
|
||||
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
|
||||
<ExtraSidebar />
|
||||
{/* Main area */}
|
||||
<main className="min-w-0 flex-1 border-t border-gray-200 dark:border-gray-700 flex">
|
||||
{/* Primary column */}
|
||||
<div className="w-full h-full">
|
||||
<TabsManagerComponent></TabsManagerComponent>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<Header />
|
||||
<Router />
|
||||
</ErrorBoundary>
|
||||
<div></div>
|
||||
<div className="flex z-40 flex-col-reverse fixed bottom-5 left-5">
|
||||
<div
|
||||
className="flex flex-col-reverse fixed bottom-5 left-5"
|
||||
style={{ zIndex: 999 }}
|
||||
>
|
||||
{alertsList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" ? (
|
||||
|
|
@ -164,14 +150,6 @@ export default function App() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a
|
||||
target={"_blank"}
|
||||
href="https://logspace.ai/"
|
||||
className="absolute left-7 bottom-2 flex h-6 cursor-pointer flex-col items-center justify-start overflow-hidden rounded-lg bg-gray-800 px-2 text-center font-sans text-xs tracking-wide text-gray-300 transition-all duration-500 ease-in-out hover:h-12 dark:bg-gray-100 dark:text-gray-800"
|
||||
>
|
||||
{version && <div className="mt-1">⛓️ LangFlow v{version}</div>}
|
||||
<div className="mt-2">Created by Logspace</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||
import Tooltip from "../../../../components/TooltipComponent";
|
||||
import { classNames, isValidConnection } from "../../../../utils";
|
||||
import {
|
||||
classNames,
|
||||
groupByFamily,
|
||||
isValidConnection,
|
||||
} from "../../../../utils";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import InputComponent from "../../../../components/inputComponent";
|
||||
import ToggleComponent from "../../../../components/toggleComponent";
|
||||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
|
|
@ -15,6 +17,12 @@ import InputFileComponent from "../../../../components/inputFileComponent";
|
|||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import IntComponent from "../../../../components/intComponent";
|
||||
import PromptAreaComponent from "../../../../components/promptComponent";
|
||||
import { nodeNames, nodeIcons } from "../../../../utils";
|
||||
import React from "react";
|
||||
import { nodeColors } from "../../../../utils";
|
||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||
import ToggleShadComponent from "../../../../components/toggleShadComponent";
|
||||
|
||||
export default function ParameterComponent({
|
||||
left,
|
||||
|
|
@ -28,14 +36,18 @@ export default function ParameterComponent({
|
|||
required = false,
|
||||
}: ParameterComponentType) {
|
||||
const ref = useRef(null);
|
||||
const refHtml = useRef(null);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const [position, setPosition] = useState(0);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const { setTabsState, tabId } = useContext(TabsContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
||||
updateNodeInternals(data.id);
|
||||
}
|
||||
}, [data.id, ref, updateNodeInternals]);
|
||||
}, [data.id, ref, ref.current, ref.current?.offsetTop, updateNodeInternals]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(data.id);
|
||||
|
|
@ -44,15 +56,72 @@ export default function ParameterComponent({
|
|||
const [enabled, setEnabled] = useState(
|
||||
data.node.template[name]?.value ?? false
|
||||
);
|
||||
|
||||
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
let disabled =
|
||||
reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
||||
const { save } = useContext(TabsContext);
|
||||
const [myData, setMyData] = useState(useContext(typesContext).data);
|
||||
|
||||
const handleOnNewValue = (newValue: any) => {
|
||||
data.node.template[name].value = newValue;
|
||||
// Set state to pending
|
||||
setTabsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const groupedObj = groupByFamily(myData, tooltipTitle);
|
||||
|
||||
refHtml.current = groupedObj.map((item, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={classNames(
|
||||
i > 0 ? "items-center flex mt-3" : "items-center flex"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className="h-5 w-5"
|
||||
style={{
|
||||
color: nodeColors[item.family],
|
||||
}}
|
||||
>
|
||||
{React.createElement(nodeIcons[item.family])}
|
||||
</div>
|
||||
<span className="ps-2 text-gray-950">
|
||||
{nodeNames[item.family] ?? ""}{" "}
|
||||
<span className={classNames(left ? "hidden" : "")}>
|
||||
{" "}
|
||||
-
|
||||
{item.type.split(", ").length > 2
|
||||
? item.type.split(", ").map((el, i) => (
|
||||
<>
|
||||
<span key={i}>
|
||||
{i == item.type.split(", ").length - 1
|
||||
? el
|
||||
: (el += `, `)}
|
||||
</span>
|
||||
{i % 2 == 0 && i > 0 && <br></br>}
|
||||
</>
|
||||
))
|
||||
: item.type}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
));
|
||||
}, [tooltipTitle]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="w-full flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800 dark:text-white mt-1 px-5 py-2"
|
||||
className="w-full flex flex-wrap justify-between items-center bg-muted dark:bg-gray-800 dark:text-white mt-1 px-5 py-2"
|
||||
>
|
||||
<>
|
||||
<div className={"text-sm truncate w-full " + (left ? "" : "text-end")}>
|
||||
|
|
@ -69,7 +138,12 @@ export default function ParameterComponent({
|
|||
type === "int") ? (
|
||||
<></>
|
||||
) : (
|
||||
<Tooltip title={tooltipTitle + (required ? " (required)" : "")}>
|
||||
<ShadTooltip
|
||||
delayDuration={0}
|
||||
content={refHtml.current}
|
||||
side={left ? "left" : "right"}
|
||||
open={refHtml?.current?.length > 0}
|
||||
>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
|
|
@ -86,7 +160,7 @@ export default function ParameterComponent({
|
|||
top: position,
|
||||
}}
|
||||
></Handle>
|
||||
</Tooltip>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
|
||||
{left === true &&
|
||||
|
|
@ -102,19 +176,13 @@ export default function ParameterComponent({
|
|||
? [""]
|
||||
: data.node.template[name].value
|
||||
}
|
||||
onChange={(t: string[]) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : data.node.template[name].multiline ? (
|
||||
<TextAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
|
|
@ -122,59 +190,52 @@ export default function ParameterComponent({
|
|||
disableCopyPaste={true}
|
||||
password={data.node.template[name].password ?? false}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : left === true && type === "bool" ? (
|
||||
<div className="mt-2">
|
||||
<ToggleComponent
|
||||
<ToggleShadComponent
|
||||
disabled={disabled}
|
||||
enabled={enabled}
|
||||
setEnabled={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
handleOnNewValue(t);
|
||||
setEnabled(t);
|
||||
save();
|
||||
}}
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "float" ? (
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
disableCopyPaste={true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2 w-full">
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
disableCopyPaste={true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true &&
|
||||
type === "str" &&
|
||||
data.node.template[name].options ? (
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={(newValue) => (data.node.template[name].value = newValue)}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
></Dropdown>
|
||||
<div className="w-full">
|
||||
<Dropdown
|
||||
options={data.node.template[name].options}
|
||||
onSelect={handleOnNewValue}
|
||||
value={data.node.template[name].value ?? "Choose an option"}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : left === true && type === "code" ? (
|
||||
<CodeAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : left === true && type === "file" ? (
|
||||
<InputFileComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
fileTypes={data.node.template[name].fileTypes}
|
||||
suffixes={data.node.template[name].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
|
|
@ -183,23 +244,19 @@ export default function ParameterComponent({
|
|||
}}
|
||||
></InputFileComponent>
|
||||
) : left === true && type === "int" ? (
|
||||
<IntComponent
|
||||
disabled={disabled}
|
||||
disableCopyPaste={true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2 w-full">
|
||||
<IntComponent
|
||||
disabled={disabled}
|
||||
disableCopyPaste={true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "prompt" ? (
|
||||
<PromptAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
onChange={handleOnNewValue}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
|
|
|
|||
|
|
@ -1,35 +1,18 @@
|
|||
import {
|
||||
BugAntIcon,
|
||||
Cog6ToothIcon,
|
||||
InformationCircleIcon,
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
EllipsisHorizontalCircleIcon,
|
||||
ExclamationCircleIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
|
||||
import {
|
||||
classNames,
|
||||
nodeColors,
|
||||
nodeIcons,
|
||||
toNormalCase,
|
||||
toTitleCase,
|
||||
} from "../../utils";
|
||||
import { classNames, nodeColors, nodeIcons, toTitleCase } from "../../utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { useContext, useState, useEffect, useRef, Fragment } from "react";
|
||||
import { useContext, useState, useEffect, useRef } from "react";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import NodeModal from "../../modals/NodeModal";
|
||||
import { useCallback } from "react";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { debounce } from "../../utils";
|
||||
import TooltipReact from "../../components/ReactTooltipComponent";
|
||||
import Tooltip from "../../components/TooltipComponent";
|
||||
import { NodeToolbar } from "reactflow";
|
||||
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
|
||||
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
import { useSSE } from "../../contexts/SSEContext";
|
||||
|
||||
export default function GenericNode({
|
||||
data,
|
||||
selected,
|
||||
|
|
@ -40,50 +23,30 @@ export default function GenericNode({
|
|||
const { setErrorData } = useContext(alertContext);
|
||||
const showError = useRef(true);
|
||||
const { types, deleteNode } = useContext(typesContext);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
|
||||
const { closePopUp, openPopUp } = useContext(PopUpContext);
|
||||
|
||||
const Icon = nodeIcons[data.type] || nodeIcons[types[data.type]];
|
||||
const [validationStatus, setValidationStatus] = useState(null);
|
||||
// State for outline color
|
||||
const [isValid, setIsValid] = useState(false);
|
||||
const { save } = useContext(TabsContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const [params, setParams] = useState([]);
|
||||
const { sseData, isBuilding } = useSSE();
|
||||
|
||||
// useEffect(() => {
|
||||
// if (reactFlowInstance) {
|
||||
// setParams(Object.values(reactFlowInstance.toObject()));
|
||||
// }
|
||||
// }, [save]);
|
||||
|
||||
// New useEffect to watch for changes in sseData and update validation status
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance) {
|
||||
setParams(Object.values(reactFlowInstance.toObject()));
|
||||
const relevantData = sseData[data.id];
|
||||
if (relevantData) {
|
||||
// Extract validation information from relevantData and update the validationStatus state
|
||||
setValidationStatus(relevantData);
|
||||
} else {
|
||||
setValidationStatus(null);
|
||||
}
|
||||
}, [save]);
|
||||
|
||||
const validateNode = useCallback(
|
||||
debounce(async () => {
|
||||
try {
|
||||
const response = await fetch(`/validate/node/${data.id}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(reactFlowInstance.toObject()),
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
let jsonResponse = await response.json();
|
||||
let jsonResponseParsed = await JSON.parse(jsonResponse);
|
||||
setValidationStatus(jsonResponseParsed);
|
||||
}
|
||||
} catch (error) {
|
||||
// console.error("Error validating node:", error);
|
||||
setValidationStatus("error");
|
||||
}
|
||||
}, 1000), // Adjust the debounce delay (500ms) as needed
|
||||
[reactFlowInstance, data.id]
|
||||
);
|
||||
useEffect(() => {
|
||||
if (params.length > 0) {
|
||||
validateNode();
|
||||
}
|
||||
}, [params, validateNode]);
|
||||
}, [sseData, data.id]);
|
||||
|
||||
if (!Icon) {
|
||||
if (showError.current) {
|
||||
|
|
@ -97,199 +60,174 @@ export default function GenericNode({
|
|||
deleteNode(data.id);
|
||||
return;
|
||||
}
|
||||
// console.log(data);
|
||||
|
||||
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
selected ? "border border-blue-500" : "border dark:border-gray-700",
|
||||
"prompt-node relative flex w-96 flex-col justify-center rounded-lg bg-white dark:bg-gray-900"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-gray-50 p-4 dark:border-b-gray-700 dark:bg-gray-800 dark:text-white ">
|
||||
<div className="flex w-full items-center gap-2 truncate text-lg">
|
||||
<Icon
|
||||
className="h-10 w-10 rounded p-1"
|
||||
style={{
|
||||
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
|
||||
}}
|
||||
/>
|
||||
<div className="ml-2 truncate">
|
||||
<TooltipReact
|
||||
delayShow={1000}
|
||||
selector={`node-selector-${data.type}`}
|
||||
htmlContent={data.type}
|
||||
position="top"
|
||||
>
|
||||
<div className="ml-2 truncate">{data.type}</div>
|
||||
</TooltipReact>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
className="relative"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
openPopUp(<NodeModal data={data} />);
|
||||
}}
|
||||
>
|
||||
<div className=" absolute -right-1 -top-2 text-red-600">
|
||||
{Object.keys(data.node.template).some(
|
||||
(t) =>
|
||||
data.node.template[t].advanced &&
|
||||
data.node.template[t].required
|
||||
)
|
||||
? " *"
|
||||
: ""}
|
||||
<>
|
||||
<NodeToolbar>
|
||||
<NodeToolbarComponent
|
||||
data={data}
|
||||
openPopUp={openPopUp}
|
||||
deleteNode={deleteNode}
|
||||
></NodeToolbarComponent>
|
||||
</NodeToolbar>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
selected ? "border border-ring" : "border dark:border-gray-700",
|
||||
"prompt-node relative flex w-96 flex-col justify-center rounded-lg bg-white dark:bg-gray-900"
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-8 rounded-t-lg border-b bg-muted p-4 dark:border-b-gray-700 dark:bg-gray-800 dark:text-white ">
|
||||
<div className="flex w-full items-center gap-2 truncate text-lg">
|
||||
<Icon
|
||||
className="h-10 w-10 rounded p-1"
|
||||
style={{
|
||||
color: nodeColors[types[data.type]] ?? nodeColors.unknown,
|
||||
}}
|
||||
/>
|
||||
<div className="ml-2 truncate">
|
||||
<ShadTooltip delayDuration={1500} content={data.type}>
|
||||
<div className="ml-2 truncate text-gray-800">{data.type}</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
<Cog6ToothIcon
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
className="relative"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
openPopUp(<NodeModal data={data} />);
|
||||
}}
|
||||
></button>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<div>
|
||||
<Tooltip
|
||||
title={
|
||||
!validationStatus ? (
|
||||
"Validating..."
|
||||
) : (
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{validationStatus.params ||
|
||||
""
|
||||
.split("\n")
|
||||
.map((line, index) => <div key={index}>{line}</div>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="w-5 h-5 relative top-[3px]">
|
||||
<div
|
||||
className={classNames(
|
||||
validationStatus && validationStatus.valid
|
||||
? "w-4 h-4 rounded-full bg-green-500 opacity-100"
|
||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
validationStatus && !validationStatus.valid
|
||||
? "w-4 h-4 rounded-full bg-red-500 opacity-100"
|
||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
!validationStatus || isBuilding
|
||||
? "w-4 h-4 rounded-full bg-yellow-500 opacity-100"
|
||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full py-5 text-gray-800">
|
||||
<div className="w-full px-5 pb-3 text-sm text-muted-foreground">
|
||||
{data.node.description}
|
||||
</div>
|
||||
|
||||
<>
|
||||
{Object.keys(data.node.template)
|
||||
.filter((t) => t.charAt(0) !== "_")
|
||||
.map((t: string, idx) => (
|
||||
<div key={idx}>
|
||||
{/* {idx === 0 ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"px-5 py-2 mt-2 dark:text-white text-center",
|
||||
Object.keys(data.node.template).filter(
|
||||
(key) =>
|
||||
!key.startsWith("_") &&
|
||||
data.node.template[key].show &&
|
||||
!data.node.template[key].advanced
|
||||
).length === 0
|
||||
? "hidden"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
Inputs
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)} */}
|
||||
{data.node.template[t].show &&
|
||||
!data.node.template[t].advanced ? (
|
||||
<ParameterComponent
|
||||
data={data}
|
||||
color={
|
||||
nodeColors[types[data.node.template[t].type]] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={
|
||||
data.node.template[t].display_name
|
||||
? data.node.template[t].display_name
|
||||
: data.node.template[t].name
|
||||
? toTitleCase(data.node.template[t].name)
|
||||
: toTitleCase(t)
|
||||
}
|
||||
name={t}
|
||||
tooltipTitle={data.node.template[t].type}
|
||||
required={data.node.template[t].required}
|
||||
id={data.node.template[t].type + "|" + t + "|" + data.id}
|
||||
left={true}
|
||||
type={data.node.template[t].type}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className={classNames(
|
||||
Object.keys(data.node.template).some(
|
||||
(t) =>
|
||||
data.node.template[t].advanced && data.node.template[t].show
|
||||
)
|
||||
? ""
|
||||
: "hidden",
|
||||
"w-5 h-5 dark:text-gray-300"
|
||||
Object.keys(data.node.template).length < 1 ? "hidden" : "",
|
||||
"flex w-full justify-center"
|
||||
)}
|
||||
></Cog6ToothIcon>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
deleteNode(data.id);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="w-5 h-5 dark:text-gray-300"></TrashIcon>
|
||||
</button>
|
||||
|
||||
<div>
|
||||
<Tooltip
|
||||
title={
|
||||
!validationStatus ? (
|
||||
"Validating..."
|
||||
) : (
|
||||
<div className="max-h-96 overflow-auto">
|
||||
{validationStatus.params.split("\n").map((line, index) => (
|
||||
<div key={index}>{line}</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div className="w-5 h-5 relative top-[3px]">
|
||||
<div
|
||||
className={classNames(
|
||||
validationStatus && validationStatus.valid
|
||||
? "w-4 h-4 rounded-full bg-green-500 opacity-100"
|
||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
validationStatus && !validationStatus.valid
|
||||
? "w-4 h-4 rounded-full bg-red-500 opacity-100"
|
||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||
)}
|
||||
></div>
|
||||
<div
|
||||
className={classNames(
|
||||
!validationStatus
|
||||
? "w-4 h-4 rounded-full bg-yellow-500 opacity-100"
|
||||
: "w-4 h-4 rounded-full bg-gray-500 opacity-0 hidden animate-spin",
|
||||
"absolute w-4 hover:text-gray-500 hover:dark:text-gray-300 transition-all ease-in-out duration-200"
|
||||
)}
|
||||
></div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{" "}
|
||||
</div>
|
||||
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
|
||||
Output
|
||||
</div> */}
|
||||
<ParameterComponent
|
||||
data={data}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
title={data.type}
|
||||
tooltipTitle={`${data.node.base_classes.join("\n")}`}
|
||||
id={[data.type, data.id, ...data.node.base_classes].join("|")}
|
||||
type={data.node.base_classes.join("|")}
|
||||
left={false}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full py-5">
|
||||
<div className="w-full px-5 pb-3 text-sm text-gray-500 dark:text-gray-300">
|
||||
{data.node.description}
|
||||
</div>
|
||||
|
||||
<>
|
||||
{Object.keys(data.node.template)
|
||||
.filter((t) => t.charAt(0) !== "_")
|
||||
.map((t: string, idx) => (
|
||||
<div key={idx}>
|
||||
{/* {idx === 0 ? (
|
||||
<div
|
||||
className={classNames(
|
||||
"px-5 py-2 mt-2 dark:text-white text-center",
|
||||
Object.keys(data.node.template).filter(
|
||||
(key) =>
|
||||
!key.startsWith("_") &&
|
||||
data.node.template[key].show &&
|
||||
!data.node.template[key].advanced
|
||||
).length === 0
|
||||
? "hidden"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
Inputs
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)} */}
|
||||
{data.node.template[t].show &&
|
||||
!data.node.template[t].advanced ? (
|
||||
<ParameterComponent
|
||||
data={data}
|
||||
color={
|
||||
nodeColors[types[data.node.template[t].type]] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
title={
|
||||
data.node.template[t].display_name
|
||||
? data.node.template[t].display_name
|
||||
: data.node.template[t].name
|
||||
? toTitleCase(data.node.template[t].name)
|
||||
: toTitleCase(t)
|
||||
}
|
||||
name={t}
|
||||
tooltipTitle={
|
||||
"Type: " +
|
||||
data.node.template[t].type +
|
||||
(data.node.template[t].list ? " list" : "")
|
||||
}
|
||||
required={data.node.template[t].required}
|
||||
id={data.node.template[t].type + "|" + t + "|" + data.id}
|
||||
left={true}
|
||||
type={data.node.template[t].type}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div
|
||||
className={classNames(
|
||||
Object.keys(data.node.template).length < 1 ? "hidden" : "",
|
||||
"flex w-full justify-center"
|
||||
)}
|
||||
>
|
||||
{" "}
|
||||
</div>
|
||||
{/* <div className="px-5 py-2 mt-2 dark:text-white text-center">
|
||||
Output
|
||||
</div> */}
|
||||
<ParameterComponent
|
||||
data={data}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
title={data.type}
|
||||
tooltipTitle={`Type: ${data.node.base_classes.join(" | ")}`}
|
||||
id={[data.type, data.id, ...data.node.base_classes].join("|")}
|
||||
type={data.node.base_classes.join("|")}
|
||||
left={false}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ export default function SingleAlert({
|
|||
{dropItem.link ? (
|
||||
<Link
|
||||
to={dropItem.link}
|
||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-blue-600"
|
||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 dark:hover:text-blue-100 hover:text-ring"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export default function NoticeAlert({
|
|||
{link !== "" ? (
|
||||
<Link
|
||||
to={link}
|
||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-blue-600"
|
||||
className="whitespace-nowrap font-medium text-blue-700 dark:text-blue-50 hover:dark:text-blue-10 hover:text-ring"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
import React, { useState, ChangeEvent } from "react";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { Label } from "../../components/ui/label";
|
||||
import { Input } from "../../components/ui/input";
|
||||
|
||||
type InputProps = {
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
maxLength?: number;
|
||||
flows: Array<{ id: string; name: string }>;
|
||||
tabId: string;
|
||||
setName: (name: string) => void;
|
||||
setDescription: (description: string) => void;
|
||||
updateFlow: (flow: { id: string; name: string }) => void;
|
||||
};
|
||||
|
||||
export const EditFlowSettings: React.FC<InputProps> = ({
|
||||
name,
|
||||
description,
|
||||
maxLength = 50,
|
||||
flows,
|
||||
tabId,
|
||||
setName,
|
||||
setDescription,
|
||||
updateFlow,
|
||||
}) => {
|
||||
const [isMaxLength, setIsMaxLength] = useState(false);
|
||||
|
||||
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.target;
|
||||
if (value.length >= maxLength) {
|
||||
setIsMaxLength(true);
|
||||
} else {
|
||||
setIsMaxLength(false);
|
||||
}
|
||||
|
||||
setName(value);
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setDescription(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label>
|
||||
<div className="flex justify-between">
|
||||
<span className="font-medium">Name</span>{" "}
|
||||
{isMaxLength && (
|
||||
<span className="text-red-500 animate-pulse ml-10">
|
||||
Character limit reached
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Input
|
||||
className="mt-2 font-normal"
|
||||
onChange={handleNameChange}
|
||||
type="text"
|
||||
name="name"
|
||||
value={name ?? ""}
|
||||
placeholder="File name"
|
||||
id="name"
|
||||
maxLength={maxLength}
|
||||
/>
|
||||
</Label>
|
||||
<Label>
|
||||
<span className="font-medium">Description (optional)</span>
|
||||
<Textarea
|
||||
name="description"
|
||||
id="description"
|
||||
onChange={handleDescriptionChange}
|
||||
value={description ?? ""}
|
||||
placeholder="Flow description"
|
||||
className="max-h-[100px] mt-2 font-normal"
|
||||
rows={3}
|
||||
/>
|
||||
</Label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditFlowSettings;
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Disclosure } from "@headlessui/react";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { useContext } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { classNames } from "../../utils";
|
||||
import { locationContext } from "../../contexts/locationContext";
|
||||
|
|
@ -13,6 +13,7 @@ export default function ExtraSidebar() {
|
|||
extraNavigation,
|
||||
extraComponent,
|
||||
} = useContext(locationContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
|
|
@ -20,12 +21,7 @@ export default function ExtraSidebar() {
|
|||
isStackedOpen ? "w-52" : "w-0 "
|
||||
} flex-shrink-0 flex overflow-hidden flex-col border-r dark:border-r-gray-700 transition-all duration-500`}
|
||||
>
|
||||
<div className="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start">
|
||||
<div className="flex pt-1 px-4 justify-between align-middle w-full">
|
||||
<span className="text-gray-900 dark:text-white py-[2px] font-medium ">
|
||||
{extraNavigation.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start bg-white">
|
||||
<div className="flex flex-grow flex-col w-full">
|
||||
{extraNavigation.options ? (
|
||||
<div className="p-4">
|
||||
|
|
@ -37,8 +33,8 @@ export default function ExtraSidebar() {
|
|||
to={item.href}
|
||||
className={classNames(
|
||||
item.href.split("/")[2] === current[4]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
? "bg-muted text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-muted hover:text-gray-900",
|
||||
"group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md"
|
||||
)}
|
||||
>
|
||||
|
|
@ -64,9 +60,9 @@ export default function ExtraSidebar() {
|
|||
<Disclosure.Button
|
||||
className={classNames(
|
||||
item.href.split("/")[2] === current[4]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
"group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
? "bg-muted text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-muted hover:text-gray-900",
|
||||
"group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
|
|
@ -97,8 +93,8 @@ export default function ExtraSidebar() {
|
|||
to={subItem.href}
|
||||
className={classNames(
|
||||
subItem.href.split("/")[3] === current[5]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
? "bg-muted text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-muted hover:text-gray-900",
|
||||
"group flex w-full items-center rounded-md py-2 pl-11 pr-2 text-sm font-medium"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -37,13 +37,15 @@ const TooltipReact: FC<TooltipProps> = ({
|
|||
id={selector}
|
||||
content={content}
|
||||
className={classNames(
|
||||
"!bg-white !text-xs !font-normal !text-gray-700 !shadow-md !opacity-100 z-20",
|
||||
"!bg-white !text-xs !font-normal !text-gray-700 !shadow-md !opacity-100 z-[9999]",
|
||||
className
|
||||
)}
|
||||
place={position}
|
||||
clickable={clickable}
|
||||
isOpen={disabled ? false : undefined}
|
||||
delayShow={delayShow}
|
||||
positionStrategy="absolute"
|
||||
float={true}
|
||||
>
|
||||
{htmlContent && htmlContent}
|
||||
</ReactTooltip>
|
||||
|
|
|
|||
26
src/frontend/src/components/ShadTooltipComponent/index.tsx
Normal file
26
src/frontend/src/components/ShadTooltipComponent/index.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "../ui/tooltip";
|
||||
|
||||
const ShadTooltip = (props) => {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={props.delayDuration}>
|
||||
<TooltipTrigger asChild>{props.children}</TooltipTrigger>
|
||||
|
||||
<TooltipContent
|
||||
side={props.side}
|
||||
avoidCollisions={false}
|
||||
sticky="always"
|
||||
>
|
||||
{props.content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShadTooltip;
|
||||
71
src/frontend/src/components/cardComponent/index.tsx
Normal file
71
src/frontend/src/components/cardComponent/index.tsx
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { Trash2, ExternalLink } from "lucide-react";
|
||||
import { useContext } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { gradients } from "../../utils";
|
||||
import {
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
Card,
|
||||
CardHeader,
|
||||
} from "../ui/card";
|
||||
|
||||
export const CardComponent = ({
|
||||
flow,
|
||||
id,
|
||||
onDelete,
|
||||
button,
|
||||
}: {
|
||||
flow: FlowType;
|
||||
id: string;
|
||||
onDelete?: () => void;
|
||||
button?: JSX.Element;
|
||||
}) => {
|
||||
const { removeFlow } = useContext(TabsContext);
|
||||
|
||||
return (
|
||||
<Card className="group">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex w-full items-center gap-4">
|
||||
<span
|
||||
className={
|
||||
"rounded-full w-7 h-7 flex items-center justify-center text-2xl " +
|
||||
gradients[parseInt(flow.id.slice(0, 12), 16) % gradients.length]
|
||||
}
|
||||
></span>
|
||||
<span className="flex-1 w-full inline-block truncate-doubleline break-words">
|
||||
{flow.name}
|
||||
</span>
|
||||
{onDelete && (
|
||||
<button className="flex self-start" onClick={onDelete}>
|
||||
<Trash2 className="w-4 h-4 text-primary opacity-0 group-hover:opacity-100 transition-all" />
|
||||
</button>
|
||||
)}
|
||||
</CardTitle>
|
||||
<CardDescription className="pt-2 pb-2">
|
||||
<div className="truncate-doubleline">
|
||||
{flow.description}
|
||||
{/* {flow.description} */}
|
||||
</div>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardFooter>
|
||||
<div className="flex gap-2 w-full justify-between items-end">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{/* <Badge variant="secondary">Agent</Badge>
|
||||
<Badge variant="secondary">
|
||||
<div className="w-3">
|
||||
<OpenAiIcon />
|
||||
</div>
|
||||
<span className="text-base"> </span>OpenAI+
|
||||
</Badge> */}
|
||||
</div>
|
||||
{button && button}
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
165
src/frontend/src/components/chatComponent/buildTrigger/index.tsx
Normal file
165
src/frontend/src/components/chatComponent/buildTrigger/index.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import { useContext } from "react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { Zap } from "lucide-react";
|
||||
import { validateNodes } from "../../../utils";
|
||||
import { FlowType } from "../../../types/flow";
|
||||
import Loading from "../../../components/ui/loading";
|
||||
import { useSSE } from "../../../contexts/SSEContext";
|
||||
import { typesContext } from "../../../contexts/typesContext";
|
||||
import { alertContext } from "../../../contexts/alertContext";
|
||||
import { postBuildInit } from "../../../controllers/API";
|
||||
|
||||
export default function BuildTrigger({
|
||||
open,
|
||||
flow,
|
||||
setIsBuilt,
|
||||
isBuilt,
|
||||
}: {
|
||||
open: boolean;
|
||||
flow: FlowType;
|
||||
setIsBuilt: any;
|
||||
isBuilt: boolean;
|
||||
}) {
|
||||
const { updateSSEData, isBuilding, setIsBuilding } = useSSE();
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
|
||||
async function handleBuild(flow: FlowType) {
|
||||
try {
|
||||
if (isBuilding) {
|
||||
return;
|
||||
}
|
||||
const errors = validateNodes(reactFlowInstance);
|
||||
if (errors.length > 0) {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed something",
|
||||
list: errors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const minimumLoadingTime = 200; // in milliseconds
|
||||
const startTime = Date.now();
|
||||
setIsBuilding(true);
|
||||
|
||||
const allNodesValid = await streamNodeData(flow);
|
||||
await enforceMinimumLoadingTime(startTime, minimumLoadingTime);
|
||||
setIsBuilt(allNodesValid);
|
||||
if (!allNodesValid) {
|
||||
setErrorData({
|
||||
title: "Oops! Looks like you missed something",
|
||||
list: [
|
||||
"Check components and retry. Hover over component status icon 🔴 to inspect.",
|
||||
],
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
setIsBuilding(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function streamNodeData(flow: FlowType) {
|
||||
// Step 1: Make a POST request to send the flow data and receive a unique session ID
|
||||
const response = await postBuildInit(flow);
|
||||
const { flowId } = response.data;
|
||||
|
||||
// Step 2: Use the session ID to establish an SSE connection using EventSource
|
||||
let validationResults = [];
|
||||
let finished = false;
|
||||
const apiUrl = `/api/v1/build/stream/${flowId}`;
|
||||
const eventSource = new EventSource(apiUrl);
|
||||
|
||||
eventSource.onmessage = (event) => {
|
||||
// If the event is parseable, return
|
||||
if (!event.data) {
|
||||
return;
|
||||
}
|
||||
const parsedData = JSON.parse(event.data);
|
||||
// if the event is the end of the stream, close the connection
|
||||
if (parsedData.end_of_stream) {
|
||||
eventSource.close();
|
||||
|
||||
return;
|
||||
} else if (parsedData.log) {
|
||||
// If the event is a log, log it
|
||||
// TODO: implement the progress
|
||||
setSuccessData({ title: parsedData.log });
|
||||
setSuccessData({ title: parsedData.progress });
|
||||
} else {
|
||||
// Otherwise, process the data
|
||||
const isValid = processStreamResult(parsedData);
|
||||
validationResults.push(isValid);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = (error) => {
|
||||
console.error("EventSource failed:", error);
|
||||
eventSource.close();
|
||||
};
|
||||
// Step 3: Wait for the stream to finish
|
||||
while (!finished) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
finished = validationResults.length === flow.data.nodes.length;
|
||||
}
|
||||
// Step 4: Return true if all nodes are valid, false otherwise
|
||||
return validationResults.every((result) => result);
|
||||
}
|
||||
|
||||
function processStreamResult(parsedData) {
|
||||
// Process each chunk of data here
|
||||
// Parse the chunk and update the context
|
||||
try {
|
||||
updateSSEData({ [parsedData.id]: parsedData });
|
||||
} catch (err) {
|
||||
console.log("Error parsing stream data: ", err);
|
||||
}
|
||||
return parsedData.valid;
|
||||
}
|
||||
|
||||
async function enforceMinimumLoadingTime(
|
||||
startTime: number,
|
||||
minimumLoadingTime: number
|
||||
) {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
const remainingTime = minimumLoadingTime - elapsedTime;
|
||||
|
||||
if (remainingTime > 0) {
|
||||
return new Promise((resolve) => setTimeout(resolve, remainingTime));
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition
|
||||
show={!open}
|
||||
appear={true}
|
||||
enter="transition ease-out duration-300"
|
||||
enterFrom="translate-y-96"
|
||||
enterTo="translate-y-0"
|
||||
leave="transition ease-in duration-300"
|
||||
leaveFrom="translate-y-0"
|
||||
leaveTo="translate-y-96"
|
||||
>
|
||||
<div className={`fixed right-4` + (isBuilt ? " bottom-20" : " bottom-4")}>
|
||||
<div
|
||||
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
||||
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
handleBuild(flow);
|
||||
}}
|
||||
>
|
||||
<button>
|
||||
<div className="flex gap-3 items-center">
|
||||
{isBuilding ? (
|
||||
// Render your loading animation here when isBuilding is true
|
||||
<Loading strokeWidth={1.5} style={{ color: "white" }} />
|
||||
) : (
|
||||
<Zap className="sh-6 w-6 fill-orange-400 stroke-1 stroke-orange-400" />
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,15 +1,23 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import {
|
||||
Bars3CenterLeftIcon,
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { nodeColors } from "../../../utils";
|
||||
import { PopUpContext } from "../../../contexts/popUpContext";
|
||||
import { useContext } from "react";
|
||||
import ChatModal from "../../../modals/chatModal";
|
||||
import { MessagesSquare } from "lucide-react";
|
||||
|
||||
import { alertContext } from "../../../contexts/alertContext";
|
||||
import { useContext } from "react";
|
||||
|
||||
export default function ChatTrigger({ open, setOpen, isBuilt }) {
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
||||
function handleClick() {
|
||||
if (isBuilt) {
|
||||
setOpen(true);
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Flow not built",
|
||||
list: ["Please build the flow before chatting"],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function ChatTrigger({ open, setOpen }) {
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
return (
|
||||
<Transition
|
||||
show={!open}
|
||||
|
|
@ -23,16 +31,16 @@ export default function ChatTrigger({ open, setOpen }) {
|
|||
>
|
||||
<div className="absolute bottom-4 right-3">
|
||||
<div
|
||||
className="border flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full bg-gradient-to-r from-blue-500 via-blue-600 to-blue-700 dark:border-gray-600 cursor-pointer"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
className="flex justify-center align-center py-1 px-3 w-12 h-12 rounded-full shadow-md shadow-[#0000002a] hover:shadow-[#00000032]
|
||||
bg-[#E2E7EE] dark:border-gray-600 cursor-pointer"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<button>
|
||||
<div className="flex gap-3 items-center">
|
||||
<ChatBubbleBottomCenterTextIcon
|
||||
className="h-6 w-6 mt-1"
|
||||
<div className="flex gap-3">
|
||||
<MessagesSquare
|
||||
className="pth-6 w-6 fill-[#5c8be1] stroke-1 stroke-[#5c8be1]"
|
||||
style={{ color: "white" }}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,23 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import { ChatMessageType, ChatType } from "../../types/chat";
|
||||
import { useNodes } from "reactflow";
|
||||
import { ChatType } from "../../types/chat";
|
||||
import ChatTrigger from "./chatTrigger";
|
||||
import BuildTrigger from "./buildTrigger";
|
||||
import ChatModal from "../../modals/chatModal";
|
||||
|
||||
import _ from "lodash";
|
||||
import { getBuildStatus } from "../../controllers/API";
|
||||
import { NodeType } from "../../types/flow";
|
||||
|
||||
export default function Chat({ flow }: ChatType) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [isBuilt, setIsBuilt] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.key === "K" || event.key === "k") &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
isBuilt
|
||||
) {
|
||||
event.preventDefault();
|
||||
setOpen((oldState) => !oldState);
|
||||
|
|
@ -22,11 +27,58 @@ export default function Chat({ flow }: ChatType) {
|
|||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, []);
|
||||
}, [isBuilt]);
|
||||
|
||||
useEffect(() => {
|
||||
// Define an async function within the useEffect hook
|
||||
const fetchBuildStatus = async () => {
|
||||
const response = await getBuildStatus(flow.id);
|
||||
setIsBuilt(response.built);
|
||||
};
|
||||
|
||||
// Call the async function
|
||||
fetchBuildStatus();
|
||||
}, [flow]);
|
||||
|
||||
const prevNodesRef = useRef<any[] | undefined>();
|
||||
const nodes = useNodes();
|
||||
useEffect(() => {
|
||||
const prevNodes = prevNodesRef.current;
|
||||
const currentNodes = nodes.map(
|
||||
(node: NodeType) => node.data.node.template.value
|
||||
);
|
||||
|
||||
if (
|
||||
prevNodes &&
|
||||
JSON.stringify(prevNodes) !== JSON.stringify(currentNodes)
|
||||
) {
|
||||
setIsBuilt(false);
|
||||
}
|
||||
|
||||
prevNodesRef.current = currentNodes;
|
||||
}, [nodes]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
||||
<ChatTrigger open={open} setOpen={setOpen} />
|
||||
{isBuilt ? (
|
||||
<div>
|
||||
<BuildTrigger
|
||||
open={open}
|
||||
flow={flow}
|
||||
setIsBuilt={setIsBuilt}
|
||||
isBuilt={isBuilt}
|
||||
/>
|
||||
<ChatModal key={flow.id} flow={flow} open={open} setOpen={setOpen} />
|
||||
<ChatTrigger open={open} setOpen={setOpen} isBuilt={isBuilt} />
|
||||
</div>
|
||||
) : (
|
||||
<BuildTrigger
|
||||
open={open}
|
||||
flow={flow}
|
||||
setIsBuilt={setIsBuilt}
|
||||
isBuilt={isBuilt}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ import { PopUpContext } from "../../contexts/popUpContext";
|
|||
import CodeAreaModal from "../../modals/codeAreaModal";
|
||||
import TextAreaModal from "../../modals/textAreaModal";
|
||||
import { TextAreaComponentType } from "../../types/components";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function CodeAreaComponent({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: TextAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
|
|
@ -18,13 +20,18 @@ export default function CodeAreaComponent({
|
|||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
disabled ? "pointer-events-none cursor-not-allowed w-full" : "w-full"
|
||||
}
|
||||
>
|
||||
<div className="w-full flex items-center gap-3">
|
||||
<div className="w-full flex items-center">
|
||||
<span
|
||||
onClick={() => {
|
||||
openPopUp(
|
||||
|
|
@ -38,11 +45,15 @@ export default function CodeAreaComponent({
|
|||
);
|
||||
}}
|
||||
className={
|
||||
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
editNode
|
||||
? "truncate cursor-pointer placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 border-1 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
: "truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "Text empty"}
|
||||
{myValue !== "" ? myValue : "Type something..."}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -57,7 +68,9 @@ export default function CodeAreaComponent({
|
|||
);
|
||||
}}
|
||||
>
|
||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" />
|
||||
{!editNode && (
|
||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300 ml-3" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,19 @@ import { ChevronUpDownIcon, CheckIcon } from "@heroicons/react/24/outline";
|
|||
import { Fragment, useState } from "react";
|
||||
import { DropDownComponentType } from "../../types/components";
|
||||
import { classNames } from "../../utils";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function Dropdown({
|
||||
value,
|
||||
options,
|
||||
onSelect,
|
||||
editNode = false,
|
||||
numberOfOptions = 0,
|
||||
}: DropDownComponentType) {
|
||||
let [internalValue, setInternalValue] = useState(
|
||||
value === "" || !value ? "Choose an option" : value
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Listbox
|
||||
|
|
@ -23,10 +27,22 @@ export default function Dropdown({
|
|||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="relative mt-1 w-full">
|
||||
<Listbox.Button className="relative w-full cursor-default rounded-md border border-gray-300 bg-white dark:bg-gray-900 py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
|
||||
<div className={editNode ? "mt-1" : "relative mt-1"}>
|
||||
<Listbox.Button
|
||||
className={
|
||||
editNode
|
||||
? "relative pr-8 placeholder:text-center block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md shadow-sm sm:text-sm border-gray-300 border-1" +
|
||||
INPUT_STYLE
|
||||
: "ring-1 ring-slate-300 dark:ring-slate-600 w-full py-2 pl-3 pr-10 text-left dark:focus:ring-offset-2 dark:focus:ring-offset-gray-900 dark:focus:ring-1 dark:focus:ring-gray-600 dark:focus-visible:ring-gray-900 dark:focus-visible:ring-offset-2 focus-visible:outline-none dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
}
|
||||
>
|
||||
<span className="block truncate w-full">{internalValue}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<span
|
||||
className={
|
||||
"pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2"
|
||||
}
|
||||
>
|
||||
<ChevronUpDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
|
|
@ -41,16 +57,24 @@ export default function Dropdown({
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full dark:bg-gray-800 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
<Listbox.Options
|
||||
className={
|
||||
editNode
|
||||
? "absolute z-10 mt-1 max-h-60 overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm w-[215px]"
|
||||
: "nowheel absolute z-10 mt-1 max-h-60 w-full overflow-auto overflow-y rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm "
|
||||
}
|
||||
>
|
||||
{options.map((option, id) => (
|
||||
<Listbox.Option
|
||||
key={id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active
|
||||
? "text-white bg-indigo-600 dark:bg-indigo-500"
|
||||
: "text-gray-900",
|
||||
"relative cursor-default select-none py-2 pl-3 pr-9 dark:text-gray-300 dark:bg-gray-800"
|
||||
? " bg-accent dark:bg-white dark:text-gray-500"
|
||||
: "",
|
||||
editNode
|
||||
? "relative cursor-default select-none py-0.5 pl-3 pr-12 dark:text-gray-300 dark:bg-gray-800"
|
||||
: "relative cursor-default select-none py-2 pl-3 pr-9 dark:text-gray-300 dark:bg-gray-800"
|
||||
)
|
||||
}
|
||||
value={option}
|
||||
|
|
@ -60,7 +84,7 @@ export default function Dropdown({
|
|||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate"
|
||||
"block truncate "
|
||||
)}
|
||||
>
|
||||
{option}
|
||||
|
|
@ -69,12 +93,16 @@ export default function Dropdown({
|
|||
{selected ? (
|
||||
<span
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-indigo-600",
|
||||
active ? "text-white dark:text-black" : "",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
)}
|
||||
>
|
||||
<CheckIcon
|
||||
className="h-5 w-5"
|
||||
className={
|
||||
active
|
||||
? "h-5 w-5 dark:text-black text-black"
|
||||
: "h-5 w-5 dark:text-white text-black"
|
||||
}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,22 +1,33 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { FloatComponentType } from "../../types/components";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function FloatComponent({
|
||||
value,
|
||||
onChange,
|
||||
disableCopyPaste = false,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: FloatComponentType) {
|
||||
const [myValue, setMyValue] = useState(value ?? "");
|
||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||
|
||||
const step = 0.1;
|
||||
const min = 0;
|
||||
const max = 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -31,12 +42,29 @@ export default function FloatComponent({
|
|||
if (disableCopyPaste) setDisableCopyPaste(false);
|
||||
}}
|
||||
type="number"
|
||||
step={step}
|
||||
min={min}
|
||||
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value < min.toString()) {
|
||||
e.target.value = min.toString();
|
||||
}
|
||||
if (e.target.value > max.toString()) {
|
||||
e.target.value = max.toString();
|
||||
}
|
||||
}}
|
||||
max={max}
|
||||
value={myValue}
|
||||
className={
|
||||
"block w-full form-input dark:bg-gray-900 arrow-hide dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
|
||||
editNode
|
||||
? "focus:placeholder-transparent text-center placeholder:text-center border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
: "focus:placeholder-transparent block w-full form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm ring-offset-gray-200 sm:text-sm" +
|
||||
INPUT_STYLE +
|
||||
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
|
||||
}
|
||||
placeholder={
|
||||
editNode ? "Number 0 to 1" : "Type a number from zero to one"
|
||||
}
|
||||
placeholder="Type a number from zero to one"
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,131 @@
|
|||
import { useContext } from "react";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||
import {
|
||||
Plus,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
Undo,
|
||||
Redo,
|
||||
Settings2,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
} from "../../../ui/dropdown-menu";
|
||||
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
|
||||
import { Button } from "../../../ui/button";
|
||||
|
||||
export const MenuBar = ({ flows, tabId }) => {
|
||||
const { updateFlow, setTabId, addFlow } = useContext(TabsContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
const { undo, redo } = useContext(undoRedoContext);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
function handleAddFlow() {
|
||||
try {
|
||||
addFlow(null, true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
// saveFlowStyleInDataBase();
|
||||
} catch (err) {
|
||||
setErrorData(err);
|
||||
}
|
||||
}
|
||||
let current_flow = flows.find((flow) => flow.id === tabId);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 items-center">
|
||||
<Link to="/">
|
||||
<ChevronLeft className="w-4" />
|
||||
</Link>
|
||||
<div className="flex items-center font-medium text-sm rounded-md py-1 px-1.5 gap-0.5">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
className="gap-2 flex items-center max-w-[200px]"
|
||||
variant="primary"
|
||||
size="sm"
|
||||
>
|
||||
<div className="truncate flex-1">{current_flow.name}</div>
|
||||
<ChevronDown className="w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-44">
|
||||
<DropdownMenuLabel>Options</DropdownMenuLabel>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
handleAddFlow();
|
||||
}}
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
New
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
openPopUp(<FlowSettingsModal />);
|
||||
}}
|
||||
>
|
||||
<Settings2 className="w-4 h-4 mr-2 dark:text-gray-300" />
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
undo();
|
||||
}}
|
||||
>
|
||||
<Undo className="w-4 h-4 mr-2 dark:text-gray-300" />
|
||||
Undo
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
redo();
|
||||
}}
|
||||
>
|
||||
<Redo className="w-4 h-4 mr-2 dark:text-gray-300" />
|
||||
Redo
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{/* <DropdownMenuLabel>Projects</DropdownMenuLabel> */}
|
||||
{/* <DropdownMenuRadioGroup className="max-h-full overflow-scroll"
|
||||
value={tabId}
|
||||
onValueChange={(value) => {
|
||||
setTabId(value);
|
||||
}}
|
||||
>
|
||||
{flows.map((flow, idx) => {
|
||||
return (
|
||||
<Link
|
||||
to={"/flow/" + flow.id}
|
||||
className="flex w-full items-center"
|
||||
>
|
||||
<DropdownMenuRadioItem
|
||||
value={flow.id}
|
||||
className="flex-1 w-full inline-block truncate break-words mr-2"
|
||||
>
|
||||
{flow.name}
|
||||
</DropdownMenuRadioItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuRadioGroup> */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuBar;
|
||||
143
src/frontend/src/components/headerComponent/index.tsx
Normal file
143
src/frontend/src/components/headerComponent/index.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { BellIcon, Home, MoonIcon, SunIcon, Users2 } from "lucide-react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { FaDiscord, FaGithub, FaTwitter } from "react-icons/fa";
|
||||
import { Button } from "../ui/button";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import AlertDropdown from "../../alerts/alertDropDown";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import MenuBar from "./components/menuBar";
|
||||
import { Link, useLocation, useParams } from "react-router-dom";
|
||||
import { USER_PROJECTS_HEADER } from "../../constants";
|
||||
import { getRepoStars } from "../../controllers/API";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
export default function Header() {
|
||||
const { flows, addFlow, tabId } = useContext(TabsContext);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
const { templates } = useContext(typesContext);
|
||||
const { id } = useParams();
|
||||
const AlertWidth = 384;
|
||||
const { dark, setDark } = useContext(darkContext);
|
||||
const { notificationCenter, setNotificationCenter, setErrorData } =
|
||||
useContext(alertContext);
|
||||
const location = useLocation();
|
||||
|
||||
const [stars, setStars] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchStars() {
|
||||
const starsCount = await getRepoStars("logspace-ai", "langflow");
|
||||
setStars(starsCount);
|
||||
}
|
||||
fetchStars();
|
||||
}, []);
|
||||
return (
|
||||
<div className="w-full h-12 flex justify-between items-center border-b bg-muted">
|
||||
<div className="flex gap-2 justify-start items-center w-96">
|
||||
<Link to="/">
|
||||
<span className="text-2xl ml-4">⛓️</span>
|
||||
</Link>
|
||||
{flows.findIndex((f) => tabId === f.id) !== -1 && tabId !== "" && (
|
||||
<MenuBar flows={flows} tabId={tabId} />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Link to="/">
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant={location.pathname === "/" ? "primary" : "secondary"}
|
||||
size="sm"
|
||||
>
|
||||
<Home className="w-4 h-4" />
|
||||
<div className="flex-1">{USER_PROJECTS_HEADER}</div>
|
||||
</Button>
|
||||
</Link>
|
||||
<Link to="/community">
|
||||
<Button
|
||||
className="gap-2"
|
||||
variant={
|
||||
location.pathname === "/community" ? "primary" : "secondary"
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
<Users2 className="w-4 h-4" />
|
||||
<div className="flex-1">Community Examples</div>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex justify-end px-2 w-96">
|
||||
<div className="ml-auto mr-2 flex gap-5 items-center">
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex shadow-sm items-center justify-center text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background text-gray-600 dark:text-gray-300 border border-input hover:bg-accent hover:text-accent-foreground h-9 px-3 pr-0 rounded-md"
|
||||
>
|
||||
<FaGithub className="h-5 w-5 mr-2" />
|
||||
Star
|
||||
<div className="ml-2 flex text-sm bg-background rounded-md rounded-l-none border px-2 h-9 -mr-px items-center justify-center">
|
||||
{stars}
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
href="https://twitter.com/logspace_ai"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<FaTwitter className="h-5 w-5" />
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/EqksyE2EX9"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<FaDiscord className="h-5 w-5" />
|
||||
</a>
|
||||
<Separator orientation="vertical" />
|
||||
<button
|
||||
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
|
||||
onClick={() => {
|
||||
setDark(!dark);
|
||||
}}
|
||||
>
|
||||
{dark ? (
|
||||
<SunIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<MoonIcon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative"
|
||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
setNotificationCenter(false);
|
||||
const { top, left } = (
|
||||
event.target as Element
|
||||
).getBoundingClientRect();
|
||||
openPopUp(
|
||||
<>
|
||||
<div
|
||||
className="z-10 absolute"
|
||||
style={{ top: top + 34, left: left - AlertWidth }}
|
||||
>
|
||||
<AlertDropdown />
|
||||
</div>
|
||||
<div className="h-screen w-screen fixed top-0 left-0"></div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{notificationCenter && (
|
||||
<div className="absolute w-1.5 h-1.5 rounded-full bg-destructive right-[3px]"></div>
|
||||
)}
|
||||
<BellIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import { useContext, useEffect, useState } from "react";
|
|||
import { InputComponentType } from "../../types/components";
|
||||
import { classNames } from "../../utils";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function InputComponent({
|
||||
value,
|
||||
|
|
@ -9,16 +11,24 @@ export default function InputComponent({
|
|||
disableCopyPaste = false,
|
||||
disabled,
|
||||
password,
|
||||
editNode = false,
|
||||
}: InputComponentType) {
|
||||
const [myValue, setMyValue] = useState(value ?? "");
|
||||
const [pwdVisible, setPwdVisible] = useState(false);
|
||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value ?? "");
|
||||
}, [closePopUp]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -36,60 +46,79 @@ export default function InputComponent({
|
|||
if (disableCopyPaste) setDisableCopyPaste(false);
|
||||
}}
|
||||
className={classNames(
|
||||
"block w-full pr-12 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
|
||||
"block w-full pr-12 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm focus:placeholder-transparent",
|
||||
disabled ? " bg-gray-200 dark:bg-gray-700" : "",
|
||||
password && !pwdVisible && myValue !== "" ? "password" : ""
|
||||
password && !pwdVisible && myValue !== "" ? "password" : "",
|
||||
editNode
|
||||
? "border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm text-center" +
|
||||
INPUT_STYLE
|
||||
: "ring-offset-gray-200" + INPUT_STYLE,
|
||||
password && editNode ? "pr-8" : "pr-3"
|
||||
)}
|
||||
placeholder="Type something..."
|
||||
placeholder={password && editNode ? "Key" : "Type something..."}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="absolute inset-y-0 right-0 items-center px-4 text-gray-600"
|
||||
onClick={() => {
|
||||
setPwdVisible(!pwdVisible);
|
||||
}}
|
||||
>
|
||||
{password &&
|
||||
(pwdVisible ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
))}
|
||||
</button>
|
||||
{password && (
|
||||
<button
|
||||
className={classNames(
|
||||
editNode
|
||||
? "absolute inset-y-0 right-0 pr-2 items-center text-gray-600"
|
||||
: "absolute inset-y-0 right-0 items-center px-4 text-gray-600"
|
||||
)}
|
||||
onClick={() => {
|
||||
setPwdVisible(!pwdVisible);
|
||||
}}
|
||||
>
|
||||
{password &&
|
||||
(pwdVisible ? (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={classNames(
|
||||
editNode
|
||||
? "w-5 h-5 absolute bottom-0.5 right-2"
|
||||
: "w-5 h-5 absolute bottom-2 right-3"
|
||||
)}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.774M6.228 6.228L3 3m3.228 3.228l3.65 3.65m7.894 7.894L21 21m-3.228-3.228l-3.65-3.65m0 0a3 3 0 10-4.243-4.243m4.242 4.242L9.88 9.88"
|
||||
/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className={classNames(
|
||||
editNode
|
||||
? "w-5 h-5 absolute bottom-0.5 right-2"
|
||||
: "w-5 h-5 absolute bottom-2 right-3"
|
||||
)}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
))}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useContext, useEffect, useState } from "react";
|
|||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { FileComponentType } from "../../types/components";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function InputFileComponent({
|
||||
value,
|
||||
|
|
@ -11,6 +12,7 @@ export default function InputFileComponent({
|
|||
suffixes,
|
||||
fileTypes,
|
||||
onFileChange,
|
||||
editNode = false,
|
||||
}: FileComponentType) {
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
|
|
@ -32,6 +34,11 @@ export default function InputFileComponent({
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
// Create a file input element
|
||||
const input = document.createElement("input");
|
||||
|
|
@ -96,14 +103,20 @@ export default function InputFileComponent({
|
|||
<span
|
||||
onClick={handleButtonClick}
|
||||
className={
|
||||
"truncate block w-full text-gray-500 dark:text-gray-300 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
editNode
|
||||
? "truncate placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm border-1" +
|
||||
INPUT_STYLE
|
||||
: "truncate block w-full text-gray-500 dark:text-gray-300 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "No file"}
|
||||
</span>
|
||||
<button onClick={handleButtonClick}>
|
||||
<DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-blue-600" />
|
||||
{!editNode && (
|
||||
<DocumentMagnifyingGlassIcon className="w-8 h-8 hover:text-ring" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import { InputListComponentType } from "../../types/components";
|
|||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
|
||||
import _ from "lodash";
|
||||
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
export default function InputListComponent({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: InputListComponentType) {
|
||||
const [inputList, setInputList] = useState(value ?? [""]);
|
||||
useEffect(() => {
|
||||
|
|
@ -21,7 +22,7 @@ export default function InputListComponent({
|
|||
<div
|
||||
className={
|
||||
(disabled ? "pointer-events-none cursor-not-allowed" : "") +
|
||||
"flex flex-col gap-3"
|
||||
"flex flex-col gap-3 py-2"
|
||||
}
|
||||
>
|
||||
{inputList.map((i, idx) => (
|
||||
|
|
@ -30,8 +31,12 @@ export default function InputListComponent({
|
|||
type="text"
|
||||
value={i}
|
||||
className={
|
||||
"block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
editNode
|
||||
? "border-[1px] truncate cursor-pointer text-center placeholder:text-center text-gray-500 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
: "block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "") +
|
||||
"focus:placeholder-transparent"
|
||||
}
|
||||
placeholder="Type something..."
|
||||
onChange={(e) => {
|
||||
|
|
@ -54,7 +59,7 @@ export default function InputListComponent({
|
|||
onChange(inputList);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="w-4 h-4 hover:text-blue-600" />
|
||||
<PlusIcon className={"w-4 h-4 hover:text-ring"} />
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,15 +1,19 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { FloatComponentType } from "../../types/components";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { classNames } from "../../utils";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function IntComponent({
|
||||
value,
|
||||
onChange,
|
||||
disableCopyPaste = false,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: FloatComponentType) {
|
||||
const [myValue, setMyValue] = useState(value ?? "");
|
||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||
const min = 0;
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -17,6 +21,11 @@ export default function IntComponent({
|
|||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -32,7 +41,7 @@ export default function IntComponent({
|
|||
if (disableCopyPaste) setDisableCopyPaste(false);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
console.log(event);
|
||||
// console.log(event);
|
||||
if (
|
||||
event.key !== "Backspace" &&
|
||||
event.key !== "Enter" &&
|
||||
|
|
@ -51,12 +60,23 @@ export default function IntComponent({
|
|||
}
|
||||
}}
|
||||
type="number"
|
||||
step="1"
|
||||
min={min}
|
||||
onInput={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value < min.toString()) {
|
||||
e.target.value = min.toString();
|
||||
}
|
||||
}}
|
||||
value={myValue}
|
||||
className={
|
||||
"block w-full form-input dark:bg-gray-900 arrow-hide dark:border-gray-600 dark:text-gray-300 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
|
||||
editNode
|
||||
? "focus:placeholder-transparent text-center placeholder:text-center border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
: "focus:placeholder-transparent block w-full form-input dark:bg-gray-900 dark:border-gray-600 dark:text-gray-300 rounded-md border-gray-300 shadow-sm ring-offset-background sm:text-sm" +
|
||||
INPUT_STYLE +
|
||||
(disabled ? " bg-gray-200 dark:bg-gray-700" : "")
|
||||
}
|
||||
placeholder="Type a integer number"
|
||||
placeholder={editNode ? "Integer number" : "Type a integer number"}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export default function LoadingComponent({ remSize }: LoadingComponentProps) {
|
|||
<div role="status" className="w-min m-auto">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className={`w-${remSize} h-${remSize} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`}
|
||||
className={`w-${remSize} h-${remSize} mr-2 text-muted animate-spin dark:text-gray-600 fill-blue-600`}
|
||||
viewBox="0 0 100 101"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import CodeAreaModal from "../../modals/codeAreaModal";
|
||||
import TextAreaModal from "../../modals/textAreaModal";
|
||||
import { TextAreaComponentType } from "../../types/components";
|
||||
import GenericModal from "../../modals/genericModal";
|
||||
import { TypeModal } from "../../utils";
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
|
||||
export default function PromptAreaComponent({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: TextAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
|
|
@ -20,6 +20,11 @@ export default function PromptAreaComponent({
|
|||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -43,11 +48,14 @@ export default function PromptAreaComponent({
|
|||
);
|
||||
}}
|
||||
className={
|
||||
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
editNode
|
||||
? "cursor-pointer truncate placeholder:text-center text-gray-500 border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
: "truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "Text empty"}
|
||||
{myValue !== "" ? myValue : "Type your prompt here"}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -65,7 +73,9 @@ export default function PromptAreaComponent({
|
|||
);
|
||||
}}
|
||||
>
|
||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" />
|
||||
{!editNode && (
|
||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,23 +4,36 @@ import { PopUpContext } from "../../contexts/popUpContext";
|
|||
import { TextAreaComponentType } from "../../types/components";
|
||||
import GenericModal from "../../modals/genericModal";
|
||||
import { TypeModal } from "../../utils";
|
||||
|
||||
import { INPUT_STYLE } from "../../constants";
|
||||
export default function TextAreaComponent({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
}: TextAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
const { openPopUp, closePopUp } = useContext(PopUpContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setMyValue(value);
|
||||
}, [closePopUp]);
|
||||
|
||||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||
<div className="w-full flex items-center gap-3">
|
||||
<div
|
||||
className={
|
||||
editNode
|
||||
? "w-full flex items-center"
|
||||
: "w-full flex items-center gap-3"
|
||||
}
|
||||
>
|
||||
<span
|
||||
onClick={() => {
|
||||
openPopUp(
|
||||
|
|
@ -37,11 +50,14 @@ export default function TextAreaComponent({
|
|||
);
|
||||
}}
|
||||
className={
|
||||
"truncate block w-full text-gray-500 dark:text-gray-100 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
editNode
|
||||
? "truncate cursor-pointer placeholder:text-center text-gray-500 border-1 block w-full pt-0.5 pb-0.5 form-input dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 rounded-md border-gray-300 shadow-sm sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
: "truncate block w-full text-gray-500 dark:text-muted px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : "Text empty"}
|
||||
{myValue !== "" ? myValue : "Type something..."}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
|
@ -59,7 +75,9 @@ export default function TextAreaComponent({
|
|||
);
|
||||
}}
|
||||
>
|
||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600 dark:text-gray-300" />
|
||||
{!editNode && (
|
||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-ring dark:text-gray-300" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ export default function ToggleComponent({
|
|||
setEnabled(x);
|
||||
}}
|
||||
className={classNames(
|
||||
enabled ? "bg-indigo-600" : "bg-gray-200",
|
||||
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2"
|
||||
enabled ? "bg-primary" : "bg-gray-200",
|
||||
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-1 focus:ring-primary focus:ring-offset-1"
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
|
|
|
|||
48
src/frontend/src/components/toggleShadComponent/index.tsx
Normal file
48
src/frontend/src/components/toggleShadComponent/index.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { useEffect } from "react";
|
||||
import { ToggleComponentType } from "../../types/components";
|
||||
import { Switch } from "../ui/switch";
|
||||
|
||||
export default function ToggleShadComponent({
|
||||
enabled,
|
||||
setEnabled,
|
||||
disabled,
|
||||
size,
|
||||
}: ToggleComponentType) {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setEnabled(false);
|
||||
}
|
||||
}, [disabled, setEnabled]);
|
||||
let scaleX, scaleY;
|
||||
switch (size) {
|
||||
case "small":
|
||||
scaleX = 0.6;
|
||||
scaleY = 0.6;
|
||||
break;
|
||||
case "medium":
|
||||
scaleX = 0.8;
|
||||
scaleY = 0.8;
|
||||
break;
|
||||
case "large":
|
||||
scaleX = 1;
|
||||
scaleY = 1;
|
||||
break;
|
||||
default:
|
||||
scaleX = 1;
|
||||
scaleY = 1;
|
||||
}
|
||||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||
<Switch
|
||||
style={{
|
||||
transform: `scaleX(${scaleX}) scaleY(${scaleY})`,
|
||||
}}
|
||||
className="data-[state=unchecked]:bg-slate-500"
|
||||
checked={enabled}
|
||||
onCheckedChange={(x: boolean) => {
|
||||
setEnabled(x);
|
||||
}}
|
||||
></Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
src/frontend/src/components/ui/badge.tsx
Normal file
35
src/frontend/src/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center border rounded-full px-2.5 h-6 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
56
src/frontend/src/components/ui/button.tsx
Normal file
56
src/frontend/src/components/ui/button.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input hover:bg-accent hover:text-accent-foreground",
|
||||
primary:
|
||||
"border bg-background text-secondary-foreground hover:bg-background/80 hover:shadow-sm",
|
||||
secondary:
|
||||
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "underline-offset-4 hover:underline text-primary",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 py-2 px-4",
|
||||
sm: "h-9 px-3 rounded-md",
|
||||
lg: "h-11 px-8 rounded-md",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
85
src/frontend/src/components/ui/card.tsx
Normal file
85
src/frontend/src/components/ui/card.tsx
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import * as React from "react";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg flex flex-col justify-between border bg-card text-card-foreground shadow-sm hover:shadow-lg transition-all",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Card.displayName = "Card";
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardHeader.displayName = "CardHeader";
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-base font-semibold leading-tight tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardTitle.displayName = "CardTitle";
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardDescription.displayName = "CardDescription";
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-4 pt-0", className)} {...props} />
|
||||
));
|
||||
CardContent.displayName = "CardContent";
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(" flex items-center p-4 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CardFooter.displayName = "CardFooter";
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
};
|
||||
29
src/frontend/src/components/ui/checkbox.tsx
Normal file
29
src/frontend/src/components/ui/checkbox.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const Checkbox = React.forwardRef<
|
||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
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
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
125
src/frontend/src/components/ui/dialog.tsx
Normal file
125
src/frontend/src/components/ui/dialog.tsx
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = ({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal className={cn(className)} {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-primary/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
};
|
||||
199
src/frontend/src/components/ui/dropdown-menu.tsx
Normal file
199
src/frontend/src/components/ui/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 pl-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
24
src/frontend/src/components/ui/input.tsx
Normal file
24
src/frontend/src/components/ui/input.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import * as React from "react";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
25
src/frontend/src/components/ui/label.tsx
Normal file
25
src/frontend/src/components/ui/label.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const labelVariants = cva(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
);
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(labelVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
39
src/frontend/src/components/ui/loading.tsx
Normal file
39
src/frontend/src/components/ui/loading.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { SVGProps } from "react";
|
||||
|
||||
// https://github.com/feathericons/feather/issues/695#issuecomment-1503699643
|
||||
export const Loading = (props: SVGProps<SVGSVGElement>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={24}
|
||||
height={24}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="feather feather-circle"
|
||||
{...props}
|
||||
>
|
||||
<circle cx={12} cy={12} r={10} strokeDasharray={63} strokeDashoffset={21}>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 12 12"
|
||||
to="360 12 12"
|
||||
dur="2s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
<animate
|
||||
attributeName="stroke-dashoffset"
|
||||
dur="8s"
|
||||
repeatCount="indefinite"
|
||||
keyTimes="0; 0.5; 1"
|
||||
values="-16; -47; -16"
|
||||
calcMode="spline"
|
||||
keySplines="0.4 0 0.2 1; 0.4 0 0.2 1"
|
||||
/>
|
||||
</circle>
|
||||
</svg>
|
||||
);
|
||||
export default Loading;
|
||||
236
src/frontend/src/components/ui/menubar.tsx
Normal file
236
src/frontend/src/components/ui/menubar.tsx
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu;
|
||||
|
||||
const MenubarGroup = MenubarPrimitive.Group;
|
||||
|
||||
const MenubarPortal = MenubarPrimitive.Portal;
|
||||
|
||||
const MenubarSub = MenubarPrimitive.Sub;
|
||||
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
));
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in slide-in-from-top-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
)
|
||||
);
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
||||
|
||||
const MenubarItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
));
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const MenubarRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
));
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
||||
|
||||
const MenubarLabel = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
||||
|
||||
const MenubarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MenubarShortcut.displayname = "MenubarShortcut";
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarSeparator,
|
||||
MenubarLabel,
|
||||
MenubarCheckboxItem,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarPortal,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarGroup,
|
||||
MenubarSub,
|
||||
MenubarShortcut,
|
||||
};
|
||||
89
src/frontend/src/components/ui/rename-label.tsx
Normal file
89
src/frontend/src/components/ui/rename-label.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { useState, useEffect, useRef } from "react";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
export default function RenameLabel(props) {
|
||||
const [internalState, setInternalState] = useState(false);
|
||||
const [isRename, setIsRename] = props.rename
|
||||
? [props.rename, props.setRename]
|
||||
: [internalState, setInternalState];
|
||||
|
||||
useEffect(() => {
|
||||
if (props.value) setMyValue(props.value);
|
||||
}, [props.value]);
|
||||
|
||||
const [myValue, setMyValue] = useState(props.value);
|
||||
useEffect(() => {
|
||||
if (isRename) {
|
||||
setMyValue(props.value);
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
setIsRename(false);
|
||||
props.setValue("");
|
||||
}
|
||||
});
|
||||
if (inputRef.current) {
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
resizeInput();
|
||||
}, [isRename]);
|
||||
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const resizeInput = () => {
|
||||
const input = inputRef.current;
|
||||
if (input) {
|
||||
const span = document.createElement("span");
|
||||
span.style.position = "absolute";
|
||||
span.style.visibility = "hidden";
|
||||
span.style.whiteSpace = "pre";
|
||||
span.style.font = window.getComputedStyle(input).font;
|
||||
span.textContent = input.value;
|
||||
|
||||
document.body.appendChild(span);
|
||||
const textWidth = span.getBoundingClientRect().width;
|
||||
document.body.removeChild(span);
|
||||
|
||||
input.style.width = `${textWidth + 16}px`;
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{isRename ? (
|
||||
<input
|
||||
autoFocus
|
||||
ref={inputRef}
|
||||
onInput={resizeInput}
|
||||
className={cn(
|
||||
"px-2 bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md",
|
||||
props.className
|
||||
)}
|
||||
onBlur={() => {
|
||||
setIsRename(false);
|
||||
if (props.value !== "") {
|
||||
props.setValue(myValue);
|
||||
}
|
||||
}}
|
||||
value={myValue}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={cn("px-2 text-left truncate", props.className)}
|
||||
onDoubleClick={() => {
|
||||
setIsRename(true);
|
||||
setMyValue(props.value);
|
||||
}}
|
||||
>
|
||||
{props.value}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/frontend/src/components/ui/separator.tsx
Normal file
30
src/frontend/src/components/ui/separator.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
28
src/frontend/src/components/ui/switch.tsx
Normal file
28
src/frontend/src/components/ui/switch.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
113
src/frontend/src/components/ui/table.tsx
Normal file
113
src/frontend/src/components/ui/table.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import * as React from "react";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
));
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn("bg-primary font-medium text-primary-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
};
|
||||
54
src/frontend/src/components/ui/tabs.tsx
Normal file
54
src/frontend/src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm data-[state=inactive]:hover:bg-secondary/80 data-[state=active]:border data-[state=inactive]:border data-[state=inactive]:border-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||
23
src/frontend/src/components/ui/textarea.tsx
Normal file
23
src/frontend/src/components/ui/textarea.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import * as React from "react";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
export interface TextareaProps
|
||||
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
|
||||
|
||||
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[80px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export { Textarea };
|
||||
29
src/frontend/src/components/ui/tooltip.tsx
Normal file
29
src/frontend/src/components/ui/tooltip.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import { cn } from "../../utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
409
src/frontend/src/constants.tsx
Normal file
409
src/frontend/src/constants.tsx
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
// src/constants.tsx
|
||||
|
||||
import { FlowType } from "./types/flow";
|
||||
import { buildTweaks } from "./utils";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Export Dialog (Toolbar)
|
||||
* @constant
|
||||
*/
|
||||
export const EXPORT_DIALOG_SUBTITLE = "Export flow as JSON file.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Flow Settings (Menubar)
|
||||
* @constant
|
||||
*/
|
||||
export const SETTINGS_DIALOG_SUBTITLE = "Edit details about your project.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Code Dialog (Toolbar)
|
||||
* @constant
|
||||
*/
|
||||
export const CODE_DIALOG_SUBTITLE =
|
||||
"Export your flow to use it with this code.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Edit Node Dialog
|
||||
* @constant
|
||||
*/
|
||||
export const EDIT_DIALOG_SUBTITLE =
|
||||
"Adjust the configurations of your component. Define parameter visibility for the canvas view. Remember to save once you’re finished.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Code Dialog
|
||||
* @constant
|
||||
*/
|
||||
export const CODE_PROMPT_DIALOG_SUBTITLE =
|
||||
"Edit your Python code. This code snippet accepts module import and a single function definition. Make sure that your function returns a string.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Prompt Dialog
|
||||
* @constant
|
||||
*/
|
||||
export const PROMPT_DIALOG_SUBTITLE =
|
||||
"Create your prompt. Prompts can help guide the behavior of a Language Model.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Text Dialog
|
||||
* @constant
|
||||
*/
|
||||
export const TEXT_DIALOG_SUBTITLE = "Edit your text.";
|
||||
|
||||
/**
|
||||
* Function to get the python code for the API
|
||||
* @param {string} flowId - The id of the flow
|
||||
* @returns {string} - The python code
|
||||
*/
|
||||
export const getPythonApiCode = (flow: FlowType): string => {
|
||||
const flowId = flow.id;
|
||||
|
||||
// create a dictionary of node ids and the values is an empty dictionary
|
||||
// flow.data.nodes.forEach((node) => {
|
||||
// node.data.id
|
||||
// }
|
||||
const tweaks = buildTweaks(flow);
|
||||
return `import requests
|
||||
|
||||
BASE_API_URL = "${window.location.protocol}//${
|
||||
window.location.host
|
||||
}/api/v1/predict"
|
||||
FLOW_ID = "${flowId}"
|
||||
# You can tweak the flow by adding a tweaks dictionary
|
||||
# e.g {"OpenAI-XXXXX": {"model_name": "gpt-4"}}
|
||||
TWEAKS = ${JSON.stringify(tweaks, null, 2)}
|
||||
|
||||
def run_flow(message: str, flow_id: str, tweaks: dict = 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 = {"message": message}
|
||||
|
||||
if tweaks:
|
||||
payload["tweaks"] = tweaks
|
||||
|
||||
response = requests.post(api_url, json=payload)
|
||||
return response.json()
|
||||
|
||||
# Setup any tweaks you want to apply to the flow
|
||||
|
||||
print(run_flow("Your message", flow_id=FLOW_ID, tweaks=TWEAKS))`;
|
||||
};
|
||||
/**
|
||||
* Function to get the curl code for the API
|
||||
* @param {string} flowId - The id of the flow
|
||||
* @returns {string} - The curl code
|
||||
*/
|
||||
export const getCurlCode = (flow: FlowType): string => {
|
||||
const flowId = flow.id;
|
||||
const tweaks = buildTweaks(flow);
|
||||
return `curl -X POST \\
|
||||
${window.location.protocol}//${
|
||||
window.location.host
|
||||
}/api/v1/predict/${flowId} \\
|
||||
-H 'Content-Type: application/json' \\
|
||||
-d '{"message": "Your message", "tweaks": ${JSON.stringify(
|
||||
tweaks,
|
||||
null,
|
||||
2
|
||||
)}}'`;
|
||||
};
|
||||
/**
|
||||
* Function to get the python code for the API
|
||||
* @param {string} flowName - The name of the flow
|
||||
* @returns {string} - The python code
|
||||
*/
|
||||
export const getPythonCode = (flow: FlowType): string => {
|
||||
const flowName = flow.name;
|
||||
const tweaks = buildTweaks(flow);
|
||||
return `from langflow import load_flow_from_json
|
||||
TWEAKS = ${JSON.stringify(tweaks, null, 2)}
|
||||
flow = load_flow_from_json("${flowName}.json", tweaks=TWEAKS)
|
||||
# Now you can use it like any chain
|
||||
flow("Hey, have you heard of LangFlow?")`;
|
||||
};
|
||||
|
||||
/**
|
||||
* The base text for subtitle of Import Dialog
|
||||
* @constant
|
||||
*/
|
||||
export const IMPORT_DIALOG_SUBTITLE =
|
||||
"Upload a JSON file or select from the available community examples.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of code dialog
|
||||
* @constant
|
||||
*/
|
||||
export const EXPORT_CODE_DIALOG =
|
||||
"Generate the code to integrate your flow into an external application.";
|
||||
|
||||
/**
|
||||
* The base text for subtitle of code dialog
|
||||
* @constant
|
||||
*/
|
||||
export const INPUT_STYLE =
|
||||
" focus:ring-1 focus:ring-offset-1 focus:ring-ring focus:outline-none ";
|
||||
|
||||
/**
|
||||
* Default description for the flow
|
||||
* @constant
|
||||
*/
|
||||
export const DESCRIPTIONS: string[] = [
|
||||
"Chain the Words, Master Language!",
|
||||
"Language Architect at Work!",
|
||||
"Empowering Language Engineering.",
|
||||
"Craft Language Connections Here.",
|
||||
"Create, Connect, Converse.",
|
||||
"Smart Chains, Smarter Conversations.",
|
||||
"Bridging Prompts for Brilliance.",
|
||||
"Language Models, Unleashed.",
|
||||
"Your Hub for Text Generation.",
|
||||
"Promptly Ingenious!",
|
||||
"Building Linguistic Labyrinths.",
|
||||
"LangFlow: Create, Chain, Communicate.",
|
||||
"Connect the Dots, Craft Language.",
|
||||
"Interactive Language Weaving.",
|
||||
"Generate, Innovate, Communicate.",
|
||||
"Conversation Catalyst Engine.",
|
||||
"Language Chainlink Master.",
|
||||
"Design Dialogues with LangFlow.",
|
||||
"Nurture NLP Nodes Here.",
|
||||
"Conversational Cartography Unlocked.",
|
||||
"Design, Develop, Dialogize.",
|
||||
];
|
||||
|
||||
/**
|
||||
* Adjectives for the name of the flow
|
||||
* @constant
|
||||
*
|
||||
*/
|
||||
export const ADJECTIVES: string[] = [
|
||||
"admiring",
|
||||
"adoring",
|
||||
"agitated",
|
||||
"amazing",
|
||||
"angry",
|
||||
"awesome",
|
||||
"backstabbing",
|
||||
"berserk",
|
||||
"big",
|
||||
"boring",
|
||||
"clever",
|
||||
"cocky",
|
||||
"compassionate",
|
||||
"condescending",
|
||||
"cranky",
|
||||
"desperate",
|
||||
"determined",
|
||||
"distracted",
|
||||
"dreamy",
|
||||
"drunk",
|
||||
"ecstatic",
|
||||
"elated",
|
||||
"elegant",
|
||||
"evil",
|
||||
"fervent",
|
||||
"focused",
|
||||
"furious",
|
||||
"gigantic",
|
||||
"gloomy",
|
||||
"goofy",
|
||||
"grave",
|
||||
"happy",
|
||||
"high",
|
||||
"hopeful",
|
||||
"hungry",
|
||||
"insane",
|
||||
"jolly",
|
||||
"jovial",
|
||||
"kickass",
|
||||
"lonely",
|
||||
"loving",
|
||||
"mad",
|
||||
"modest",
|
||||
"naughty",
|
||||
"nauseous",
|
||||
"nostalgic",
|
||||
"pedantic",
|
||||
"pensive",
|
||||
"prickly",
|
||||
"reverent",
|
||||
"romantic",
|
||||
"sad",
|
||||
"serene",
|
||||
"sharp",
|
||||
"sick",
|
||||
"silly",
|
||||
"sleepy",
|
||||
"small",
|
||||
"stoic",
|
||||
"stupefied",
|
||||
"suspicious",
|
||||
"tender",
|
||||
"thirsty",
|
||||
"tiny",
|
||||
"trusting",
|
||||
];
|
||||
|
||||
/**
|
||||
* Nouns for the name of the flow
|
||||
* @constant
|
||||
*
|
||||
*/
|
||||
export const NOUNS: string[] = [
|
||||
"albattani",
|
||||
"allen",
|
||||
"almeida",
|
||||
"archimedes",
|
||||
"ardinghelli",
|
||||
"aryabhata",
|
||||
"austin",
|
||||
"babbage",
|
||||
"banach",
|
||||
"bardeen",
|
||||
"bartik",
|
||||
"bassi",
|
||||
"bell",
|
||||
"bhabha",
|
||||
"bhaskara",
|
||||
"blackwell",
|
||||
"bohr",
|
||||
"booth",
|
||||
"borg",
|
||||
"bose",
|
||||
"boyd",
|
||||
"brahmagupta",
|
||||
"brattain",
|
||||
"brown",
|
||||
"carson",
|
||||
"chandrasekhar",
|
||||
"colden",
|
||||
"cori",
|
||||
"cray",
|
||||
"curie",
|
||||
"darwin",
|
||||
"davinci",
|
||||
"dijkstra",
|
||||
"dubinsky",
|
||||
"easley",
|
||||
"einstein",
|
||||
"elion",
|
||||
"engelbart",
|
||||
"euclid",
|
||||
"euler",
|
||||
"fermat",
|
||||
"fermi",
|
||||
"feynman",
|
||||
"franklin",
|
||||
"galileo",
|
||||
"gates",
|
||||
"goldberg",
|
||||
"goldstine",
|
||||
"goldwasser",
|
||||
"golick",
|
||||
"goodall",
|
||||
"hamilton",
|
||||
"hawking",
|
||||
"heisenberg",
|
||||
"heyrovsky",
|
||||
"hodgkin",
|
||||
"hoover",
|
||||
"hopper",
|
||||
"hugle",
|
||||
"hypatia",
|
||||
"jang",
|
||||
"jennings",
|
||||
"jepsen",
|
||||
"joliot",
|
||||
"jones",
|
||||
"kalam",
|
||||
"kare",
|
||||
"keller",
|
||||
"khorana",
|
||||
"kilby",
|
||||
"kirch",
|
||||
"knuth",
|
||||
"kowalevski",
|
||||
"lalande",
|
||||
"lamarr",
|
||||
"leakey",
|
||||
"leavitt",
|
||||
"lichterman",
|
||||
"liskov",
|
||||
"lovelace",
|
||||
"lumiere",
|
||||
"mahavira",
|
||||
"mayer",
|
||||
"mccarthy",
|
||||
"mcclintock",
|
||||
"mclean",
|
||||
"mcnulty",
|
||||
"meitner",
|
||||
"meninsky",
|
||||
"mestorf",
|
||||
"minsky",
|
||||
"mirzakhani",
|
||||
"morse",
|
||||
"murdock",
|
||||
"newton",
|
||||
"nobel",
|
||||
"noether",
|
||||
"northcutt",
|
||||
"noyce",
|
||||
"panini",
|
||||
"pare",
|
||||
"pasteur",
|
||||
"payne",
|
||||
"perlman",
|
||||
"pike",
|
||||
"poincare",
|
||||
"poitras",
|
||||
"ptolemy",
|
||||
"raman",
|
||||
"ramanujan",
|
||||
"ride",
|
||||
"ritchie",
|
||||
"roentgen",
|
||||
"rosalind",
|
||||
"saha",
|
||||
"sammet",
|
||||
"shaw",
|
||||
"shirley",
|
||||
"shockley",
|
||||
"sinoussi",
|
||||
"snyder",
|
||||
"spence",
|
||||
"stallman",
|
||||
"stonebraker",
|
||||
"swanson",
|
||||
"swartz",
|
||||
"swirles",
|
||||
"tesla",
|
||||
"thompson",
|
||||
"torvalds",
|
||||
"turing",
|
||||
"varahamihira",
|
||||
"visvesvaraya",
|
||||
"volhard",
|
||||
"wescoff",
|
||||
"williams",
|
||||
"wilson",
|
||||
"wing",
|
||||
"wozniak",
|
||||
"wright",
|
||||
"yalow",
|
||||
"yonath",
|
||||
];
|
||||
|
||||
/**
|
||||
* Header text for user projects
|
||||
* @constant
|
||||
*
|
||||
*/
|
||||
export const USER_PROJECTS_HEADER = "My Collection";
|
||||
40
src/frontend/src/contexts/SSEContext.tsx
Normal file
40
src/frontend/src/contexts/SSEContext.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
} from "react";
|
||||
|
||||
const initialValue = {
|
||||
updateSSEData: ({}) => {},
|
||||
sseData: {},
|
||||
isBuilding: false,
|
||||
setIsBuilding: (isBuilding: boolean) => {},
|
||||
};
|
||||
|
||||
const SSEContext = createContext(initialValue);
|
||||
|
||||
export function useSSE() {
|
||||
return useContext(SSEContext);
|
||||
}
|
||||
|
||||
export function SSEProvider({ children }) {
|
||||
const [sseData, setSSEData] = useState({});
|
||||
const [isBuilding, setIsBuilding] = useState(false);
|
||||
|
||||
const updateSSEData = useCallback((newData: any) => {
|
||||
setSSEData((prevData) => ({
|
||||
...prevData,
|
||||
...newData,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SSEContext.Provider
|
||||
value={{ sseData, updateSSEData, isBuilding, setIsBuilding }}
|
||||
>
|
||||
{children}
|
||||
</SSEContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -80,7 +80,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
function setErrorData(newState: { title: string; list?: Array<string> }) {
|
||||
setErrorDataState(newState);
|
||||
setErrorOpen(true);
|
||||
if (newState.title) {
|
||||
if (newState.title && newState.title !== "") {
|
||||
setNotificationCenter(true);
|
||||
pushNotificationList({
|
||||
type: "error",
|
||||
|
|
@ -97,7 +97,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
function setNoticeData(newState: { title: string; link?: string }) {
|
||||
setNoticeDataState(newState);
|
||||
setNoticeOpen(true);
|
||||
if (newState.title) {
|
||||
if (newState.title && newState.title !== "") {
|
||||
// Add new notice to notification center
|
||||
setNotificationCenter(true);
|
||||
pushNotificationList({
|
||||
|
|
@ -117,7 +117,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
|
|||
setSuccessOpen(true); // open the success alert
|
||||
|
||||
// If the new state has a "title" property, add a new success notification to the list
|
||||
if (newState.title) {
|
||||
if (newState.title && newState.title !== "") {
|
||||
setNotificationCenter(true); // show the notification center
|
||||
pushNotificationList({
|
||||
// add the new notification to the list
|
||||
|
|
|
|||
|
|
@ -13,13 +13,16 @@ const initialValue = {
|
|||
export const darkContext = createContext<darkContextType>(initialValue);
|
||||
|
||||
export function DarkProvider({ children }) {
|
||||
const [dark, setDark] = useState(false);
|
||||
const [dark, setDark] = useState(
|
||||
JSON.parse(window.localStorage.getItem("isDark")) ?? false
|
||||
);
|
||||
useEffect(() => {
|
||||
if (dark) {
|
||||
document.getElementById("body").classList.add("dark");
|
||||
} else {
|
||||
document.getElementById("body").classList.remove("dark");
|
||||
}
|
||||
window.localStorage.setItem("isDark", dark.toString());
|
||||
}, [dark]);
|
||||
return (
|
||||
<darkContext.Provider
|
||||
|
|
|
|||
|
|
@ -5,22 +5,31 @@ import { LocationProvider } from "./locationContext";
|
|||
import PopUpProvider from "./popUpContext";
|
||||
import { TabsProvider } from "./tabsContext";
|
||||
import { TypesProvider } from "./typesContext";
|
||||
import { ReactFlowProvider } from "reactflow";
|
||||
import { UndoRedoProvider } from "./undoRedoContext";
|
||||
import { SSEProvider } from "./SSEContext";
|
||||
|
||||
export default function ContextWrapper({ children }: { children: ReactNode }) {
|
||||
//element to wrap all context
|
||||
return (
|
||||
<>
|
||||
<DarkProvider>
|
||||
<TypesProvider>
|
||||
<LocationProvider>
|
||||
<AlertProvider>
|
||||
<TabsProvider>
|
||||
<PopUpProvider>{children}</PopUpProvider>
|
||||
</TabsProvider>
|
||||
</AlertProvider>
|
||||
</LocationProvider>
|
||||
</TypesProvider>
|
||||
</DarkProvider>
|
||||
<ReactFlowProvider>
|
||||
<DarkProvider>
|
||||
<TypesProvider>
|
||||
<LocationProvider>
|
||||
<AlertProvider>
|
||||
<SSEProvider>
|
||||
<TabsProvider>
|
||||
<UndoRedoProvider>
|
||||
<PopUpProvider>{children}</PopUpProvider>
|
||||
</UndoRedoProvider>
|
||||
</TabsProvider>
|
||||
</SSEProvider>
|
||||
</AlertProvider>
|
||||
</LocationProvider>
|
||||
</TypesProvider>
|
||||
</DarkProvider>
|
||||
</ReactFlowProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,38 +7,55 @@ import {
|
|||
useContext,
|
||||
} from "react";
|
||||
import { FlowType, NodeType } from "../types/flow";
|
||||
import { LangFlowState, TabsContextType } from "../types/tabs";
|
||||
import { TabsContextType, TabsState } from "../types/tabs";
|
||||
import {
|
||||
normalCaseToSnakeCase,
|
||||
updateIds,
|
||||
updateObject,
|
||||
updateTemplate,
|
||||
getRandomDescription,
|
||||
getRandomName,
|
||||
} from "../utils";
|
||||
import { alertContext } from "./alertContext";
|
||||
import { typesContext } from "./typesContext";
|
||||
import { APITemplateType, TemplateVariableType } from "../types/api";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { APITemplateType } from "../types/api";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import { addEdge } from "reactflow";
|
||||
import {
|
||||
readFlowsFromDatabase,
|
||||
deleteFlowFromDatabase,
|
||||
saveFlowToDatabase,
|
||||
downloadFlowsFromDatabase,
|
||||
uploadFlowsToDatabase,
|
||||
updateFlowInDatabase,
|
||||
} from "../controllers/API";
|
||||
import _ from "lodash";
|
||||
|
||||
const uid = new ShortUniqueId({ length: 5 });
|
||||
|
||||
const TabsContextInitialValue: TabsContextType = {
|
||||
save: () => {},
|
||||
tabIndex: 0,
|
||||
setTabIndex: (index: number) => {},
|
||||
tabId: "",
|
||||
setTabId: (index: string) => {},
|
||||
flows: [],
|
||||
removeFlow: (id: string) => {},
|
||||
addFlow: (flowData?: any) => {},
|
||||
addFlow: async (flowData?: any) => "",
|
||||
updateFlow: (newFlow: FlowType) => {},
|
||||
incrementNodeId: () => uuidv4(),
|
||||
incrementNodeId: () => uid(),
|
||||
downloadFlow: (flow: FlowType) => {},
|
||||
downloadFlows: () => {},
|
||||
uploadFlows: () => {},
|
||||
uploadFlow: () => {},
|
||||
hardReset: () => {},
|
||||
saveFlow: async (flow: FlowType) => {},
|
||||
disableCopyPaste: false,
|
||||
setDisableCopyPaste: (state: boolean) => {},
|
||||
getNodeId: () => "",
|
||||
lastCopiedSelection: null,
|
||||
setLastCopiedSelection: (selection: any) => {},
|
||||
tabsState: {},
|
||||
setTabsState: (state: TabsState) => {},
|
||||
getNodeId: (nodeType: string) => "",
|
||||
paste: (
|
||||
selection: { nodes: any; edges: any },
|
||||
position: { x: number; y: number }
|
||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||
) => {},
|
||||
};
|
||||
|
||||
|
|
@ -47,17 +64,22 @@ export const TabsContext = createContext<TabsContextType>(
|
|||
);
|
||||
|
||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
const { setNoticeData } = useContext(alertContext);
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [flows, setFlows] = useState<Array<FlowType>>([]);
|
||||
const [id, setId] = useState(uuidv4());
|
||||
const { templates, reactFlowInstance } = useContext(typesContext);
|
||||
const { setErrorData, setNoticeData } = useContext(alertContext);
|
||||
|
||||
const newNodeId = useRef(uuidv4());
|
||||
const [tabId, setTabId] = useState("");
|
||||
|
||||
const [flows, setFlows] = useState<Array<FlowType>>([]);
|
||||
const [id, setId] = useState(uid());
|
||||
const { templates, reactFlowInstance } = useContext(typesContext);
|
||||
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
|
||||
const [tabsState, setTabsState] = useState<TabsState>({});
|
||||
|
||||
const newNodeId = useRef(uid());
|
||||
function incrementNodeId() {
|
||||
newNodeId.current = uuidv4();
|
||||
newNodeId.current = uid();
|
||||
return newNodeId.current;
|
||||
}
|
||||
|
||||
function save() {
|
||||
// added clone deep to avoid mutating the original object
|
||||
let Saveflows = _.cloneDeep(flows);
|
||||
|
|
@ -79,63 +101,156 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
});
|
||||
window.localStorage.setItem(
|
||||
"tabsData",
|
||||
JSON.stringify({ tabIndex, flows: Saveflows, id })
|
||||
JSON.stringify({ tabId, flows: Saveflows, id })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
//get tabs locally saved
|
||||
let cookie = window.localStorage.getItem("tabsData");
|
||||
if (cookie && Object.keys(templates).length > 0) {
|
||||
let cookieObject: LangFlowState = JSON.parse(cookie);
|
||||
cookieObject.flows.forEach((flow) => {
|
||||
flow.data.edges.forEach((edge) => {
|
||||
edge.className = "";
|
||||
edge.style = { stroke: "#555555" };
|
||||
});
|
||||
flow.data.nodes.forEach((node) => {
|
||||
if (Object.keys(templates[node.data.type]["template"]).length > 0) {
|
||||
node.data.node.base_classes =
|
||||
templates[node.data.type]["base_classes"];
|
||||
flow.data.edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(templates[node.data.type]["base_classes"])
|
||||
.join("|");
|
||||
}
|
||||
});
|
||||
node.data.node.description =
|
||||
templates[node.data.type]["description"];
|
||||
node.data.node.template = updateTemplate(
|
||||
templates[node.data.type][
|
||||
"template"
|
||||
] as unknown as APITemplateType,
|
||||
// function loadCookie(cookie: string) {
|
||||
// if (cookie && Object.keys(templates).length > 0) {
|
||||
// let cookieObject: LangFlowState = JSON.parse(cookie);
|
||||
// try {
|
||||
// cookieObject.flows.forEach((flow) => {
|
||||
// if (!flow.data) {
|
||||
// return;
|
||||
// }
|
||||
// flow.data.edges.forEach((edge) => {
|
||||
// edge.className = "";
|
||||
// edge.style = { stroke: "#555555" };
|
||||
// });
|
||||
|
||||
node.data.node.template as APITemplateType
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
setTabIndex(cookieObject.tabIndex);
|
||||
setFlows(cookieObject.flows);
|
||||
setId(cookieObject.id);
|
||||
}
|
||||
// flow.data.nodes.forEach((node) => {
|
||||
// const template = templates[node.data.type];
|
||||
// if (!template) {
|
||||
// setErrorData({ title: `Unknown node type: ${node.data.type}` });
|
||||
// return;
|
||||
// }
|
||||
// if (Object.keys(template["template"]).length > 0) {
|
||||
// node.data.node.base_classes = template["base_classes"];
|
||||
// flow.data.edges.forEach((edge) => {
|
||||
// if (edge.source === node.id) {
|
||||
// edge.sourceHandle = edge.sourceHandle
|
||||
// .split("|")
|
||||
// .slice(0, 2)
|
||||
// .concat(template["base_classes"])
|
||||
// .join("|");
|
||||
// }
|
||||
// });
|
||||
// node.data.node.description = template["description"];
|
||||
// node.data.node.template = updateTemplate(
|
||||
// template["template"] as unknown as APITemplateType,
|
||||
// node.data.node.template as APITemplateType
|
||||
// );
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// setTabIndex(cookieObject.tabIndex);
|
||||
// setFlows(cookieObject.flows);
|
||||
// setId(cookieObject.id);
|
||||
// } catch (e) {
|
||||
// console.log(e);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
function refreshFlows() {
|
||||
getTabsDataFromDB().then((DbData) => {
|
||||
if (DbData && Object.keys(templates).length > 0) {
|
||||
try {
|
||||
processDBData(DbData);
|
||||
updateStateWithDbData(DbData);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// get data from db
|
||||
//get tabs locally saved
|
||||
// let tabsData = getLocalStorageTabsData();
|
||||
refreshFlows();
|
||||
}, [templates]);
|
||||
|
||||
useEffect(() => {
|
||||
//save tabs locally
|
||||
console.log(id);
|
||||
save();
|
||||
}, [flows, id, tabIndex, newNodeId]);
|
||||
function getTabsDataFromDB() {
|
||||
//get tabs from db
|
||||
return readFlowsFromDatabase();
|
||||
}
|
||||
function processDBData(DbData) {
|
||||
DbData.forEach((flow) => {
|
||||
try {
|
||||
if (!flow.data) {
|
||||
return;
|
||||
}
|
||||
processFlowEdges(flow);
|
||||
processFlowNodes(flow);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function processFlowEdges(flow) {
|
||||
flow.data.edges.forEach((edge) => {
|
||||
edge.className = "";
|
||||
edge.style = { stroke: "#555555" };
|
||||
});
|
||||
}
|
||||
|
||||
function processFlowNodes(flow) {
|
||||
flow.data.nodes.forEach((node) => {
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
setErrorData({ title: `Unknown node type: ${node.data.type}` });
|
||||
return;
|
||||
}
|
||||
if (Object.keys(template["template"]).length > 0) {
|
||||
updateNodeBaseClasses(node, template);
|
||||
updateNodeEdges(flow, node, template);
|
||||
updateNodeDescription(node, template);
|
||||
updateNodeTemplate(node, template);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateNodeBaseClasses(node, template) {
|
||||
node.data.node.base_classes = template["base_classes"];
|
||||
}
|
||||
|
||||
function updateNodeEdges(flow, node, template) {
|
||||
flow.data.edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateNodeDescription(node, template) {
|
||||
node.data.node.description = template["description"];
|
||||
}
|
||||
|
||||
function updateNodeTemplate(node, template) {
|
||||
node.data.node.template = updateTemplate(
|
||||
template["template"] as unknown as APITemplateType,
|
||||
node.data.node.template as APITemplateType
|
||||
);
|
||||
}
|
||||
|
||||
function updateStateWithDbData(tabsData) {
|
||||
setFlows(tabsData);
|
||||
}
|
||||
|
||||
function hardReset() {
|
||||
newNodeId.current = uuidv4();
|
||||
setTabIndex(0);
|
||||
newNodeId.current = uid();
|
||||
setTabId("");
|
||||
|
||||
setFlows([]);
|
||||
setId(uuidv4());
|
||||
setId(uid());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -150,17 +265,33 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
// create a link element and set its properties
|
||||
const link = document.createElement("a");
|
||||
link.href = jsonString;
|
||||
link.download = `${flows[tabIndex].name}.json`;
|
||||
link.download = `${flows.find((f) => f.id === tabId).name}.json`;
|
||||
|
||||
// simulate a click on the link element to trigger the download
|
||||
link.click();
|
||||
setNoticeData({
|
||||
title: "Warning: Critical data,JSON file may including API keys.",
|
||||
title: "Warning: Critical data, JSON file may include API keys.",
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeId() {
|
||||
return `dndnode_` + incrementNodeId();
|
||||
function downloadFlows() {
|
||||
downloadFlowsFromDatabase().then((flows) => {
|
||||
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(flows)
|
||||
)}`;
|
||||
|
||||
// create a link element and set its properties
|
||||
const link = document.createElement("a");
|
||||
link.href = jsonString;
|
||||
link.download = `flows.json`;
|
||||
|
||||
// simulate a click on the link element to trigger the download
|
||||
link.click();
|
||||
});
|
||||
}
|
||||
|
||||
function getNodeId(nodeType: string) {
|
||||
return nodeType + "-" + incrementNodeId();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -168,10 +299,11 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
* If the file type is application/json, the file is read and parsed into a JSON object.
|
||||
* The resulting JSON object is passed to the addFlow function.
|
||||
*/
|
||||
function uploadFlow() {
|
||||
function uploadFlow(newProject?: boolean) {
|
||||
// create a file input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = ".json";
|
||||
// add a change event listener to the file input
|
||||
input.onchange = (e: Event) => {
|
||||
// check if the file type is application/json
|
||||
|
|
@ -183,7 +315,29 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
// parse the text into a JSON object
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
|
||||
addFlow(flow);
|
||||
addFlow(flow, newProject);
|
||||
});
|
||||
}
|
||||
};
|
||||
// trigger the file input click event to open the file dialog
|
||||
input.click();
|
||||
}
|
||||
|
||||
function uploadFlows() {
|
||||
// create a file input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
// add a change event listener to the file input
|
||||
input.onchange = (e: Event) => {
|
||||
// check if the file type is application/json
|
||||
if ((e.target as HTMLInputElement).files[0].type === "application/json") {
|
||||
// get the file from the file input
|
||||
const file = (e.target as HTMLInputElement).files[0];
|
||||
// read the file as text
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
uploadFlowsToDatabase(formData).then(() => {
|
||||
refreshFlows();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -196,28 +350,22 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
* @param {string} id - The id of the flow to remove.
|
||||
*/
|
||||
function removeFlow(id: string) {
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState];
|
||||
const index = newFlows.findIndex((flow) => flow.id === id);
|
||||
if (index >= 0) {
|
||||
if (index === tabIndex) {
|
||||
setTabIndex(flows.length - 2);
|
||||
newFlows.splice(index, 1);
|
||||
} else {
|
||||
let flowId = flows[tabIndex].id;
|
||||
newFlows.splice(index, 1);
|
||||
setTabIndex(newFlows.findIndex((flow) => flow.id === flowId));
|
||||
}
|
||||
}
|
||||
return newFlows;
|
||||
});
|
||||
const index = flows.findIndex((flow) => flow.id === id);
|
||||
if (index >= 0) {
|
||||
deleteFlowFromDatabase(id).then(() => {
|
||||
setFlows(flows.filter((flow) => flow.id !== id));
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a new flow to the list of flows.
|
||||
* @param flow Optional flow to add.
|
||||
*/
|
||||
|
||||
function paste(selectionInstance, position) {
|
||||
function paste(
|
||||
selectionInstance,
|
||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||
) {
|
||||
let minimumX = Infinity;
|
||||
let minimumY = Infinity;
|
||||
let idsMap = {};
|
||||
|
|
@ -232,11 +380,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
}
|
||||
});
|
||||
|
||||
const insidePosition = reactFlowInstance.project(position);
|
||||
const insidePosition = position.paneX
|
||||
? { x: position.paneX + position.x, y: position.paneY + position.y }
|
||||
: reactFlowInstance.project({ x: position.x, y: position.y });
|
||||
|
||||
selectionInstance.nodes.forEach((n) => {
|
||||
selectionInstance.nodes.forEach((n: NodeType) => {
|
||||
// Generate a unique node ID
|
||||
let newId = getNodeId();
|
||||
let newId = getNodeId(n.data.type);
|
||||
idsMap[n.id] = newId;
|
||||
|
||||
// Create a new node object
|
||||
|
|
@ -301,63 +451,106 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
reactFlowInstance.setEdges(edges);
|
||||
}
|
||||
|
||||
function addFlow(flow?: FlowType) {
|
||||
// Get data from the flow or set it to null if there's no flow provided.
|
||||
const addFlow = async (
|
||||
flow?: FlowType,
|
||||
newProject?: Boolean
|
||||
): Promise<String> => {
|
||||
if (newProject) {
|
||||
let flowData = extractDataFromFlow(flow);
|
||||
if (flowData.description == "") {
|
||||
flowData.description = getRandomDescription();
|
||||
}
|
||||
|
||||
// Create a new flow with a default name if no flow is provided.
|
||||
const newFlow = createNewFlow(flowData, flow);
|
||||
|
||||
try {
|
||||
const { id } = await saveFlowToDatabase(newFlow);
|
||||
// Change the id to the new id.
|
||||
newFlow.id = id;
|
||||
|
||||
// Add the new flow to the list of flows.
|
||||
addFlowToLocalState(newFlow);
|
||||
|
||||
// Return the id
|
||||
return id;
|
||||
} catch (error) {
|
||||
// Handle the error if needed
|
||||
console.error("Error while adding flow:", error);
|
||||
throw error; // Re-throw the error so the caller can handle it if needed
|
||||
}
|
||||
} else {
|
||||
paste(
|
||||
{ nodes: flow.data.nodes, edges: flow.data.edges },
|
||||
{ x: 10, y: 10 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const extractDataFromFlow = (flow) => {
|
||||
let data = flow?.data ? flow.data : null;
|
||||
const description = flow?.description ? flow.description : "";
|
||||
|
||||
if (data) {
|
||||
data.edges.forEach((edge) => {
|
||||
edge.style = { stroke: "inherit" };
|
||||
edge.className =
|
||||
edge.targetHandle.split("|")[0] === "Text"
|
||||
? "stroke-gray-800 dark:stroke-gray-300"
|
||||
: "stroke-gray-900 dark:stroke-gray-200";
|
||||
edge.animated = edge.targetHandle.split("|")[0] === "Text";
|
||||
});
|
||||
data.nodes.forEach((node) => {
|
||||
if (Object.keys(templates[node.data.type]["template"]).length > 0) {
|
||||
node.data.node.base_classes =
|
||||
templates[node.data.type]["base_classes"];
|
||||
flow.data.edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(templates[node.data.type]["base_classes"])
|
||||
.join("|");
|
||||
}
|
||||
});
|
||||
node.data.node.description = templates[node.data.type]["description"];
|
||||
node.data.node.template = updateTemplate(
|
||||
templates[node.data.type]["template"] as unknown as APITemplateType,
|
||||
node.data.node.template as APITemplateType
|
||||
);
|
||||
}
|
||||
});
|
||||
updateIds(data, getNodeId);
|
||||
updateEdges(data.edges);
|
||||
updateNodes(data.nodes, data.edges);
|
||||
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
|
||||
}
|
||||
// Create a new flow with a default name if no flow is provided.
|
||||
let newFlow: FlowType = {
|
||||
description,
|
||||
name: flow?.name ?? "New Flow",
|
||||
id: uuidv4(),
|
||||
data,
|
||||
};
|
||||
|
||||
// Increment the ID counter.
|
||||
setId(uuidv4());
|
||||
return { data, description };
|
||||
};
|
||||
|
||||
// Add the new flow to the list of flows.
|
||||
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState, newFlow];
|
||||
return newFlows;
|
||||
const updateEdges = (edges) => {
|
||||
edges.forEach((edge) => {
|
||||
edge.style = { stroke: "inherit" };
|
||||
edge.className =
|
||||
edge.targetHandle.split("|")[0] === "Text"
|
||||
? "stroke-gray-800 dark:stroke-gray-300"
|
||||
: "stroke-gray-900 dark:stroke-gray-200";
|
||||
edge.animated = edge.targetHandle.split("|")[0] === "Text";
|
||||
});
|
||||
};
|
||||
|
||||
const updateNodes = (nodes, edges) => {
|
||||
nodes.forEach((node) => {
|
||||
const template = templates[node.data.type];
|
||||
if (!template) {
|
||||
setErrorData({ title: `Unknown node type: ${node.data.type}` });
|
||||
return;
|
||||
}
|
||||
if (Object.keys(template["template"]).length > 0) {
|
||||
node.data.node.base_classes = template["base_classes"];
|
||||
edges.forEach((edge) => {
|
||||
if (edge.source === node.id) {
|
||||
edge.sourceHandle = edge.sourceHandle
|
||||
.split("|")
|
||||
.slice(0, 2)
|
||||
.concat(template["base_classes"])
|
||||
.join("|");
|
||||
}
|
||||
});
|
||||
node.data.node.description = template["description"];
|
||||
node.data.node.template = updateTemplate(
|
||||
template["template"] as unknown as APITemplateType,
|
||||
node.data.node.template as APITemplateType
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const createNewFlow = (flowData, flow) => ({
|
||||
description: flowData.description,
|
||||
name: flow?.name ?? getRandomName(),
|
||||
data: flowData.data,
|
||||
id: "",
|
||||
});
|
||||
|
||||
const addFlowToLocalState = (newFlow) => {
|
||||
setFlows((prevState) => {
|
||||
return [...prevState, newFlow];
|
||||
});
|
||||
};
|
||||
|
||||
// Set the tab index to the new flow.
|
||||
setTabIndex(flows.length);
|
||||
}
|
||||
/**
|
||||
* Updates an existing flow with new data
|
||||
* @param newFlow - The new flow object containing the updated data
|
||||
|
|
@ -374,25 +567,64 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
return newFlows;
|
||||
});
|
||||
}
|
||||
|
||||
async function saveFlow(newFlow: FlowType) {
|
||||
try {
|
||||
// updates flow in db
|
||||
const updatedFlow = await updateFlowInDatabase(newFlow);
|
||||
if (updatedFlow) {
|
||||
// updates flow in state
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState];
|
||||
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
|
||||
if (index !== -1) {
|
||||
newFlows[index].description = newFlow.description ?? "";
|
||||
newFlows[index].data = newFlow.data;
|
||||
newFlows[index].name = newFlow.name;
|
||||
}
|
||||
return newFlows;
|
||||
});
|
||||
//update tabs state
|
||||
setTabsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
isPending: false,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setErrorData(err);
|
||||
}
|
||||
}
|
||||
|
||||
const [disableCopyPaste, setDisableCopyPaste] = useState(false);
|
||||
|
||||
return (
|
||||
<TabsContext.Provider
|
||||
value={{
|
||||
saveFlow,
|
||||
lastCopiedSelection,
|
||||
setLastCopiedSelection,
|
||||
disableCopyPaste,
|
||||
setDisableCopyPaste,
|
||||
save,
|
||||
hardReset,
|
||||
tabIndex,
|
||||
setTabIndex,
|
||||
tabId,
|
||||
setTabId,
|
||||
flows,
|
||||
save,
|
||||
incrementNodeId,
|
||||
removeFlow,
|
||||
addFlow,
|
||||
updateFlow,
|
||||
downloadFlow,
|
||||
downloadFlows,
|
||||
uploadFlows,
|
||||
uploadFlow,
|
||||
getNodeId,
|
||||
tabsState,
|
||||
setTabsState,
|
||||
paste,
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -51,16 +51,21 @@ export function TypesProvider({ children }: { children: ReactNode }) {
|
|||
);
|
||||
// Set the types by reducing over the keys of the result data and updating the accumulator.
|
||||
setTypes(
|
||||
Object.keys(result.data).reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
|
||||
acc[c] = curr;
|
||||
// Add the base classes to the accumulator as well.
|
||||
result.data[curr][c].base_classes?.forEach((b) => {
|
||||
acc[b] = curr;
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
}, {})
|
||||
// Reverse the keys so the tool world does not overlap
|
||||
Object.keys(result.data)
|
||||
.reverse()
|
||||
.reduce((acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach(
|
||||
(c: keyof APIKindType) => {
|
||||
acc[c] = curr;
|
||||
// Add the base classes to the accumulator as well.
|
||||
result.data[curr][c].base_classes?.forEach((b) => {
|
||||
acc[b] = curr;
|
||||
});
|
||||
}
|
||||
);
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
}
|
||||
// Clear the interval if successful.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
import { useCallback, useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Edge, Node, useReactFlow } from "reactflow";
|
||||
import { TabsContext } from "../../../contexts/tabsContext";
|
||||
import { cloneDeep } from "lodash";
|
||||
import { TabsContext } from "./tabsContext";
|
||||
|
||||
type undoRedoContextType = {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
takeSnapshot: () => void;
|
||||
};
|
||||
|
||||
type UseUndoRedoOptions = {
|
||||
maxHistorySize: number;
|
||||
|
|
@ -21,27 +33,34 @@ type HistoryItem = {
|
|||
edges: Edge[];
|
||||
};
|
||||
|
||||
const initialValue = {
|
||||
undo: () => {},
|
||||
redo: () => {},
|
||||
takeSnapshot: () => {},
|
||||
};
|
||||
|
||||
const defaultOptions: UseUndoRedoOptions = {
|
||||
maxHistorySize: 100,
|
||||
enableShortcuts: true,
|
||||
};
|
||||
|
||||
// https://redux.js.org/usage/implementing-undo-history
|
||||
export const useUndoRedo: UseUndoRedo = ({
|
||||
maxHistorySize = defaultOptions.maxHistorySize,
|
||||
enableShortcuts = defaultOptions.enableShortcuts,
|
||||
} = defaultOptions) => {
|
||||
// the past and future arrays store the states that we can jump to
|
||||
const { tabIndex, flows } = useContext(TabsContext);
|
||||
export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
|
||||
|
||||
export function UndoRedoProvider({ children }) {
|
||||
const { tabId, flows } = useContext(TabsContext);
|
||||
|
||||
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
|
||||
const [tabIndex, setTabIndex] = useState(
|
||||
flows.findIndex((f) => f.id === tabId)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// whenever the flows variable changes, we need to add one array to the past and future states
|
||||
setPast((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
||||
setFuture((old) => flows.map((f, i) => (old[i] ? old[i] : [])));
|
||||
}, [flows]);
|
||||
setTabIndex(flows.findIndex((f) => f.id === tabId));
|
||||
}, [flows, tabId]);
|
||||
|
||||
const { setNodes, setEdges, getNodes, getEdges } = useReactFlow();
|
||||
|
||||
|
|
@ -50,7 +69,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
|||
setPast((old) => {
|
||||
let newPast = cloneDeep(old);
|
||||
newPast[tabIndex] = old[tabIndex].slice(
|
||||
old[tabIndex].length - maxHistorySize + 1,
|
||||
old[tabIndex].length - defaultOptions.maxHistorySize + 1,
|
||||
old[tabIndex].length
|
||||
);
|
||||
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
|
||||
|
|
@ -63,16 +82,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
|||
newFuture[tabIndex] = [];
|
||||
return newFuture;
|
||||
});
|
||||
}, [
|
||||
getNodes,
|
||||
getEdges,
|
||||
past,
|
||||
future,
|
||||
tabIndex,
|
||||
setPast,
|
||||
setFuture,
|
||||
maxHistorySize,
|
||||
]);
|
||||
}, [getNodes, getEdges, past, future, flows, tabId, setPast, setFuture]);
|
||||
|
||||
const undo = useCallback(() => {
|
||||
// get the last state that we want to go back to
|
||||
|
|
@ -141,7 +151,7 @@ export const useUndoRedo: UseUndoRedo = ({
|
|||
|
||||
useEffect(() => {
|
||||
// this effect is used to attach the global event handlers
|
||||
if (!enableShortcuts) {
|
||||
if (!defaultOptions.enableShortcuts) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -165,15 +175,16 @@ export const useUndoRedo: UseUndoRedo = ({
|
|||
return () => {
|
||||
document.removeEventListener("keydown", keyDownHandler);
|
||||
};
|
||||
}, [undo, redo, enableShortcuts]);
|
||||
|
||||
return {
|
||||
undo,
|
||||
redo,
|
||||
takeSnapshot,
|
||||
canUndo: !!past.length,
|
||||
canRedo: !!future.length,
|
||||
};
|
||||
};
|
||||
|
||||
export default useUndoRedo;
|
||||
}, [undo, redo]);
|
||||
return (
|
||||
<undoRedoContext.Provider
|
||||
value={{
|
||||
undo,
|
||||
redo,
|
||||
takeSnapshot,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</undoRedoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,28 +1,75 @@
|
|||
import { PromptTypeAPI, errorsTypeAPI } from "./../../types/api/index";
|
||||
import {
|
||||
BuildStatusTypeAPI,
|
||||
PromptTypeAPI,
|
||||
errorsTypeAPI,
|
||||
InitTypeAPI,
|
||||
} from "./../../types/api/index";
|
||||
import { APIObjectType, sendAllProps } from "../../types/api/index";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { FlowStyleType, FlowType } from "../../types/flow";
|
||||
import { ReactFlowJsonObject } from "reactflow";
|
||||
|
||||
/**
|
||||
* Fetches all objects from the API endpoint.
|
||||
*
|
||||
* @returns {Promise<AxiosResponse<APIObjectType>>} A promise that resolves to an AxiosResponse containing all the objects.
|
||||
*/
|
||||
export async function getAll(): Promise<AxiosResponse<APIObjectType>> {
|
||||
return await axios.get(`/all`);
|
||||
return await axios.get(`/api/v1/all`);
|
||||
}
|
||||
|
||||
const GITHUB_API_URL = "https://api.github.com";
|
||||
|
||||
export async function getRepoStars(owner, repo) {
|
||||
try {
|
||||
const response = await axios.get(`${GITHUB_API_URL}/repos/${owner}/${repo}`);
|
||||
return response.data.stargazers_count;
|
||||
} catch (error) {
|
||||
console.error("Error fetching repository data:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends data to the API for prediction.
|
||||
*
|
||||
* @param {sendAllProps} data - The data to be sent to the API.
|
||||
* @returns {AxiosResponse<any>} The API response.
|
||||
*/
|
||||
export async function sendAll(data: sendAllProps) {
|
||||
return await axios.post(`/predict`, data);
|
||||
return await axios.post(`/api/v1/predict`, data);
|
||||
}
|
||||
|
||||
export async function checkCode(
|
||||
export async function postValidateCode(
|
||||
code: string
|
||||
): Promise<AxiosResponse<errorsTypeAPI>> {
|
||||
return await axios.post("/validate/code", { code });
|
||||
return await axios.post("/api/v1/validate/code", { code });
|
||||
}
|
||||
|
||||
export async function postValidateNode(
|
||||
nodeId: string,
|
||||
data: any
|
||||
): Promise<AxiosResponse<string>> {
|
||||
return await axios.post(`/api/v1/validate/node/${nodeId}`, { data });
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the prompt for the code block by sending it to an API endpoint.
|
||||
*
|
||||
* @param {string} template - The template string of the prompt to check.
|
||||
* @returns {Promise<AxiosResponse<PromptTypeAPI>>} A promise that resolves to an AxiosResponse containing the validation results.
|
||||
*/
|
||||
export async function checkPrompt(
|
||||
template: string
|
||||
): Promise<AxiosResponse<PromptTypeAPI>> {
|
||||
return await axios.post("/validate/prompt", { template });
|
||||
return await axios.post("/api/v1/validate/prompt", { template });
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a list of JSON files from a GitHub repository and returns their contents as an array of FlowType objects.
|
||||
*
|
||||
* @returns {Promise<FlowType[]>} A promise that resolves to an array of FlowType objects.
|
||||
*/
|
||||
export async function getExamples(): Promise<FlowType[]> {
|
||||
const url =
|
||||
"https://api.github.com/repos/logspace-ai/langflow_examples/contents/examples";
|
||||
|
|
@ -37,7 +84,236 @@ export async function getExamples(): Promise<FlowType[]> {
|
|||
return contentResponse.data;
|
||||
});
|
||||
|
||||
const contents = await Promise.all(contentsPromises);
|
||||
|
||||
return contents;
|
||||
return await Promise.all(contentsPromises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new flow to the database.
|
||||
*
|
||||
* @param {FlowType} newFlow - The flow data to save.
|
||||
* @returns {Promise<any>} The saved flow data.
|
||||
* @throws Will throw an error if saving fails.
|
||||
*/
|
||||
export async function saveFlowToDatabase(newFlow: {
|
||||
name: string;
|
||||
id: string;
|
||||
data: ReactFlowJsonObject;
|
||||
description: string;
|
||||
style?: FlowStyleType;
|
||||
}): Promise<FlowType> {
|
||||
try {
|
||||
const response = await axios.post("/api/v1/flows/", {
|
||||
name: newFlow.name,
|
||||
data: newFlow.data,
|
||||
description: newFlow.description,
|
||||
});
|
||||
if (response.status !== 201) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Updates an existing flow in the database.
|
||||
*
|
||||
* @param {FlowType} updatedFlow - The updated flow data.
|
||||
* @returns {Promise<any>} The updated flow data.
|
||||
* @throws Will throw an error if the update fails.
|
||||
*/
|
||||
export async function updateFlowInDatabase(
|
||||
updatedFlow: FlowType
|
||||
): Promise<FlowType> {
|
||||
try {
|
||||
const response = await axios.patch(`/api/v1/flows/${updatedFlow.id}`, {
|
||||
name: updatedFlow.name,
|
||||
data: updatedFlow.data,
|
||||
description: updatedFlow.description,
|
||||
});
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all flows from the database.
|
||||
*
|
||||
* @returns {Promise<any>} The flows data.
|
||||
* @throws Will throw an error if reading fails.
|
||||
*/
|
||||
export async function readFlowsFromDatabase() {
|
||||
try {
|
||||
const response = await axios.get("/api/v1/flows/");
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadFlowsFromDatabase() {
|
||||
try {
|
||||
const response = await axios.get("/api/v1/flows/download/");
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadFlowsToDatabase(flows) {
|
||||
try {
|
||||
const response = await axios.post(`/api/v1/flows/upload/`, flows);
|
||||
|
||||
if (response.status !== 201) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a flow from the database.
|
||||
*
|
||||
* @param {string} flowId - The ID of the flow to delete.
|
||||
* @returns {Promise<any>} The deleted flow data.
|
||||
* @throws Will throw an error if deletion fails.
|
||||
*/
|
||||
export async function deleteFlowFromDatabase(flowId: string) {
|
||||
try {
|
||||
const response = await axios.delete(`/api/v1/flows/${flowId}`);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a flow from the database by ID.
|
||||
*
|
||||
* @param {number} flowId - The ID of the flow to fetch.
|
||||
* @returns {Promise<any>} The flow data.
|
||||
* @throws Will throw an error if fetching fails.
|
||||
*/
|
||||
export async function getFlowFromDatabase(flowId: number) {
|
||||
try {
|
||||
const response = await axios.get(`/api/v1/flows/${flowId}`);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches flow styles from the database.
|
||||
*
|
||||
* @returns {Promise<any>} The flow styles data.
|
||||
* @throws Will throw an error if fetching fails.
|
||||
*/
|
||||
export async function getFlowStylesFromDatabase() {
|
||||
try {
|
||||
const response = await axios.get("/api/v1/flow_styles/");
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a new flow style to the database.
|
||||
*
|
||||
* @param {FlowStyleType} flowStyle - The flow style data to save.
|
||||
* @returns {Promise<any>} The saved flow style data.
|
||||
* @throws Will throw an error if saving fails.
|
||||
*/
|
||||
export async function saveFlowStyleToDatabase(flowStyle: FlowStyleType) {
|
||||
try {
|
||||
const response = await axios.post("/api/v1/flow_styles/", flowStyle, {
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status !== 201) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the version of the API.
|
||||
*
|
||||
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the version information.
|
||||
*/
|
||||
export async function getVersion() {
|
||||
const respnose = await axios.get("/api/v1/version");
|
||||
return respnose.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the health status of the API.
|
||||
*
|
||||
* @returns {Promise<AxiosResponse<any>>} A promise that resolves to an AxiosResponse containing the health status.
|
||||
*/
|
||||
export async function getHealth() {
|
||||
return await axios.get("/health"); // Health is the only endpoint that doesn't require /api/v1
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the build status of a flow.
|
||||
* @param {string} flowId - The ID of the flow to fetch the build status for.
|
||||
* @returns {Promise<BuildStatusTypeAPI>} A promise that resolves to an AxiosResponse containing the build status.
|
||||
*
|
||||
*/
|
||||
export async function getBuildStatus(
|
||||
flowId: string
|
||||
): Promise<BuildStatusTypeAPI> {
|
||||
return await axios.get(`/api/v1/build/${flowId}/status`);
|
||||
}
|
||||
|
||||
//docs for postbuildinit
|
||||
/**
|
||||
* Posts the build init of a flow.
|
||||
* @param {string} flowId - The ID of the flow to fetch the build status for.
|
||||
* @returns {Promise<InitTypeAPI>} A promise that resolves to an AxiosResponse containing the build status.
|
||||
*
|
||||
*/
|
||||
export async function postBuildInit(
|
||||
flow: FlowType
|
||||
): Promise<AxiosResponse<InitTypeAPI>> {
|
||||
return await axios.post(`/api/v1/build/init`, flow);
|
||||
}
|
||||
|
|
|
|||
9
src/frontend/src/icons/Anthropic/anthropic.svg
Normal file
9
src/frontend/src/icons/Anthropic/anthropic.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-170.333 113.047 600 67.4" width="600" height="67.4">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.st0{fill:#1F1F1E;}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M -23.533 126.747 L -1.633 126.747 L -1.633 179.347 L 12.367 179.347 L 12.367 126.747 L 34.267 126.747 L 34.267 114.147 L -23.533 114.147 L -23.533 126.747 Z M -48.133 159.747 L -77.433 114.147 L -93.233 114.147 L -93.233 179.247 L -79.733 179.247 L -79.733 133.647 L -50.433 179.347 L -34.633 179.347 L -34.633 114.247 L -48.133 114.247 L -48.133 159.747 Z M 90.067 140.147 L 59.367 140.147 L 59.367 114.147 L 45.367 114.147 L 45.367 179.247 L 59.367 179.247 L 59.367 152.647 L 90.067 152.647 L 90.067 179.247 L 104.067 179.247 L 104.067 114.147 L 90.067 114.147 L 90.067 140.147 Z M -144.333 114.147 L -170.333 179.247 L -155.833 179.247 L -150.533 165.547 L -123.333 165.547 L -118.033 179.247 L -103.533 179.247 L -129.533 114.147 L -144.333 114.147 Z M -145.833 153.547 L -136.933 130.647 L -128.033 153.547 L -145.833 153.547 Z M 219.667 113.047 C 200.867 113.047 187.567 127.047 187.567 146.847 C 187.567 166.447 200.867 180.447 219.667 180.447 C 238.367 180.447 251.567 166.447 251.567 146.847 C 251.567 127.047 238.367 113.047 219.667 113.047 Z M 219.667 167.447 C 208.667 167.447 201.967 159.647 201.967 146.847 C 201.967 133.947 208.667 126.047 219.667 126.047 C 230.567 126.047 237.167 133.847 237.167 146.847 C 237.167 159.547 230.567 167.447 219.667 167.447 Z M 414.767 157.447 C 412.367 163.747 407.467 167.447 400.867 167.447 C 389.867 167.447 383.167 159.647 383.167 146.847 C 383.167 133.947 389.867 126.047 400.867 126.047 C 407.467 126.047 412.267 129.647 414.767 136.047 L 429.567 136.047 C 425.967 122.047 415.067 113.047 400.867 113.047 C 382.067 113.047 368.767 127.047 368.767 146.847 C 368.767 166.447 382.067 180.447 400.867 180.447 C 415.067 180.447 425.967 171.347 429.667 157.447 L 414.767 157.447 Z M 325.867 114.147 L 351.867 179.247 L 366.067 179.247 L 340.067 114.147 L 325.867 114.147 Z M 296.367 114.147 L 264.567 114.147 L 264.567 179.247 L 278.567 179.247 L 278.567 155.647 L 296.467 155.647 C 311.267 155.647 320.267 147.847 320.267 134.847 C 320.267 121.947 311.167 114.147 296.367 114.147 Z M 295.767 143.147 L 278.567 143.147 L 278.567 126.747 L 295.767 126.747 C 302.667 126.747 306.267 129.547 306.267 134.947 C 306.267 140.347 302.667 143.147 295.767 143.147 Z M 176.867 134.047 C 176.867 121.747 167.867 114.247 153.067 114.247 L 121.267 114.247 L 121.267 179.347 L 135.267 179.347 L 135.267 153.847 L 150.767 153.847 L 164.767 179.347 L 180.167 179.347 L 164.667 151.947 C 172.367 148.847 176.867 142.647 176.867 134.047 Z M 135.167 126.747 L 152.367 126.747 C 159.267 126.747 162.867 129.247 162.867 134.047 C 162.867 138.747 159.267 141.347 152.367 141.347 L 135.167 141.347 L 135.167 126.747 Z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
11
src/frontend/src/icons/Anthropic/anthropic_box.svg
Normal file
11
src/frontend/src/icons/Anthropic/anthropic_box.svg
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="-126.9 247.9 207.161 212.728" width="207.161" height="212.728">
|
||||
<defs>
|
||||
<style type="text/css">
|
||||
.st0{fill:#1F1F1E;}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="st0" d="M 19.9 260.5 L 41.8 260.5 L 41.8 313.1 L 55.8 313.1 L 55.8 260.5 L 77.7 260.5 L 77.7 247.9 L 19.9 247.9 L 19.9 260.5 Z M -4.7 293.5 L -34 247.9 L -49.8 247.9 L -49.8 313 L -36.3 313 L -36.3 267.4 L -7 313.1 L 8.8 313.1 L 8.8 248 L -4.7 248 L -4.7 293.5 Z M -100.9 247.9 L -126.9 313 L -112.4 313 L -107.1 299.3 L -79.9 299.3 L -74.6 313 L -60.1 313 L -86.1 247.9 L -100.9 247.9 Z M -102.4 287.3 L -93.5 264.4 L -84.6 287.3 L -102.4 287.3 Z"></path>
|
||||
<path class="st0" d="M 38.246 437.628 C 35.846 443.928 30.946 447.628 24.346 447.628 C 13.346 447.628 6.646 439.828 6.646 427.028 C 6.646 414.128 13.346 406.228 24.346 406.228 C 30.946 406.228 35.746 409.828 38.246 416.228 L 53.046 416.228 C 49.446 402.228 38.546 393.228 24.346 393.228 C 5.546 393.228 -7.754 407.228 -7.754 427.028 C -7.754 446.628 5.546 460.628 24.346 460.628 C 38.546 460.628 49.446 451.528 53.146 437.628 L 38.246 437.628 Z M -50.654 394.328 L -24.654 459.428 L -10.454 459.428 L -36.454 394.328 L -50.654 394.328 Z M -80.154 394.328 L -111.954 394.328 L -111.954 459.428 L -97.954 459.428 L -97.954 435.828 L -80.054 435.828 C -65.254 435.828 -56.254 428.028 -56.254 415.028 C -56.254 402.128 -65.354 394.328 -80.154 394.328 Z M -80.754 423.328 L -97.954 423.328 L -97.954 406.928 L -80.754 406.928 C -73.854 406.928 -70.254 409.728 -70.254 415.128 C -70.254 420.528 -73.854 423.328 -80.754 423.328 Z"></path>
|
||||
<path class="st0" d="M -81.239 347.704 L -111.939 347.704 L -111.939 321.704 L -125.939 321.704 L -125.939 386.804 L -111.939 386.804 L -111.939 360.204 L -81.239 360.204 L -81.239 386.804 L -67.239 386.804 L -67.239 321.704 L -81.239 321.704 L -81.239 347.704 Z M 48.361 320.604 C 29.561 320.604 16.261 334.604 16.261 354.404 C 16.261 374.004 29.561 388.004 48.361 388.004 C 67.061 388.004 80.261 374.004 80.261 354.404 C 80.261 334.604 67.061 320.604 48.361 320.604 Z M 48.361 375.004 C 37.361 375.004 30.661 367.204 30.661 354.404 C 30.661 341.504 37.361 333.604 48.361 333.604 C 59.261 333.604 65.861 341.404 65.861 354.404 C 65.861 367.104 59.261 375.004 48.361 375.004 Z M 5.561 341.604 C 5.561 329.304 -3.439 321.804 -18.239 321.804 L -50.039 321.804 L -50.039 386.904 L -36.039 386.904 L -36.039 361.404 L -20.539 361.404 L -6.539 386.904 L 8.861 386.904 L -6.639 359.504 C 1.061 356.404 5.561 350.204 5.561 341.604 Z M -36.139 334.304 L -18.939 334.304 C -12.039 334.304 -8.439 336.804 -8.439 341.604 C -8.439 346.304 -12.039 348.904 -18.939 348.904 L -36.139 348.904 L -36.139 334.304 Z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
9
src/frontend/src/icons/Anthropic/index.tsx
Normal file
9
src/frontend/src/icons/Anthropic/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import { ReactComponent as AnthropicSVG } from "./anthropic_box.svg";
|
||||
|
||||
export const AnthropicIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{}>
|
||||
>((props, ref) => {
|
||||
return <AnthropicSVG ref={ref} {...props} />;
|
||||
});
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 109 KiB |
|
|
@ -2,16 +2,166 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--muted: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
|
||||
|
||||
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--card: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--border: 214.3 31.8% 91.4%; /* hsl(214 32% 91%) */
|
||||
--input: 214.3 31.8% 91.4%; /* hsl(214 32% 91%) */
|
||||
|
||||
--primary: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--accent: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
|
||||
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
|
||||
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||
--foreground: 213 31% 91%; /* hsl(213 31% 91%) */
|
||||
|
||||
--muted: 223 47% 11%; /* hsl(223 47% 11%) */
|
||||
--muted-foreground: 215.4 16.3% 56.9%; /* hsl(215 16% 56%) */
|
||||
|
||||
--popover: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||
--popover-foreground: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||
|
||||
--card: 224 71% 4%; /* hsl(224 71% 4%) */
|
||||
--card-foreground: 213 31% 91%; /* hsl(213 31% 91%) */
|
||||
|
||||
--border: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||
--input: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||
|
||||
--primary: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
--primary-foreground: 222.2 47.4% 1.2%; /* hsl(222 47% 1%) */
|
||||
|
||||
--secondary: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--secondary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--accent: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||
--accent-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--destructive: 0 63% 31%; /* hsl(0 63% 31%) */
|
||||
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
|
||||
--ring: 216 34% 17%; /* hsl(216 34% 17%) */
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--muted: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
--muted-foreground: 215.4 16.3% 46.9%; /* hsl(215 16% 46%) */
|
||||
--popover: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--popover-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--card: 0 0% 100%; /* hsl(0 0% 100%) */
|
||||
--card-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--border: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
|
||||
--input: 214.3 21.8% 91.4%; /* hsl(214 32% 91%) */
|
||||
--primary: 222.2 27% 11.2%; /* hsl(222 27% 18%) */
|
||||
--primary-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
--secondary: 210 40% 96.1%; /* hsl(210 40% 96%) */
|
||||
--secondary-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--accent: 210 30% 96.1%; /* hsl(210 30% 96%) */
|
||||
--accent-foreground: 222.2 47.4% 11.2%; /* hsl(222 47% 11%) */
|
||||
--destructive: 0 100% 50%; /* hsl(0 100% 50%) */
|
||||
--destructive-foreground: 210 40% 98%; /* hsl(210 40% 98%) */
|
||||
--ring: 215 20.2% 65.1%; /* hsl(215 20% 65%) */
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
-background: 224 71% 4%;
|
||||
/* hsl(224 71% 4%) */
|
||||
-foreground: 213 31% 91%;
|
||||
/* hsl(213 31% 91%) */
|
||||
-muted: 223 47% 11%;
|
||||
/* hsl(223 47% 11%) */
|
||||
-muted-foreground: 215.4 16.3% 56.9%;
|
||||
/* hsl(215 16% 56%) */
|
||||
-popover: 224 71% 4%;
|
||||
/* hsl(224 71% 4%) */
|
||||
-popover-foreground: 215 20.2% 65.1%;
|
||||
/* hsl(215 20% 65%) */
|
||||
-card: 224 71% 4%;
|
||||
/* hsl(224 71% 4%) */
|
||||
-card-foreground: 213 31% 91%;
|
||||
/* hsl(213 31% 91%) */
|
||||
-border: 216 34% 17%;
|
||||
/* hsl(216 34% 17%) */
|
||||
-input: 216 34% 17%;
|
||||
/* hsl(216 34% 17%) */
|
||||
-primary: 210 40% 98%;
|
||||
/* hsl(210 40% 98%) */
|
||||
-primary-foreground: 222.2 47.4% 1.2%;
|
||||
/* hsl(222 47% 1%) */
|
||||
-secondary: 222.2 47.4% 11.2%;
|
||||
/* hsl(222 47% 11%) */
|
||||
-secondary-foreground: 210 40% 98%;
|
||||
/* hsl(210 40% 98%) */
|
||||
-accent: 216 34% 17%;
|
||||
/* hsl(216 34% 17%) */
|
||||
-accent-foreground: 210 40% 98%;
|
||||
/* hsl(210 40% 98%) */
|
||||
-destructive: 0 63% 31%;
|
||||
/* hsl(0 63% 31%) */
|
||||
-destructive-foreground: 210 40% 98%;
|
||||
/* hsl(210 40% 98%) */
|
||||
-ring: 216 34% 17%;
|
||||
/* hsl(216 34% 17%) */
|
||||
-radius: 0.5rem;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
|
||||
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
|
||||
monospace;
|
||||
}
|
||||
|
||||
/* The style below sets the cursor property of the element with the class .react-flow__pane to the default cursor.
|
||||
The cursor: default; property value restores the browser's default cursor style for the targeted element. By applying this style, the element will no longer have a custom cursor appearance such as "grab" or any other custom cursor defined elsewhere in the application. Instead, it will revert to the default cursor style determined by the browser, typically an arrow-shaped cursor. */
|
||||
.react-flow__pane {
|
||||
cursor: default;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { IconCheck, IconClipboard, IconDownload } from "@tabler/icons-react";
|
||||
import {
|
||||
XMarkIcon,
|
||||
CommandLineIcon,
|
||||
CodeBracketSquareIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useRef, useState } from "react";
|
||||
import { CodeBracketSquareIcon } from "@heroicons/react/24/outline";
|
||||
import { useContext, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import "ace-builds/src-noconflict/mode-python";
|
||||
import "ace-builds/src-noconflict/theme-github";
|
||||
|
|
@ -15,13 +9,30 @@ import "ace-builds/src-noconflict/ext-language_tools";
|
|||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { FlowType } from "../../types/flow/index";
|
||||
import { getCurlCode, getPythonApiCode, getPythonCode } from "../../constants";
|
||||
import { EXPORT_CODE_DIALOG } from "../../constants";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "../../components/ui/tabs";
|
||||
import { Check, Clipboard } from "lucide-react";
|
||||
|
||||
export default function ApiModal({ flowName }) {
|
||||
export default function ApiModal({ flow }: { flow: FlowType }) {
|
||||
const [open, setOpen] = useState(true);
|
||||
const { dark } = useContext(darkContext);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const ref = useRef();
|
||||
const [activeTab, setActiveTab] = useState("0");
|
||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
||||
|
||||
const copyToClipboard = () => {
|
||||
|
|
@ -40,33 +51,22 @@ export default function ApiModal({ flowName }) {
|
|||
function setModalOpen(x: boolean) {
|
||||
setOpen(x);
|
||||
if (x === false) {
|
||||
setTimeout(() => {
|
||||
closePopUp();
|
||||
}, 300);
|
||||
closePopUp();
|
||||
}
|
||||
}
|
||||
|
||||
const pythonApiCode = `import requests
|
||||
import json
|
||||
const pythonApiCode = getPythonApiCode(flow);
|
||||
|
||||
API_URL = "${window.location.protocol}//${window.location.host}/predict"
|
||||
|
||||
def predict(message):
|
||||
with open("${flowName}.json", "r") as f:
|
||||
json_data = json.load(f)
|
||||
payload = {'exported_flow': json_data, 'message': message}
|
||||
response = requests.post(API_URL, json=payload)
|
||||
return response.json() # JSON {"result": "Response"}
|
||||
|
||||
print(predict("Your message"))`;
|
||||
|
||||
const pythonCode = `from langflow import load_flow_from_json
|
||||
|
||||
flow = load_flow_from_json("${flowName}.json")
|
||||
# Now you can use it like any chain
|
||||
flow("Hey, have you heard of LangFlow?")`;
|
||||
const curl_code = getCurlCode(flow);
|
||||
const pythonCode = getPythonCode(flow);
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
name: "cURL",
|
||||
mode: "bash",
|
||||
image: "https://curl.se/logo/curl-symbol-transparent.png",
|
||||
code: curl_code,
|
||||
},
|
||||
{
|
||||
name: "Python API",
|
||||
mode: "python",
|
||||
|
|
@ -82,120 +82,58 @@ flow("Hey, have you heard of LangFlow?")`;
|
|||
},
|
||||
];
|
||||
return (
|
||||
<Transition.Root show={open} appear={true} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setModalOpen}
|
||||
initialFocus={ref}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent className="lg:max-w-[800px] sm:max-w-[600px] h-[580px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Code</span>
|
||||
<CodeBracketSquareIcon
|
||||
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>{EXPORT_CODE_DIALOG}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
|
||||
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md text-gray-400 hover:text-gray-500"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-full w-full flex flex-col justify-center items-center">
|
||||
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
|
||||
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<CodeBracketSquareIcon
|
||||
className="h-6 w-6 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 text-center sm:ml-4 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
|
||||
>
|
||||
Code
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full bg-gray-200 overflow-auto dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
|
||||
<div className="flex flex-col h-full w-full ">
|
||||
<div className="flex px-5 z-10">
|
||||
{tabs.map((tab, index) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab(index);
|
||||
}}
|
||||
className={
|
||||
"p-2 rounded-t-lg w-44 border border-b-0 border-gray-300 dark:border-gray-700 dark:text-gray-300 -mr-px flex justify-center items-center gap-4 " +
|
||||
(activeTab === index
|
||||
? " bg-white dark:bg-gray-800"
|
||||
: "bg-gray-100 dark:bg-gray-900")
|
||||
}
|
||||
>
|
||||
{tab.name}
|
||||
<img src={tab.image} className="w-6" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="overflow-hidden px-4 py-5 sm:p-6 w-full h-full rounded-lg shadow bg-white dark:bg-gray-800">
|
||||
<div className="w-full flex items-center justify-between mb-2">
|
||||
<span className="text-sm text-gray-500 dark:text-gray-300">
|
||||
Export your flow to use it with this code.
|
||||
</span>
|
||||
<button
|
||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconCheck size={18} />
|
||||
) : (
|
||||
<IconClipboard size={18} />
|
||||
)}
|
||||
{isCopied ? "Copied!" : "Copy code"}
|
||||
</button>
|
||||
</div>
|
||||
<SyntaxHighlighter
|
||||
className="h-[370px]"
|
||||
language={tabs[activeTab].mode}
|
||||
style={oneDark}
|
||||
customStyle={{ margin: 0 }}
|
||||
>
|
||||
{tabs[activeTab].code}
|
||||
</SyntaxHighlighter>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
<Tabs
|
||||
defaultValue={"0"}
|
||||
className="w-full h-full overflow-hidden text-center bg-muted rounded-md border"
|
||||
onValueChange={(value) => setActiveTab(value)}
|
||||
>
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<TabsList>
|
||||
{tabs.map((tab, index) => (
|
||||
<TabsTrigger value={index.toString()}>{tab.name}</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
<div className="float-right">
|
||||
<button
|
||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-gray-500 dark:text-gray-300"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{isCopied ? <Check size={18} /> : <Clipboard size={15} />}
|
||||
{isCopied ? "Copied!" : "Copy code"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
{tabs.map((tab, index) => (
|
||||
<TabsContent
|
||||
value={index.toString()}
|
||||
className="overflow-hidden w-full h-full px-4 pb-4 -mt-1"
|
||||
>
|
||||
<SyntaxHighlighter
|
||||
className="h-[400px] w-full overflow-auto"
|
||||
language={tab.mode}
|
||||
style={oneDark}
|
||||
>
|
||||
{tab.code}
|
||||
</SyntaxHighlighter>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
316
src/frontend/src/modals/EditNodeModal/index.tsx
Normal file
316
src/frontend/src/modals/EditNodeModal/index.tsx
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { NodeDataType } from "../../types/flow";
|
||||
import { classNames, limitScrollFieldsModal } from "../../utils";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import ToggleShadComponent from "../../components/toggleShadComponent";
|
||||
import { VariableIcon } from "@heroicons/react/24/outline";
|
||||
import InputListComponent from "../../components/inputListComponent";
|
||||
import TextAreaComponent from "../../components/textAreaComponent";
|
||||
import InputComponent from "../../components/inputComponent";
|
||||
import FloatComponent from "../../components/floatComponent";
|
||||
import Dropdown from "../../components/dropdownComponent";
|
||||
import IntComponent from "../../components/intComponent";
|
||||
import InputFileComponent from "../../components/inputFileComponent";
|
||||
import PromptAreaComponent from "../../components/promptComponent";
|
||||
import CodeAreaComponent from "../../components/codeAreaComponent";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
|
||||
export default function EditNodeModal({ data }: { data: NodeDataType }) {
|
||||
const [open, setOpen] = useState(true);
|
||||
const [nodeLength, setNodeLength] = useState(
|
||||
Object.keys(data.node.template).filter(
|
||||
(t) =>
|
||||
t.charAt(0) !== "_" &&
|
||||
data.node.template[t].show &&
|
||||
(data.node.template[t].type === "str" ||
|
||||
data.node.template[t].type === "bool" ||
|
||||
data.node.template[t].type === "float" ||
|
||||
data.node.template[t].type === "code" ||
|
||||
data.node.template[t].type === "prompt" ||
|
||||
data.node.template[t].type === "file" ||
|
||||
data.node.template[t].type === "int")
|
||||
).length
|
||||
);
|
||||
const [nodeValue, setNodeValue] = useState(null);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const { types } = useContext(typesContext);
|
||||
const ref = useRef();
|
||||
const [enabled, setEnabled] = useState(null);
|
||||
if (nodeLength == 0) {
|
||||
closePopUp();
|
||||
}
|
||||
|
||||
function setModalOpen(x: boolean) {
|
||||
setOpen(x);
|
||||
if (x === false) {
|
||||
closePopUp();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {}, [closePopUp, data.node.template]);
|
||||
|
||||
function changeAdvanced(node): void {
|
||||
Object.keys(data.node.template).filter((n, i) => {
|
||||
if (n === node.name) {
|
||||
data.node.template[n].advanced = !data.node.template[n].advanced;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
setNodeValue(!nodeValue);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent className="lg:max-w-[700px] ">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">{data.type}</span>
|
||||
<Badge variant="secondary">ID: {data.id}</Badge>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{data.node?.description}
|
||||
<div className="flex pt-4">
|
||||
<VariableIcon className="w-5 h-5 pe-1 text-gray-700 stroke-2 dark:text-slate-200">
|
||||
|
||||
</VariableIcon>
|
||||
<span className="text-sm font-semibold text-gray-800 dark:text-white">
|
||||
Parameters
|
||||
</span>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex w-full max-h-[350px] h-fit">
|
||||
<div
|
||||
className={classNames(
|
||||
"w-full rounded-lg bg-white dark:bg-gray-800 border-[1px] border-gray-200",
|
||||
nodeLength > limitScrollFieldsModal
|
||||
? "overflow-scroll overflow-x-hidden custom-scroll"
|
||||
: "overflow-hidden"
|
||||
)}
|
||||
>
|
||||
{nodeLength > 0 && (
|
||||
<div className="flex flex-col gap-5 h-fit">
|
||||
<Table className="table-fixed bg-muted outline-1">
|
||||
<TableHeader className="border-gray-200 text-gray-500 text-xs font-medium h-10">
|
||||
<TableRow className="dark:border-b-muted">
|
||||
<TableHead className="h-7 text-center">PARAM</TableHead>
|
||||
<TableHead className="p-0 h-7 text-center">
|
||||
VALUE
|
||||
</TableHead>
|
||||
<TableHead className="text-center h-7">SHOW</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody className="p-0">
|
||||
{Object.keys(data.node.template)
|
||||
.filter(
|
||||
(t) =>
|
||||
t.charAt(0) !== "_" &&
|
||||
data.node.template[t].show &&
|
||||
(data.node.template[t].type === "str" ||
|
||||
data.node.template[t].type === "bool" ||
|
||||
data.node.template[t].type === "float" ||
|
||||
data.node.template[t].type === "code" ||
|
||||
data.node.template[t].type === "prompt" ||
|
||||
data.node.template[t].type === "file" ||
|
||||
data.node.template[t].type === "int")
|
||||
)
|
||||
.map((n, i) => (
|
||||
<TableRow key={i} className="h-10 dark:border-b-muted">
|
||||
<TableCell className="p-0 text-center text-gray-900 dark:text-gray-300 text-sm">
|
||||
{data.node.template[n].name
|
||||
? data.node.template[n].name
|
||||
: data.node.template[n].display_name}
|
||||
</TableCell>
|
||||
<TableCell className="p-0 text-center text-gray-900 text-xs w-[300px] dark:text-gray-300">
|
||||
{data.node.template[n].type === "str" &&
|
||||
!data.node.template[n].options ? (
|
||||
<div className="mx-auto">
|
||||
{data.node.template[n].list ? (
|
||||
<InputListComponent
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
value={
|
||||
!data.node.template[n].value ||
|
||||
data.node.template[n].value === ""
|
||||
? [""]
|
||||
: data.node.template[n].value
|
||||
}
|
||||
onChange={(t: string[]) => {
|
||||
data.node.template[n].value = t;
|
||||
}}
|
||||
/>
|
||||
) : data.node.template[n].multiline ? (
|
||||
<TextAreaComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[n].value = t;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
password={
|
||||
data.node.template[n].password ?? false
|
||||
}
|
||||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[n].value = t;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : data.node.template[n].type === "bool" ? (
|
||||
<div className="ml-auto">
|
||||
{" "}
|
||||
<ToggleShadComponent
|
||||
enabled={data.node.template[n].value}
|
||||
setEnabled={(e) => {
|
||||
data.node.template[n].value = e;
|
||||
setEnabled(e);
|
||||
}}
|
||||
size="small"
|
||||
disabled={false}
|
||||
/>
|
||||
</div>
|
||||
) : data.node.template[n].type === "float" ? (
|
||||
<div className="mx-auto">
|
||||
<FloatComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[n].value = t;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : data.node.template[n].type === "str" &&
|
||||
data.node.template[n].options ? (
|
||||
<div className="mx-auto">
|
||||
<Dropdown
|
||||
numberOfOptions={nodeLength}
|
||||
editNode={true}
|
||||
options={data.node.template[n].options}
|
||||
onSelect={(newValue) =>
|
||||
(data.node.template[n].value = newValue)
|
||||
}
|
||||
value={
|
||||
data.node.template[n].value ??
|
||||
"Choose an option"
|
||||
}
|
||||
></Dropdown>
|
||||
</div>
|
||||
) : data.node.template[n].type === "int" ? (
|
||||
<div className="mx-auto">
|
||||
<IntComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[n].value = t;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : data.node.template[n].type === "file" ? (
|
||||
<div className="mx-auto">
|
||||
<InputFileComponent
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[n].value = t;
|
||||
}}
|
||||
fileTypes={data.node.template[n].fileTypes}
|
||||
suffixes={data.node.template[n].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
data.node.template[n].content = t;
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
) : data.node.template[n].type === "prompt" ? (
|
||||
<div className="mx-auto">
|
||||
<PromptAreaComponent
|
||||
editNode={true}
|
||||
disabled={false}
|
||||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[n].value = t;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : data.node.template[n].type === "code" ? (
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
disabled={false}
|
||||
editNode={true}
|
||||
value={data.node.template[n].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[n].value = t;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : data.node.template[n].type === "Any" ? (
|
||||
"-"
|
||||
) : (
|
||||
<div className="hidden"></div>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="p-0 text-right">
|
||||
<div className="items-center text-center">
|
||||
<ToggleShadComponent
|
||||
enabled={!data.node.template[n].advanced}
|
||||
setEnabled={(e) =>
|
||||
changeAdvanced(data.node.template[n])
|
||||
}
|
||||
disabled={false}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -21,7 +21,6 @@ export default function ModalField({
|
|||
type,
|
||||
index,
|
||||
}) {
|
||||
const { save } = useContext(TabsContext);
|
||||
const [enabled, setEnabled] = useState(
|
||||
data.node.template[name]?.value ?? false
|
||||
);
|
||||
|
|
@ -71,7 +70,6 @@ export default function ModalField({
|
|||
}
|
||||
onChange={(t: string[]) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
) : data.node.template[name].multiline ? (
|
||||
|
|
@ -80,7 +78,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
|
@ -90,7 +87,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -104,7 +100,6 @@ export default function ModalField({
|
|||
setEnabled={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
setEnabled(t);
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -115,7 +110,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -134,7 +128,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -150,7 +143,6 @@ export default function ModalField({
|
|||
suffixes={data.node.template[name].suffixes}
|
||||
onFileChange={(t: string) => {
|
||||
data.node.template[name].content = t;
|
||||
save();
|
||||
}}
|
||||
></InputFileComponent>
|
||||
</div>
|
||||
|
|
@ -161,7 +153,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -172,7 +163,6 @@ export default function ModalField({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
save();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ export default function NodeModal({ data }: { data: NodeDataType }) {
|
|||
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:ring-offset-1 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { LockClosedIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
|
|||
import { classNames } from "../../../utils";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { TabsContext } from "../../../contexts/tabsContext";
|
||||
|
||||
import { INPUT_STYLE } from "../../../constants";
|
||||
export default function ChatInput({
|
||||
lockChat,
|
||||
chatValue,
|
||||
|
|
@ -44,9 +44,10 @@ export default function ChatInput({
|
|||
}}
|
||||
className={classNames(
|
||||
lockChat
|
||||
? " bg-gray-300 text-black dark:bg-gray-700 dark:text-gray-300"
|
||||
? " bg-input text-black dark:bg-gray-700 dark:text-gray-300"
|
||||
: " bg-white-200 text-black dark:bg-gray-900 dark:text-gray-300",
|
||||
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm"
|
||||
"form-input block w-full custom-scroll rounded-md border-gray-300 dark:border-gray-600 pr-10 sm:text-sm" +
|
||||
INPUT_STYLE
|
||||
)}
|
||||
placeholder={"Send a message..."}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,9 +16,11 @@ import Convert from "ansi-to-html";
|
|||
export default function ChatMessage({
|
||||
chat,
|
||||
lockChat,
|
||||
lastMessage,
|
||||
}: {
|
||||
chat: ChatMessageType;
|
||||
lockChat: boolean;
|
||||
lastMessage: boolean;
|
||||
}) {
|
||||
const convert = new Convert({ newline: true });
|
||||
const [message, setMessage] = useState("");
|
||||
|
|
@ -32,8 +34,8 @@ export default function ChatMessage({
|
|||
className={classNames(
|
||||
"w-full py-2 pl-2 flex",
|
||||
chat.isSend
|
||||
? "bg-white dark:bg-gray-900 "
|
||||
: "bg-gray-200 dark:bg-gray-800"
|
||||
? "bg-background dark:bg-gray-900 "
|
||||
: "bg-input dark:bg-gray-800"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
|
@ -48,7 +50,7 @@ export default function ChatMessage({
|
|||
"absolute transition-opacity duration-500 scale-150 " +
|
||||
(lockChat ? "opacity-100" : "opacity-0")
|
||||
}
|
||||
src={AiIcon}
|
||||
src={lastMessage ? AiIcon : AiIconStill}
|
||||
/>
|
||||
<img
|
||||
className={
|
||||
|
|
@ -78,7 +80,7 @@ export default function ChatMessage({
|
|||
<div
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
className=" text-start inline-block rounded-md text-gray-600 dark:text-gray-200 h-full border border-gray-300 dark:border-gray-500
|
||||
bg-gray-100 dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
|
||||
bg-muted dark:bg-gray-800 w-[95%] pb-3 pt-3 px-2 ml-3 cursor-pointer scrollbar-hide overflow-scroll"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: convert.toHtml(chat.thought),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function FileCard({ fileName, content, fileType }) {
|
|||
/>
|
||||
{isHovered && (
|
||||
<div
|
||||
className={`absolute top-0 right-0 bg-gray-100 text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`}
|
||||
className={`absolute top-0 right-0 bg-muted text-gray-700 rounded-bl-lg px-1 text-sm font-bold dark:bg-gray-700 dark:text-gray-300`}
|
||||
>
|
||||
<button
|
||||
className="text-gray-500 py-1 px-2 dark:bg-gray-700 dark:text-gray-300"
|
||||
|
|
@ -54,7 +54,7 @@ export default function FileCard({ fileName, content, fileType }) {
|
|||
return (
|
||||
<button
|
||||
onClick={handleDownload}
|
||||
className="bg-gray-100 shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
|
||||
className="bg-muted shadow rounded w-1/2 text-gray-700 hover:drop-shadow-lg px-2 py-2 flex justify-between items-center border border-gray-300"
|
||||
>
|
||||
<div className="flex gap-2 text-current items-center w-full mr-2">
|
||||
{" "}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,18 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import {
|
||||
ChatBubbleOvalLeftEllipsisIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { ChatBubbleOvalLeftEllipsisIcon } from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react";
|
||||
import { FlowType, NodeType } from "../../types/flow";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { toNormalCase } from "../../utils";
|
||||
import { validateNodes } from "../../utils";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import ChatMessage from "./chatMessage";
|
||||
import { FaEraser } from "react-icons/fa";
|
||||
import { HiX } from "react-icons/hi";
|
||||
import { Eraser } from "lucide-react";
|
||||
import { X } from "lucide-react";
|
||||
import { sendAllProps } from "../../types/api";
|
||||
import { ChatMessageType, ChatType } from "../../types/chat";
|
||||
import { ChatMessageType } from "../../types/chat";
|
||||
import ChatInput from "./chatInput";
|
||||
|
||||
import _, { set } from "lodash";
|
||||
import _ from "lodash";
|
||||
|
||||
export default function ChatModal({
|
||||
flow,
|
||||
|
|
@ -114,6 +111,17 @@ export default function ChatModal({
|
|||
}
|
||||
}
|
||||
|
||||
function getWebSocketUrl(chatId, isDevelopment = false) {
|
||||
const isSecureProtocol = window.location.protocol === "https:";
|
||||
const webSocketProtocol = isSecureProtocol ? "wss" : "ws";
|
||||
const host = isDevelopment ? "localhost:7860" : window.location.host;
|
||||
const chatEndpoint = `/api/v1/chat/${chatId}`;
|
||||
|
||||
return `${
|
||||
isDevelopment ? "ws" : webSocketProtocol
|
||||
}://${host}${chatEndpoint}`;
|
||||
}
|
||||
|
||||
function handleWsMessage(data: any) {
|
||||
if (Array.isArray(data)) {
|
||||
//set chat history
|
||||
|
|
@ -180,12 +188,10 @@ export default function ChatModal({
|
|||
|
||||
function connectWS() {
|
||||
try {
|
||||
const urlWs =
|
||||
const urlWs = getWebSocketUrl(
|
||||
id.current,
|
||||
process.env.NODE_ENV === "development"
|
||||
? `ws://localhost:7860/chat/${id.current}`
|
||||
: `${window.location.protocol === "https:" ? "wss" : "ws"}://${
|
||||
window.location.host
|
||||
}/chat/${id.current}`;
|
||||
);
|
||||
const newWs = new WebSocket(urlWs);
|
||||
newWs.onopen = () => {
|
||||
console.log("WebSocket connection established!");
|
||||
|
|
@ -215,19 +221,11 @@ export default function ChatModal({
|
|||
}
|
||||
};
|
||||
ws.current = newWs;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
if (flow.id === "") {
|
||||
connectWS();
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "There was an error on web connection, please: ",
|
||||
list: [
|
||||
"Refresh the page",
|
||||
"Use a new flow tab",
|
||||
"Check if the backend is up",
|
||||
],
|
||||
});
|
||||
}
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -236,7 +234,7 @@ export default function ChatModal({
|
|||
return () => {
|
||||
console.log("unmount");
|
||||
console.log(ws);
|
||||
if (ws) {
|
||||
if (ws.current) {
|
||||
ws.current.close();
|
||||
}
|
||||
};
|
||||
|
|
@ -244,8 +242,9 @@ export default function ChatModal({
|
|||
|
||||
useEffect(() => {
|
||||
if (
|
||||
ws.current.readyState === ws.current.CLOSED ||
|
||||
ws.current.readyState === ws.current.CLOSING
|
||||
ws.current &&
|
||||
(ws.current.readyState === ws.current.CLOSED ||
|
||||
ws.current.readyState === ws.current.CLOSING)
|
||||
) {
|
||||
connectWS();
|
||||
setLockChat(false);
|
||||
|
|
@ -271,52 +270,6 @@ export default function ChatModal({
|
|||
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||
}, [chatHistory]);
|
||||
|
||||
function validateNode(n: NodeType): Array<string> {
|
||||
if (!n.data?.node?.template || !Object.keys(n.data.node.template)) {
|
||||
setNoticeData({
|
||||
title:
|
||||
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
|
||||
});
|
||||
return [];
|
||||
}
|
||||
|
||||
const {
|
||||
type,
|
||||
node: { template },
|
||||
} = n.data;
|
||||
|
||||
return Object.keys(template).reduce(
|
||||
(errors: Array<string>, t) =>
|
||||
errors.concat(
|
||||
template[t].required &&
|
||||
template[t].show &&
|
||||
(!template[t].value || template[t].value === "") &&
|
||||
!reactFlowInstance
|
||||
.getEdges()
|
||||
.some(
|
||||
(e) =>
|
||||
e.targetHandle.split("|")[1] === t &&
|
||||
e.targetHandle.split("|")[2] === n.id
|
||||
)
|
||||
? [
|
||||
`${type} is missing ${
|
||||
template.display_name
|
||||
? template.display_name
|
||||
: toNormalCase(template[t].name)
|
||||
}.`,
|
||||
]
|
||||
: []
|
||||
),
|
||||
[] as string[]
|
||||
);
|
||||
}
|
||||
|
||||
function validateNodes() {
|
||||
return reactFlowInstance
|
||||
.getNodes()
|
||||
.flatMap((n: NodeType) => validateNode(n));
|
||||
}
|
||||
|
||||
const ref = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -327,7 +280,7 @@ export default function ChatModal({
|
|||
|
||||
function sendMessage() {
|
||||
if (chatValue !== "") {
|
||||
let nodeValidationErrors = validateNodes();
|
||||
let nodeValidationErrors = validateNodes(reactFlowInstance);
|
||||
if (nodeValidationErrors.length === 0) {
|
||||
setLockChat(true);
|
||||
let message = chatValue;
|
||||
|
|
@ -399,13 +352,13 @@ export default function ChatModal({
|
|||
onClick={() => clearChat()}
|
||||
className="absolute top-2 right-10 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
||||
>
|
||||
<FaEraser className="w-4 h-4" />
|
||||
<Eraser className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setModalOpen(false)}
|
||||
className="absolute top-1.5 right-2 hover:text-red-500 text-gray-600 dark:text-gray-300 dark:hover:text-red-500 z-30"
|
||||
>
|
||||
<HiX className="w-5 h-5" />
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -414,7 +367,12 @@ export default function ChatModal({
|
|||
>
|
||||
{chatHistory.length > 0 ? (
|
||||
chatHistory.map((c, i) => (
|
||||
<ChatMessage lockChat={lockChat} chat={c} key={i} />
|
||||
<ChatMessage
|
||||
lockChat={lockChat}
|
||||
chat={c}
|
||||
lastMessage={chatHistory.length - 1 == i ? true : false}
|
||||
key={i}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col h-full text-center justify-center w-full items-center align-middle">
|
||||
|
|
@ -425,7 +383,7 @@ export default function ChatModal({
|
|||
</span>
|
||||
</span>
|
||||
<br />
|
||||
<div className="bg-gray-100 dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
|
||||
<div className="bg-muted dark:bg-gray-900 rounded-md w-2/4 px-6 py-8 border border-gray-200 dark:border-gray-700">
|
||||
<span className="text-base text-gray-500">
|
||||
Start a conversation and click the agent’s thoughts{" "}
|
||||
<span>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XMarkIcon, CommandLineIcon } from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useRef, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
|
|
@ -9,9 +8,21 @@ import "ace-builds/src-noconflict/theme-twilight";
|
|||
import "ace-builds/src-noconflict/ext-language_tools";
|
||||
// import "ace-builds/webpack-resolver";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { checkCode } from "../../controllers/API";
|
||||
import { postValidateCode } from "../../controllers/API";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants";
|
||||
|
||||
export default function CodeAreaModal({
|
||||
value,
|
||||
setValue,
|
||||
|
|
@ -34,143 +45,86 @@ export default function CodeAreaModal({
|
|||
}
|
||||
}
|
||||
return (
|
||||
<Transition.Root show={open} appear={true} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setModalOpen}
|
||||
initialFocus={ref}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent className="lg:max-w-[700px] h-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Edit Code</span>
|
||||
<CommandLineIcon
|
||||
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>{CODE_PROMPT_DIALOG_SUBTITLE}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
|
||||
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md text-gray-400 hover:text-gray-500"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-full w-full flex flex-col justify-center items-center">
|
||||
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
|
||||
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<CommandLineIcon
|
||||
className="h-6 w-6 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 text-center sm:ml-4 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
|
||||
>
|
||||
Edit Code
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full bg-gray-200 overflow-auto dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="overflow-hidden px-4 py-5 sm:p-6 w-full h-full rounded-lg bg-white dark:bg-gray-800 shadow">
|
||||
<AceEditor
|
||||
value={code}
|
||||
mode="python"
|
||||
highlightActiveLine={true}
|
||||
showPrintMargin={false}
|
||||
fontSize={14}
|
||||
showGutter
|
||||
enableLiveAutocompletion
|
||||
theme={dark ? "twilight" : "github"}
|
||||
name="CodeEditor"
|
||||
onChange={(value) => {
|
||||
setCode(value);
|
||||
}}
|
||||
className="h-full w-full rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={() => {
|
||||
checkCode(code)
|
||||
.then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
let importsErrors = apiReturn.data.imports.errors;
|
||||
let funcErrors = apiReturn.data.function.errors;
|
||||
if (
|
||||
funcErrors.length === 0 &&
|
||||
importsErrors.length === 0
|
||||
) {
|
||||
setSuccessData({
|
||||
title: "Code is ready to run",
|
||||
});
|
||||
setModalOpen(false);
|
||||
setValue(code);
|
||||
} else {
|
||||
if (funcErrors.length !== 0) {
|
||||
setErrorData({
|
||||
title: "There is an error in your function",
|
||||
list: funcErrors,
|
||||
});
|
||||
}
|
||||
if (importsErrors.length !== 0) {
|
||||
setErrorData({
|
||||
title: "There is an error in your imports",
|
||||
list: importsErrors,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Something went wrong, please try again",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((_) =>
|
||||
setErrorData({
|
||||
title:
|
||||
"There is something wrong with this code, please review it",
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
Check & Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
<div className="flex h-full w-full mt-2">
|
||||
<AceEditor
|
||||
value={code}
|
||||
mode="python"
|
||||
highlightActiveLine={true}
|
||||
showPrintMargin={false}
|
||||
fontSize={14}
|
||||
showGutter
|
||||
enableLiveAutocompletion
|
||||
theme={dark ? "twilight" : "github"}
|
||||
name="CodeEditor"
|
||||
onChange={(value) => {
|
||||
setCode(value);
|
||||
}}
|
||||
className="w-full rounded-lg h-[300px] custom-scroll border-[1px] border-gray-300 dark:border-gray-600"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
postValidateCode(code)
|
||||
.then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
let importsErrors = apiReturn.data.imports.errors;
|
||||
let funcErrors = apiReturn.data.function.errors;
|
||||
if (funcErrors.length === 0 && importsErrors.length === 0) {
|
||||
setSuccessData({
|
||||
title: "Code is ready to run",
|
||||
});
|
||||
setModalOpen(false);
|
||||
setValue(code);
|
||||
} else {
|
||||
if (funcErrors.length !== 0) {
|
||||
setErrorData({
|
||||
title: "There is an error in your function",
|
||||
list: funcErrors,
|
||||
});
|
||||
}
|
||||
if (importsErrors.length !== 0) {
|
||||
setErrorData({
|
||||
title: "There is an error in your imports",
|
||||
list: importsErrors,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Something went wrong, please try again",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((_) =>
|
||||
setErrorData({
|
||||
title:
|
||||
"There is something wrong with this code, please review it",
|
||||
})
|
||||
);
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
Check & Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,31 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import {
|
||||
XMarkIcon,
|
||||
ArrowDownTrayIcon,
|
||||
DocumentDuplicateIcon,
|
||||
ComputerDesktopIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useRef, useState } from "react";
|
||||
import { ArrowDownTrayIcon } from "@heroicons/react/24/outline";
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { removeApiKeys } from "../../utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Checkbox } from "../../components/ui/checkbox";
|
||||
import { EXPORT_DIALOG_SUBTITLE } from "../../constants";
|
||||
import { Download } from "lucide-react";
|
||||
import EditFlowSettings from "../../components/EditFlowSettingsComponent";
|
||||
|
||||
export default function ExportModal() {
|
||||
const [open, setOpen] = useState(true);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const ref = useRef();
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const { flows, tabIndex, updateFlow, downloadFlow } = useContext(TabsContext);
|
||||
const { flows, tabId, updateFlow, downloadFlow } = useContext(TabsContext);
|
||||
const [isMaxLength, setIsMaxLength] = useState(false);
|
||||
function setModalOpen(x: boolean) {
|
||||
setOpen(x);
|
||||
if (x === false) {
|
||||
|
|
@ -25,156 +34,65 @@ export default function ExportModal() {
|
|||
}, 300);
|
||||
}
|
||||
}
|
||||
const [checked, setChecked] = useState(true);
|
||||
const [name, setName] = useState(flows[tabIndex].name);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [name, setName] = useState(flows.find((f) => f.id === tabId).name);
|
||||
const [description, setDescription] = useState(
|
||||
flows.find((f) => f.id === tabId).description
|
||||
);
|
||||
return (
|
||||
<Transition.Root show={open} appear={true} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setModalOpen}
|
||||
initialFocus={ref}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger asChild></DialogTrigger>
|
||||
<DialogContent className="lg:max-w-[600px] h-[420px] ">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Export</span>
|
||||
<Download
|
||||
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>{EXPORT_DIALOG_SUBTITLE}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
|
||||
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md text-gray-400 hover:text-gray-500"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-full w-full flex flex-col justify-center items-center">
|
||||
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
|
||||
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<ArrowDownTrayIcon
|
||||
className="h-6 w-6 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 text-center sm:ml-4 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
|
||||
>
|
||||
Export
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-16 flex flex-col items-start justify-start h-full w-full bg-gray-200 dark:bg-gray-900 p-4 gap-16">
|
||||
<div className="w-full">
|
||||
<label
|
||||
htmlFor="name"
|
||||
className="block mb-2 font-medium text-gray-700 dark:text-white"
|
||||
>
|
||||
Name
|
||||
</label>
|
||||
<input
|
||||
onChange={(event) => {
|
||||
if (event.target.value != "") {
|
||||
let newFlow = flows[tabIndex];
|
||||
newFlow.name = event.target.value;
|
||||
setName(event.target.value);
|
||||
updateFlow(newFlow);
|
||||
} else {
|
||||
setName(event.target.value);
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
name="name"
|
||||
value={name ?? null}
|
||||
placeholder="File name"
|
||||
id="name"
|
||||
className="focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<label
|
||||
htmlFor="description"
|
||||
className="block mb-2 font-medium text-gray-700 dark:text-white"
|
||||
>
|
||||
Description{" "}
|
||||
<span className="text-gray-400 text-sm">
|
||||
{" "}
|
||||
(optional)
|
||||
</span>
|
||||
</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
onChange={(event) => {
|
||||
let newFlow = flows[tabIndex];
|
||||
newFlow.description = event.target.value;
|
||||
updateFlow(newFlow);
|
||||
}}
|
||||
value={flows[tabIndex].description ?? null}
|
||||
placeholder="Flow description"
|
||||
rows={3}
|
||||
className=" focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 text-gray-900 dark:text-gray-100 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="checkbox" className="flex items-center">
|
||||
<input
|
||||
onChange={(event) => {
|
||||
setChecked(event.target.checked);
|
||||
}}
|
||||
checked={checked}
|
||||
id="checkbox"
|
||||
type="checkbox"
|
||||
className="h-4 w-4 text-blue-600 border-gray-300 rounded dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500"
|
||||
/>
|
||||
<span className="ml-2 font-medium text-gray-700 dark:text-white">
|
||||
Save with my API keys
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-full flex justify-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
if (checked) downloadFlow(flows[tabIndex]);
|
||||
else downloadFlow(removeApiKeys(flows[tabIndex]));
|
||||
}}
|
||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
||||
>
|
||||
Download Flow
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
<EditFlowSettings
|
||||
name={name}
|
||||
description={description}
|
||||
flows={flows}
|
||||
tabId={tabId}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
updateFlow={updateFlow}
|
||||
/>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
onCheckedChange={(event: boolean) => {
|
||||
setChecked(event);
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="terms"
|
||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||
>
|
||||
Save with my API keys
|
||||
</label>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (checked) downloadFlow(flows.find((f) => f.id === tabId));
|
||||
else
|
||||
downloadFlow(removeApiKeys(flows.find((f) => f.id === tabId)));
|
||||
|
||||
closePopUp();
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
Download Flow
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
78
src/frontend/src/modals/flowSettingsModal/index.tsx
Normal file
78
src/frontend/src/modals/flowSettingsModal/index.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { useContext, useRef, useState } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants";
|
||||
import EditFlowSettings from "../../components/EditFlowSettingsComponent";
|
||||
import { Settings2 } from "lucide-react";
|
||||
import { updateFlowInDatabase } from "../../controllers/API";
|
||||
|
||||
export default function FlowSettingsModal() {
|
||||
const [open, setOpen] = useState(true);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const ref = useRef();
|
||||
const { flows, tabId, updateFlow, setTabsState, saveFlow } =
|
||||
useContext(TabsContext);
|
||||
const maxLength = 50;
|
||||
const [name, setName] = useState(flows.find((f) => f.id === tabId).name);
|
||||
const [description, setDescription] = useState(
|
||||
flows.find((f) => f.id === tabId).description
|
||||
);
|
||||
function setModalOpen(x: boolean) {
|
||||
setOpen(x);
|
||||
if (x === false) {
|
||||
setTimeout(() => {
|
||||
closePopUp();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
function handleClick() {
|
||||
let savedFlow = flows.find((f) => f.id === tabId);
|
||||
savedFlow.name = name;
|
||||
savedFlow.description = description;
|
||||
saveFlow(savedFlow);
|
||||
setSuccessData({ title: "Changes saved successfully" });
|
||||
closePopUp();
|
||||
}
|
||||
return (
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger asChild></DialogTrigger>
|
||||
<DialogContent className="lg:max-w-[600px] h-[390px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">Settings </span>
|
||||
<Settings2 className="w-4 h-4 mr-2 dark:text-gray-300" />
|
||||
</DialogTitle>
|
||||
<DialogDescription>{SETTINGS_DIALOG_SUBTITLE}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<EditFlowSettings
|
||||
name={name}
|
||||
description={description}
|
||||
flows={flows}
|
||||
tabId={tabId}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
updateFlow={updateFlow}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button onClick={handleClick} type="submit">
|
||||
Save
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XMarkIcon, DocumentTextIcon } from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useRef, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
|
|
@ -6,7 +5,20 @@ import { darkContext } from "../../contexts/darkContext";
|
|||
import { checkPrompt } from "../../controllers/API";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TypeModal } from "../../utils";
|
||||
export default function PromptAreaModal({
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Textarea } from "../../components/ui/textarea";
|
||||
import { PROMPT_DIALOG_SUBTITLE, TEXT_DIALOG_SUBTITLE } from "../../constants";
|
||||
|
||||
export default function GenericModal({
|
||||
value,
|
||||
setValue,
|
||||
buttonText,
|
||||
|
|
@ -31,145 +43,101 @@ export default function PromptAreaModal({
|
|||
function setModalOpen(x: boolean) {
|
||||
setOpen(x);
|
||||
if (x === false) {
|
||||
setTimeout(() => {
|
||||
closePopUp();
|
||||
}, 300);
|
||||
closePopUp();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={open} appear={true} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setModalOpen}
|
||||
initialFocus={ref}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent className="lg:max-w-[700px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
<span className="pr-2">{myModalTitle}</span>
|
||||
<DocumentTextIcon
|
||||
className="h-6 w-6 text-gray-800 pl-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{(() => {
|
||||
switch (myModalTitle) {
|
||||
case "Edit Text":
|
||||
return TEXT_DIALOG_SUBTITLE;
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
|
||||
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md text-gray-400 hover:text-gray-500"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-full w-full flex flex-col justify-center items-center">
|
||||
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
|
||||
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<DocumentTextIcon
|
||||
className="h-6 w-6 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 text-center sm:ml-4 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
|
||||
>
|
||||
{myModalTitle}
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full bg-gray-200 overflow-auto dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="overflow-hidden px-4 py-5 sm:p-6 w-full h-full rounded-lg bg-white dark:bg-gray-800 shadow">
|
||||
<textarea
|
||||
ref={ref}
|
||||
className="form-input h-full w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white"
|
||||
value={myValue}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={() => {
|
||||
switch (myModalType) {
|
||||
case 1:
|
||||
setModalOpen(false);
|
||||
break;
|
||||
case 2:
|
||||
checkPrompt(myValue)
|
||||
.then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
let inputVariables =
|
||||
apiReturn.data.input_variables;
|
||||
if (inputVariables.length === 0) {
|
||||
setErrorData({
|
||||
title:
|
||||
"The template you are attempting to use does not contain any variables for data entry.",
|
||||
});
|
||||
} else {
|
||||
setSuccessData({
|
||||
title: "Prompt is ready",
|
||||
});
|
||||
setModalOpen(false);
|
||||
setValue(myValue);
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title:
|
||||
"Something went wrong, please try again",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
return setErrorData({
|
||||
title:
|
||||
"There is something wrong with this prompt, please review it",
|
||||
list: [error.response.data.detail],
|
||||
});
|
||||
});
|
||||
break;
|
||||
case "Edit Prompt":
|
||||
return PROMPT_DIALOG_SUBTITLE;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{myButtonText}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})()}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex h-full w-full mt-2">
|
||||
<Textarea
|
||||
ref={ref}
|
||||
className="form-input h-[300px] w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white focus-visible:ring-1"
|
||||
value={myValue}
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
placeholder="Type message here."
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
switch (myModalType) {
|
||||
case 1:
|
||||
setModalOpen(false);
|
||||
break;
|
||||
case 2:
|
||||
checkPrompt(myValue)
|
||||
.then((apiReturn) => {
|
||||
if (apiReturn.data) {
|
||||
let inputVariables = apiReturn.data.input_variables;
|
||||
if (inputVariables.length === 0) {
|
||||
setErrorData({
|
||||
title:
|
||||
"The template you are attempting to use does not contain any variables for data entry.",
|
||||
});
|
||||
} else {
|
||||
setSuccessData({
|
||||
title: "Prompt is ready",
|
||||
});
|
||||
setModalOpen(false);
|
||||
setValue(myValue);
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Something went wrong, please try again",
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
return setErrorData({
|
||||
title:
|
||||
"There is something wrong with this prompt, please review it",
|
||||
list: [error.response.data.detail],
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
{myButtonText}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import {
|
||||
XMarkIcon,
|
||||
ArrowDownTrayIcon,
|
||||
|
|
@ -6,6 +5,7 @@ import {
|
|||
ComputerDesktopIcon,
|
||||
ArrowUpTrayIcon,
|
||||
ArrowLeftIcon,
|
||||
CommandLineIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useRef, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
|
|
@ -17,6 +17,17 @@ import { alertContext } from "../../contexts/alertContext";
|
|||
import LoadingComponent from "../../components/loadingComponent";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { classNames, snakeToSpaces, toNormalCase } from "../../utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "../../components/ui/dialog";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { IMPORT_DIALOG_SUBTITLE } from "../../constants";
|
||||
|
||||
export default function ImportModal() {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
|
@ -52,181 +63,142 @@ export default function ImportModal() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Transition.Root show={open} appear={true} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setModalOpen}
|
||||
initialFocus={ref}
|
||||
<Dialog open={true} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger></DialogTrigger>
|
||||
<DialogContent
|
||||
className={classNames(
|
||||
showExamples
|
||||
? "lg:max-w-[650px] h-[600px]"
|
||||
: "lg:max-w-[650px] h-[450px]"
|
||||
)}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[776px]">
|
||||
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center">
|
||||
{showExamples && (
|
||||
<>
|
||||
<div className="z-50 absolute top-2 left-0 hidden pt-4 pl-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md text-gray-400 hover:text-gray-500"
|
||||
className="rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
setShowExamples(false);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
<ArrowLeftIcon
|
||||
className="h-5 w-5 text-gray-800 ml-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{showExamples && (
|
||||
<>
|
||||
<div className="z-50 absolute top-2 left-0 hidden pt-4 pl-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
onClick={() => {
|
||||
setShowExamples(false);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<ArrowLeftIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="h-full w-full flex flex-col justify-center items-center">
|
||||
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
|
||||
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<ArrowUpTrayIcon
|
||||
className="h-6 w-6 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 text-center sm:ml-4 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
|
||||
>
|
||||
{showExamples ? "Select an example" : "Import"}
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
"h-full w-full bg-gray-200 dark:bg-gray-900 gap-4 overflow-y-auto scrollbar-hide",
|
||||
showExamples && !loadingExamples
|
||||
? "flex flex-row start justify-start items-start p-9 flex-wrap overflow-auto"
|
||||
: "flex flex-row justify-center items-center p-4"
|
||||
)}
|
||||
>
|
||||
{!showExamples && (
|
||||
<div className="flex h-full w-full justify-evenly items-center">
|
||||
<ButtonBox
|
||||
size="big"
|
||||
bgColor="bg-emerald-500 dark:bg-emerald-500/75"
|
||||
description="Prebuilt Examples"
|
||||
icon={
|
||||
<DocumentDuplicateIcon className="h-10 w-10 flex-shrink-0" />
|
||||
}
|
||||
onClick={() => {
|
||||
setShowExamples(true);
|
||||
handleExamples();
|
||||
}}
|
||||
textColor="text-emerald-500 dark:text-emerald-500/75"
|
||||
title="Examples"
|
||||
></ButtonBox>
|
||||
<ButtonBox
|
||||
size="big"
|
||||
bgColor="bg-blue-500 dark:bg-blue-500/75"
|
||||
description="Import from Local"
|
||||
icon={
|
||||
<ComputerDesktopIcon className="h-10 w-10 flex-shrink-0" />
|
||||
}
|
||||
onClick={() => {
|
||||
uploadFlow();
|
||||
setModalOpen(false);
|
||||
}}
|
||||
textColor="text-blue-500 dark:text-blue-500/75"
|
||||
title="Local File"
|
||||
></ButtonBox>
|
||||
</div>
|
||||
)}
|
||||
{showExamples && loadingExamples && (
|
||||
<div className="flex align-middle justify-center items-center">
|
||||
<LoadingComponent remSize={30} />
|
||||
</div>
|
||||
)}
|
||||
{showExamples &&
|
||||
!loadingExamples &&
|
||||
examples.map((example, index) => {
|
||||
return (
|
||||
<div key={index}>
|
||||
{" "}
|
||||
<ButtonBox
|
||||
size="small"
|
||||
bgColor="bg-emerald-500 dark:bg-emerald-500/75"
|
||||
description={
|
||||
example.description ?? "Prebuilt Examples"
|
||||
}
|
||||
icon={
|
||||
<DocumentDuplicateIcon className="h-6 w-6 flex-shrink-0" />
|
||||
}
|
||||
onClick={() => {
|
||||
addFlow(example);
|
||||
setModalOpen(false);
|
||||
}}
|
||||
textColor="text-emerald-500 dark:text-emerald-500/75"
|
||||
title={example.name}
|
||||
></ButtonBox>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 w-full h-20 flex items-center justify-center">
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow_examples"
|
||||
target="_blank"
|
||||
className="flex items-center justify-center text-gray-600 dark:text-gray-300"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
viewBox="0 0 98 96"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="ml-2 ">LangFlow Examples</span>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<span className={classNames(showExamples ? "pl-8 pr-2" : "pr-2")}>
|
||||
{showExamples ? "Select an example" : "Import"}
|
||||
</span>
|
||||
<ArrowUpTrayIcon
|
||||
className="h-5 w-5 text-gray-800 ml-1 dark:text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</DialogTitle>
|
||||
<DialogDescription>{IMPORT_DIALOG_SUBTITLE}</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
"h-full w-full dark:bg-gray-900 overflow-y-auto scrollbar-hide",
|
||||
showExamples && !loadingExamples
|
||||
? "flex flex-row start justify-center items-start flex-wrap overflow-auto mx-auto"
|
||||
: "flex flex-row justify-center items-center"
|
||||
)}
|
||||
>
|
||||
{!showExamples && (
|
||||
<div className="flex h-full w-full justify-evenly items-center">
|
||||
<ButtonBox
|
||||
size="big"
|
||||
bgColor="bg-emerald-500 dark:bg-emerald-500/75"
|
||||
description="Prebuilt Examples"
|
||||
icon={
|
||||
<DocumentDuplicateIcon className="h-10 w-10 flex-shrink-0" />
|
||||
}
|
||||
onClick={() => {
|
||||
setShowExamples(true);
|
||||
handleExamples();
|
||||
}}
|
||||
textColor="text-emerald-500 dark:text-emerald-500/75"
|
||||
title="Examples"
|
||||
></ButtonBox>
|
||||
<ButtonBox
|
||||
size="big"
|
||||
bgColor="bg-blue-500 dark:bg-blue-500/75"
|
||||
description="Import from Local"
|
||||
icon={
|
||||
<ComputerDesktopIcon className="h-10 w-10 flex-shrink-0" />
|
||||
}
|
||||
onClick={() => {
|
||||
uploadFlow();
|
||||
setModalOpen(false);
|
||||
}}
|
||||
textColor="text-blue-500 dark:text-blue-500/75"
|
||||
title="Local File"
|
||||
></ButtonBox>
|
||||
</div>
|
||||
)}
|
||||
{showExamples && loadingExamples && (
|
||||
<div className="flex align-middle justify-center items-center">
|
||||
<LoadingComponent remSize={30} />
|
||||
</div>
|
||||
)}
|
||||
{showExamples &&
|
||||
!loadingExamples &&
|
||||
examples.map((example, index) => {
|
||||
return (
|
||||
<div key={example.name} className="m-2">
|
||||
{" "}
|
||||
<ButtonBox
|
||||
size="small"
|
||||
bgColor="bg-emerald-500 dark:bg-emerald-500/75"
|
||||
description={example.description ?? "Prebuilt Examples"}
|
||||
icon={
|
||||
<DocumentDuplicateIcon className="h-6 w-6 flex-shrink-0" />
|
||||
}
|
||||
onClick={() => {
|
||||
addFlow(example, false);
|
||||
setModalOpen(false);
|
||||
}}
|
||||
textColor="text-emerald-500 dark:text-emerald-500/75"
|
||||
title={example.name}
|
||||
></ButtonBox>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
<DialogFooter>
|
||||
<div className="w-full flex items-center justify-center mt-2">
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow_examples"
|
||||
target="_blank"
|
||||
className="flex items-center justify-center text-gray-600 dark:text-gray-300"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
viewBox="0 0 98 96"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
<span className="ml-2 ">LangFlow Examples</span>
|
||||
</a>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ export default function PromptAreaModal({
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
|
||||
<div className="fixed inset-0 bg-ring dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
|
|
@ -87,7 +87,7 @@ export default function PromptAreaModal({
|
|||
</Dialog.Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full bg-gray-200 overflow-auto dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
|
||||
<div className="h-full w-full bg-accent overflow-auto dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="overflow-hidden px-4 py-5 sm:p-6 w-full h-full rounded-lg bg-white dark:bg-gray-800 shadow">
|
||||
<textarea
|
||||
|
|
@ -105,7 +105,7 @@ export default function PromptAreaModal({
|
|||
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-ring focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:ring-offset-1 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={() => {
|
||||
checkPrompt(myValue)
|
||||
.then((apiReturn) => {
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ export default function TextAreaModal({
|
|||
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:ring-offset-1 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
|
|
|
|||
91
src/frontend/src/pages/CommunityPage/index.tsx
Normal file
91
src/frontend/src/pages/CommunityPage/index.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { GithubIcon, Users2, GitFork } from "lucide-react";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { Button } from "../../components/ui/button";
|
||||
|
||||
import { getExamples } from "../../controllers/API";
|
||||
import { FlowType } from "../../types/flow";
|
||||
import { CardComponent } from "../../components/cardComponent";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
export default function CommunityPage() {
|
||||
const { flows, setTabId, downloadFlows, uploadFlows, addFlow } =
|
||||
useContext(TabsContext);
|
||||
useEffect(() => {
|
||||
setTabId("");
|
||||
}, []);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const [loadingExamples, setLoadingExamples] = useState(false);
|
||||
const [examples, setExamples] = useState<FlowType[]>([]);
|
||||
function handleExamples() {
|
||||
setLoadingExamples(true);
|
||||
getExamples()
|
||||
.then((result) => {
|
||||
setLoadingExamples(false);
|
||||
setExamples(result);
|
||||
})
|
||||
.catch((error) =>
|
||||
setErrorData({
|
||||
title: "there was an error loading examples, please try again",
|
||||
list: [error.message],
|
||||
})
|
||||
);
|
||||
}
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
handleExamples();
|
||||
}, []);
|
||||
return (
|
||||
<div className="w-full h-full flex overflow-auto flex-col bg-muted px-16">
|
||||
<div className="w-full flex justify-between py-12 pb-2 px-6">
|
||||
<span className="text-2xl flex items-center justify-center gap-2 font-semibold">
|
||||
<Users2 className="w-6" />
|
||||
Community Examples
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href="https://github.com/logspace-ai/langflow_examples"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<Button variant="primary">
|
||||
<GithubIcon className="w-4 mr-2" />
|
||||
Add Your Example
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<span className="flex pb-8 px-6 w-[70%] text-muted-foreground">
|
||||
Discover and learn from shared examples by the LangFlow community. We
|
||||
welcome new example contributions that can help our community explore
|
||||
new and powerful features.
|
||||
</span>
|
||||
<div className="w-full p-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{!loadingExamples &&
|
||||
examples.map((flow, idx) => (
|
||||
<CardComponent
|
||||
key={idx}
|
||||
flow={flow}
|
||||
id={flow.id}
|
||||
button={
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="whitespace-nowrap "
|
||||
onClick={() => {
|
||||
addFlow(flow, true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GitFork className="w-4 mr-2" />
|
||||
Fork Example
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,13 +5,14 @@ import { DisclosureComponentType } from "../../../../types/components";
|
|||
export default function DisclosureComponent({
|
||||
button: { title, Icon, buttons = [] },
|
||||
children,
|
||||
openDisc,
|
||||
}: DisclosureComponentType) {
|
||||
return (
|
||||
<Disclosure as="div" key={title}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Disclosure.Button className="select-none bg-gray-50 dark:bg-gray-700/60 dark:border-y-gray-600 w-full flex justify-between items-center -mt-px px-3 py-2 border-y border-y-gray-200">
|
||||
<Disclosure.Button className="select-none bg-muted dark:bg-gray-700/60 dark:border-y-gray-600 w-full flex justify-between items-center -mt-px px-3 py-2 border-y border-y-gray-200">
|
||||
<div className="flex gap-4">
|
||||
<Icon className="w-6 text-gray-800 dark:text-white/80" />
|
||||
<span className="flex items-center text-sm text-gray-800 dark:text-white/80 font-medium">
|
||||
|
|
@ -27,14 +28,14 @@ export default function DisclosureComponent({
|
|||
<div>
|
||||
<ChevronRightIcon
|
||||
className={`${
|
||||
open ? "rotate-90 transform" : ""
|
||||
open || openDisc ? "rotate-90 transform" : ""
|
||||
} h-4 w-4 text-gray-800 dark:text-white`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
<Disclosure.Panel as="div" className="-mt-px">
|
||||
<Disclosure.Panel as="div" className="-mt-px" static={openDisc}>
|
||||
{children}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,416 @@
|
|||
import _, { set } from "lodash";
|
||||
import { useContext, useRef, useState, useEffect, useCallback } from "react";
|
||||
import ReactFlow, {
|
||||
OnSelectionChangeParams,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
useReactFlow,
|
||||
EdgeChange,
|
||||
Connection,
|
||||
addEdge,
|
||||
NodeDragHandler,
|
||||
SelectionDragHandler,
|
||||
OnEdgesDelete,
|
||||
Edge,
|
||||
updateEdge,
|
||||
Background,
|
||||
Controls,
|
||||
NodeChange,
|
||||
} from "reactflow";
|
||||
import GenericNode from "../../../../CustomNodes/GenericNode";
|
||||
import Chat from "../../../../components/chatComponent";
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import { locationContext } from "../../../../contexts/locationContext";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { APIClassType } from "../../../../types/api";
|
||||
import { FlowType, NodeType } from "../../../../types/flow";
|
||||
import { isValidConnection } from "../../../../utils";
|
||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||
import ExtraSidebar from "../extraSidebarComponent";
|
||||
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
|
||||
|
||||
const nodeTypes = {
|
||||
genericNode: GenericNode,
|
||||
};
|
||||
|
||||
export default function Page({ flow }: { flow: FlowType }) {
|
||||
let {
|
||||
updateFlow,
|
||||
disableCopyPaste,
|
||||
addFlow,
|
||||
getNodeId,
|
||||
paste,
|
||||
lastCopiedSelection,
|
||||
setLastCopiedSelection,
|
||||
tabsState,
|
||||
saveFlow,
|
||||
setTabsState,
|
||||
tabId,
|
||||
} = useContext(TabsContext);
|
||||
const { types, reactFlowInstance, setReactFlowInstance, templates } =
|
||||
useContext(typesContext);
|
||||
const reactFlowWrapper = useRef(null);
|
||||
|
||||
const { takeSnapshot } = useContext(undoRedoContext);
|
||||
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [lastSelection, setLastSelection] =
|
||||
useState<OnSelectionChangeParams>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// this effect is used to attach the global event handlers
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "c" &&
|
||||
lastSelection &&
|
||||
!disableCopyPaste
|
||||
) {
|
||||
event.preventDefault();
|
||||
setLastCopiedSelection(_.cloneDeep(lastSelection));
|
||||
}
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "v" &&
|
||||
lastCopiedSelection &&
|
||||
!disableCopyPaste
|
||||
) {
|
||||
event.preventDefault();
|
||||
let bounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
paste(lastCopiedSelection, {
|
||||
x: position.x - bounds.left,
|
||||
y: position.y - bounds.top,
|
||||
});
|
||||
}
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "g" &&
|
||||
lastSelection
|
||||
) {
|
||||
event.preventDefault();
|
||||
// addFlow(newFlow, false);
|
||||
}
|
||||
};
|
||||
const handleMouseMove = (event) => {
|
||||
setPosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, [position, lastCopiedSelection, lastSelection]);
|
||||
|
||||
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
|
||||
|
||||
const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||
flow.data?.nodes ?? []
|
||||
);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
||||
flow.data?.edges ?? []
|
||||
);
|
||||
const { setViewport } = useReactFlow();
|
||||
const edgeUpdateSuccessful = useRef(true);
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance && flow) {
|
||||
flow.data = reactFlowInstance.toObject();
|
||||
updateFlow(flow);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodes, edges]);
|
||||
//update flow when tabs change
|
||||
useEffect(() => {
|
||||
setNodes(flow?.data?.nodes ?? []);
|
||||
setEdges(flow?.data?.edges ?? []);
|
||||
if (reactFlowInstance) {
|
||||
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
|
||||
reactFlowInstance.fitView();
|
||||
}
|
||||
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
|
||||
//set extra sidebar
|
||||
useEffect(() => {
|
||||
setExtraComponent(<ExtraSidebar />);
|
||||
setExtraNavigation({ title: "Components" });
|
||||
}, [setExtraComponent, setExtraNavigation]);
|
||||
|
||||
const onEdgesChangeMod = useCallback(
|
||||
(s: EdgeChange[]) => {
|
||||
onEdgesChange(s);
|
||||
setNodes((x) => {
|
||||
let newX = _.cloneDeep(x);
|
||||
return newX;
|
||||
});
|
||||
setTabsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[onEdgesChange, setNodes, setTabsState, tabId]
|
||||
);
|
||||
|
||||
const onNodesChangeMod = useCallback(
|
||||
(s: NodeChange[]) => {
|
||||
onNodesChange(s);
|
||||
setTabsState((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
[tabId]: {
|
||||
isPending: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[onNodesChange, setTabsState, tabId]
|
||||
);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => {
|
||||
takeSnapshot();
|
||||
setEdges((eds) =>
|
||||
addEdge(
|
||||
{
|
||||
...params,
|
||||
style: { stroke: "inherit" },
|
||||
className:
|
||||
params.targetHandle.split("|")[0] === "Text"
|
||||
? "stroke-gray-800 dark:stroke-gray-300"
|
||||
: "stroke-gray-900 dark:stroke-gray-200",
|
||||
animated: params.targetHandle.split("|")[0] === "Text",
|
||||
},
|
||||
eds
|
||||
)
|
||||
);
|
||||
setNodes((x) => {
|
||||
let newX = _.cloneDeep(x);
|
||||
return newX;
|
||||
});
|
||||
},
|
||||
[setEdges, setNodes, takeSnapshot]
|
||||
);
|
||||
|
||||
const onNodeDragStart: NodeDragHandler = useCallback(() => {
|
||||
// 👇 make dragging a node undoable
|
||||
takeSnapshot();
|
||||
// 👉 you can place your event handlers here
|
||||
}, [takeSnapshot]);
|
||||
|
||||
const onSelectionDragStart: SelectionDragHandler = useCallback(() => {
|
||||
// 👇 make dragging a selection undoable
|
||||
takeSnapshot();
|
||||
}, [takeSnapshot]);
|
||||
|
||||
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
|
||||
// 👇 make deleting edges undoable
|
||||
takeSnapshot();
|
||||
}, [takeSnapshot]);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
takeSnapshot();
|
||||
|
||||
// Get the current bounds of the ReactFlow wrapper element
|
||||
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
|
||||
// Extract the data from the drag event and parse it as a JSON object
|
||||
let data: { type: string; node?: APIClassType } = JSON.parse(
|
||||
event.dataTransfer.getData("json")
|
||||
);
|
||||
|
||||
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
|
||||
// Calculate the position where the node should be created
|
||||
const position = reactFlowInstance.project({
|
||||
x: event.clientX - reactflowBounds.left,
|
||||
y: event.clientY - reactflowBounds.top,
|
||||
});
|
||||
|
||||
// Generate a unique node ID
|
||||
let { type } = data;
|
||||
let newId = getNodeId(type);
|
||||
let newNode: NodeType;
|
||||
|
||||
if (data.type !== "groupNode") {
|
||||
// Create a new node object
|
||||
newNode = {
|
||||
id: newId,
|
||||
type: "genericNode",
|
||||
position,
|
||||
data: {
|
||||
...data,
|
||||
id: newId,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Create a new node object
|
||||
newNode = {
|
||||
id: newId,
|
||||
type: "genericNode",
|
||||
position,
|
||||
data: {
|
||||
...data,
|
||||
id: newId,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Add the new node to the list of nodes in state
|
||||
}
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
},
|
||||
// Specify dependencies for useCallback
|
||||
[getNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (tabsState && tabsState[flow.id]?.isPending) {
|
||||
saveFlow(flow);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onDelete = useCallback(
|
||||
(mynodes) => {
|
||||
takeSnapshot();
|
||||
setEdges(
|
||||
edges.filter(
|
||||
(ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id)
|
||||
)
|
||||
);
|
||||
},
|
||||
[takeSnapshot, edges, setEdges]
|
||||
);
|
||||
|
||||
const onEdgeUpdateStart = useCallback(() => {
|
||||
edgeUpdateSuccessful.current = false;
|
||||
}, []);
|
||||
|
||||
const onEdgeUpdate = useCallback(
|
||||
(oldEdge: Edge, newConnection: Connection) => {
|
||||
if (isValidConnection(newConnection, reactFlowInstance)) {
|
||||
edgeUpdateSuccessful.current = true;
|
||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onEdgeUpdateEnd = useCallback((_, edge) => {
|
||||
if (!edgeUpdateSuccessful.current) {
|
||||
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
|
||||
}
|
||||
|
||||
edgeUpdateSuccessful.current = true;
|
||||
}, []);
|
||||
|
||||
const [selectionEnded, setSelectionEnded] = useState(false);
|
||||
|
||||
const onSelectionEnd = useCallback(() => {
|
||||
setSelectionEnded(true);
|
||||
}, []);
|
||||
const onSelectionStart = useCallback(() => {
|
||||
setSelectionEnded(false);
|
||||
}, []);
|
||||
|
||||
// Workaround to show the menu only after the selection has ended.
|
||||
useEffect(() => {
|
||||
if (selectionEnded && lastSelection && lastSelection.nodes.length > 1) {
|
||||
setSelectionMenuVisible(true);
|
||||
} else {
|
||||
setSelectionMenuVisible(false);
|
||||
}
|
||||
}, [selectionEnded, lastSelection]);
|
||||
|
||||
const onSelectionChange = useCallback((flow) => {
|
||||
setLastSelection(flow);
|
||||
}, []);
|
||||
|
||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||
|
||||
return (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<ExtraSidebar />
|
||||
{/* Main area */}
|
||||
<main className="flex-1 flex">
|
||||
{/* Primary column */}
|
||||
<div className="w-full h-full">
|
||||
<div className="w-full h-full" ref={reactFlowWrapper}>
|
||||
{Object.keys(templates).length > 0 &&
|
||||
Object.keys(types).length > 0 ? (
|
||||
<div className="w-full h-full">
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
onMove={() => {
|
||||
updateFlow({
|
||||
...flow,
|
||||
data: reactFlowInstance.toObject(),
|
||||
});
|
||||
}}
|
||||
edges={edges}
|
||||
onPaneClick={() => {
|
||||
setDisableCopyPaste(false);
|
||||
}}
|
||||
onPaneMouseLeave={() => {
|
||||
setDisableCopyPaste(true);
|
||||
}}
|
||||
onPaneMouseEnter={() => {
|
||||
setDisableCopyPaste(false);
|
||||
}}
|
||||
onNodesChange={onNodesChangeMod}
|
||||
onEdgesChange={onEdgesChangeMod}
|
||||
onConnect={onConnect}
|
||||
disableKeyboardA11y={true}
|
||||
onLoad={setReactFlowInstance}
|
||||
onInit={setReactFlowInstance}
|
||||
nodeTypes={nodeTypes}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onSelectionDragStart={onSelectionDragStart}
|
||||
onSelectionEnd={onSelectionEnd}
|
||||
onSelectionStart={onSelectionStart}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
connectionLineComponent={ConnectionLineComponent}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onNodesDelete={onDelete}
|
||||
onSelectionChange={onSelectionChange}
|
||||
nodesDraggable={!disableCopyPaste}
|
||||
panOnDrag={!disableCopyPaste}
|
||||
zoomOnDoubleClick={!disableCopyPaste}
|
||||
selectNodesOnDrag={false}
|
||||
className="theme-attribution"
|
||||
minZoom={0.05}
|
||||
>
|
||||
<Background className="dark:bg-gray-900" />
|
||||
<Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600"></Controls>
|
||||
</ReactFlow>
|
||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance} />
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,14 +1,35 @@
|
|||
import { Bars2Icon } from "@heroicons/react/24/outline";
|
||||
import DisclosureComponent from "../DisclosureComponent";
|
||||
import { nodeColors, nodeIcons, nodeNames } from "../../../../utils";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
classNames,
|
||||
nodeColors,
|
||||
nodeIcons,
|
||||
nodeNames,
|
||||
} from "../../../../utils";
|
||||
import { useContext, useState } from "react";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { APIClassType, APIObjectType } from "../../../../types/api";
|
||||
import TooltipReact from "../../../../components/ReactTooltipComponent";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||
import { Code2, FileDown, FileUp, Save } from "lucide-react";
|
||||
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||
import ExportModal from "../../../../modals/exportModal";
|
||||
import ApiModal from "../../../../modals/ApiModal";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import { updateFlowInDatabase } from "../../../../controllers/API";
|
||||
import { INPUT_STYLE } from "../../../../constants";
|
||||
import { Separator } from "../../../../components/ui/separator";
|
||||
|
||||
export default function ExtraSidebar() {
|
||||
const { data } = useContext(typesContext);
|
||||
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
const { flows, tabId, uploadFlow, tabsState, saveFlow } =
|
||||
useContext(TabsContext);
|
||||
const { setSuccessData, setErrorData } = useContext(alertContext);
|
||||
const [dataFilter, setFilterData] = useState(data);
|
||||
const [search, setSearch] = useState("");
|
||||
const isPending = tabsState[tabId]?.isPending;
|
||||
function onDragStart(
|
||||
event: React.DragEvent<any>,
|
||||
data: { type: string; node?: APIClassType }
|
||||
|
|
@ -24,66 +45,161 @@ export default function ExtraSidebar() {
|
|||
event.dataTransfer.setData("json", JSON.stringify(data));
|
||||
}
|
||||
|
||||
function handleSearchInput(e: string) {
|
||||
setFilterData((_) => {
|
||||
let ret = {};
|
||||
Object.keys(data).forEach((d: keyof APIObjectType, i) => {
|
||||
ret[d] = {};
|
||||
let keys = Object.keys(data[d]).filter((nd) =>
|
||||
nd.toLowerCase().includes(e.toLowerCase())
|
||||
);
|
||||
keys.forEach((element) => {
|
||||
ret[d][element] = data[d][element];
|
||||
});
|
||||
});
|
||||
return ret;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-1 w-full">
|
||||
{Object.keys(data)
|
||||
.sort()
|
||||
.map((d: keyof APIObjectType, i) => (
|
||||
<DisclosureComponent
|
||||
key={i}
|
||||
button={{
|
||||
title: nodeNames[d] ?? nodeNames.unknown,
|
||||
Icon: nodeIcons[d] ?? nodeIcons.unknown,
|
||||
<div className="w-52 flex flex-col overflow-hidden scrollbar-hide h-full border-r">
|
||||
<div className="mt-2 mb-2 w-full flex gap-2 justify-between px-2 items-center">
|
||||
<ShadTooltip delayDuration={1000} content="Import" side="top">
|
||||
<button
|
||||
className="hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center rounded-md bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
|
||||
onClick={() => {
|
||||
// openPopUp(<ImportModal />);
|
||||
uploadFlow();
|
||||
}}
|
||||
>
|
||||
<div className="p-2 flex flex-col gap-2">
|
||||
{Object.keys(data[d])
|
||||
.sort()
|
||||
.map((t: string, k) => (
|
||||
<TooltipReact
|
||||
selector={t}
|
||||
htmlContent={t}
|
||||
position="right"
|
||||
delayShow={1500}
|
||||
key={k}
|
||||
>
|
||||
<div key={k} data-tooltip-id={t}>
|
||||
<div
|
||||
draggable
|
||||
className={" cursor-grab border-l-8 rounded-l-md"}
|
||||
style={{
|
||||
borderLeftColor: nodeColors[d] ?? nodeColors.unknown,
|
||||
}}
|
||||
onDragStart={(event) =>
|
||||
onDragStart(event, {
|
||||
type: t,
|
||||
node: data[d][t],
|
||||
})
|
||||
}
|
||||
onDragEnd={() => {
|
||||
document.body.removeChild(
|
||||
document.getElementsByClassName(
|
||||
"cursor-grabbing"
|
||||
)[0]
|
||||
);
|
||||
}}
|
||||
<FileUp className="w-5 h-5 dark:text-gray-300"></FileUp>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
|
||||
<ShadTooltip delayDuration={1000} content="Export" side="top">
|
||||
<button
|
||||
className={classNames(
|
||||
"hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
||||
)}
|
||||
onClick={(event) => {
|
||||
openPopUp(<ExportModal />);
|
||||
}}
|
||||
>
|
||||
<FileDown className="w-5 h-5 dark:text-gray-300"></FileDown>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
<ShadTooltip delayDuration={1000} content="Code" side="top">
|
||||
<button
|
||||
className={classNames(
|
||||
"hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center shadow-sm transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
||||
)}
|
||||
onClick={(event) => {
|
||||
openPopUp(<ApiModal flow={flows.find((f) => f.id === tabId)} />);
|
||||
}}
|
||||
>
|
||||
<Code2 className="w-5 h-5 dark:text-gray-300"></Code2>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
|
||||
<ShadTooltip delayDuration={1000} content="Save" side="top">
|
||||
<button
|
||||
className="hover:dark:hover:bg-[#242f47] text-gray-700 w-full justify-center transition-all shadow-sm duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 relative inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 rounded-md"
|
||||
onClick={(event) => {
|
||||
saveFlow(flows.find((f) => f.id === tabId));
|
||||
setSuccessData({ title: "Changes saved successfully" });
|
||||
}}
|
||||
disabled={!isPending}
|
||||
>
|
||||
<Save
|
||||
className={
|
||||
"w-5 h-5" + (isPending ? " " : " text-muted-foreground")
|
||||
}
|
||||
></Save>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="relative mt-2 flex items-center mb-2 mx-2">
|
||||
<input
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
placeholder="Search"
|
||||
className={
|
||||
INPUT_STYLE +
|
||||
"w-full border-1 dark:border-slate-600 dark:border-0.5 dark:ring-0 focus-visible:dark:ring-0 focus-visible:dark:ring-offset-1 rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50"
|
||||
}
|
||||
onChange={(e) => {
|
||||
handleSearchInput(e.target.value);
|
||||
setSearch(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex py-1.5 pr-3 items-center">
|
||||
<MagnifyingGlassIcon className="h-5 w-5 dark:text-white"></MagnifyingGlassIcon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full overflow-auto scrollbar-hide">
|
||||
{Object.keys(dataFilter)
|
||||
.sort()
|
||||
.map((d: keyof APIObjectType, i) =>
|
||||
Object.keys(dataFilter[d]).length > 0 ? (
|
||||
<DisclosureComponent
|
||||
openDisc={search.length == 0 ? false : true}
|
||||
key={i}
|
||||
button={{
|
||||
title: nodeNames[d] ?? nodeNames.unknown,
|
||||
Icon: nodeIcons[d] ?? nodeIcons.unknown,
|
||||
}}
|
||||
>
|
||||
<div className="p-2 flex flex-col gap-2">
|
||||
{Object.keys(dataFilter[d])
|
||||
.sort()
|
||||
.map((t: string, k) => (
|
||||
<ShadTooltip
|
||||
content={t}
|
||||
delayDuration={1500}
|
||||
side="right"
|
||||
>
|
||||
<div className="flex w-full justify-between text-sm px-3 py-1 bg-white dark:bg-gray-800 items-center border-dashed border-gray-400 dark:border-gray-600 border-l-0 rounded-md rounded-l-none border">
|
||||
<span className="text-black dark:text-white w-36 pr-1 truncate text-xs">
|
||||
{t}
|
||||
</span>
|
||||
<Bars2Icon className="w-4 h-6 text-gray-400 dark:text-gray-600" />
|
||||
<div key={k} data-tooltip-id={t}>
|
||||
<div
|
||||
draggable
|
||||
className={" cursor-grab border-l-8 rounded-l-md"}
|
||||
style={{
|
||||
borderLeftColor:
|
||||
nodeColors[d] ?? nodeColors.unknown,
|
||||
}}
|
||||
onDragStart={(event) =>
|
||||
onDragStart(event, {
|
||||
type: t,
|
||||
node: data[d][t],
|
||||
})
|
||||
}
|
||||
onDragEnd={() => {
|
||||
document.body.removeChild(
|
||||
document.getElementsByClassName(
|
||||
"cursor-grabbing"
|
||||
)[0]
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className="flex w-full justify-between text-sm px-3 py-1 bg-white dark:bg-gray-800 items-center border-dashed border-gray-400 dark:border-gray-600 border-l-0 rounded-md rounded-l-none border">
|
||||
<span className="text-black dark:text-white w-full pr-1 truncate text-xs">
|
||||
{t}
|
||||
</span>
|
||||
<Bars2Icon className="w-4 h-6 text-gray-400 dark:text-gray-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TooltipReact>
|
||||
))}
|
||||
{Object.keys(data[d]).length === 0 && (
|
||||
<div className="text-gray-400 text-center">Coming soon</div>
|
||||
)}
|
||||
</div>
|
||||
</DisclosureComponent>
|
||||
))}
|
||||
</ShadTooltip>
|
||||
))}
|
||||
</div>
|
||||
</DisclosureComponent>
|
||||
) : (
|
||||
<div key={i}></div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { Settings2, Copy, Trash2 } from "lucide-react";
|
||||
import { classNames } from "../../../../utils";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { useReactFlow } from "reactflow";
|
||||
import EditNodeModal from "../../../../modals/EditNodeModal";
|
||||
import ShadTooltip from "../../../../components/ShadTooltipComponent";
|
||||
|
||||
const NodeToolbarComponent = (props) => {
|
||||
const [nodeLength, setNodeLength] = useState(
|
||||
Object.keys(props.data.node.template).filter(
|
||||
(t) =>
|
||||
t.charAt(0) !== "_" &&
|
||||
props.data.node.template[t].show &&
|
||||
(props.data.node.template[t].type === "str" ||
|
||||
props.data.node.template[t].type === "bool" ||
|
||||
props.data.node.template[t].type === "float" ||
|
||||
props.data.node.template[t].type === "code" ||
|
||||
props.data.node.template[t].type === "prompt" ||
|
||||
props.data.node.template[t].type === "file" ||
|
||||
props.data.node.template[t].type === "Any" ||
|
||||
props.data.node.template[t].type === "int")
|
||||
).length
|
||||
);
|
||||
|
||||
const { setLastCopiedSelection, paste } = useContext(TabsContext);
|
||||
const reactFlowInstance = useReactFlow();
|
||||
return (
|
||||
<>
|
||||
<div className="h-10 w-26">
|
||||
<span className="isolate inline-flex rounded-md shadow-sm">
|
||||
<ShadTooltip delayDuration={1000} content="Delete" side="top">
|
||||
<button
|
||||
className="hover:dark:hover:bg-[#242f47] text-gray-700 transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 shadow-md relative inline-flex items-center rounded-l-md bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-muted focus:z-10"
|
||||
onClick={() => {
|
||||
props.deleteNode(props.data.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 dark:text-gray-300"></Trash2>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
|
||||
<ShadTooltip delayDuration={1000} content="Duplicate" side="top">
|
||||
<button
|
||||
className={classNames(
|
||||
nodeLength > 0
|
||||
? "hover:dark:hover:bg-[#242f47] text-gray-700 transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 shadow-md relative -ml-px inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-muted focus:z-10"
|
||||
: "hover:dark:hover:bg-[#242f47] text-gray-700 transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 shadow-md relative -ml-px inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-muted focus:z-10 rounded-r-md"
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
// console.log(reactFlowInstance.getNode(props.data.id));
|
||||
paste(
|
||||
{
|
||||
nodes: [reactFlowInstance.getNode(props.data.id)],
|
||||
edges: [],
|
||||
},
|
||||
{
|
||||
x: 50,
|
||||
y: 10,
|
||||
paneX: reactFlowInstance.getNode(props.data.id).position.x,
|
||||
paneY: reactFlowInstance.getNode(props.data.id).position.y,
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Copy className="w-4 h-4 dark:text-gray-300"></Copy>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
|
||||
{nodeLength > 0 && (
|
||||
<ShadTooltip delayDuration={1000} content="Edit" side="top">
|
||||
<button
|
||||
className="hover:dark:hover:bg-[#242f47] text-gray-700 transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 shadow-md relative -ml-px inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-muted focus:z-10 rounded-r-md"
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
props.openPopUp(<EditNodeModal data={props.data} />);
|
||||
}}
|
||||
>
|
||||
<Settings2 className="w-4 h-4 dark:text-gray-300"></Settings2>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
|
||||
{/*
|
||||
<Menu as="div" className="relative inline-block text-left z-100">
|
||||
<button className="hover:dark:hover:bg-[#242f47] text-gray-700 transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300 shadow-md relative -ml-px inline-flex items-center bg-white px-2 py-2 ring-1 ring-inset ring-gray-300 hover:bg-muted focus:z-10 rounded-r-md">
|
||||
<div>
|
||||
<Menu.Button className="flex items-center">
|
||||
<EllipsisVerticalIcon
|
||||
className="w-5 h-5 dark:text-gray-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Menu.Button>
|
||||
</div>
|
||||
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-100"
|
||||
enterFrom="transform opacity-0 scale-95"
|
||||
enterTo="transform opacity-100 scale-100"
|
||||
leave="transition ease-in duration-75"
|
||||
leaveFrom="transform opacity-100 scale-100"
|
||||
leaveTo="transform opacity-0 scale-95"
|
||||
>
|
||||
<Menu.Items className="absolute z-40 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none top-[28px]">
|
||||
<div className="py-1">
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
props.openPopUp(
|
||||
<EditNodeModal data={props.data} />
|
||||
);
|
||||
}}
|
||||
className={classNames(
|
||||
active
|
||||
? "bg-muted text-gray-900"
|
||||
: "text-gray-700",
|
||||
"w-full group flex items-center px-4 py-2 text-sm"
|
||||
)}
|
||||
>
|
||||
<Settings
|
||||
className="mr-3 h-5 w-5 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
{({ active }) => (
|
||||
<button
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
console.log(
|
||||
reactFlowInstance.getNode(props.data.id)
|
||||
);
|
||||
paste(
|
||||
{
|
||||
nodes: [
|
||||
reactFlowInstance.getNode(props.data.id),
|
||||
],
|
||||
edges: [],
|
||||
},
|
||||
{
|
||||
x: 50,
|
||||
y: 10,
|
||||
paneX: reactFlowInstance.getNode(props.data.id)
|
||||
.position.x,
|
||||
paneY: reactFlowInstance.getNode(props.data.id)
|
||||
.position.y,
|
||||
}
|
||||
);
|
||||
}}
|
||||
className={classNames(
|
||||
active
|
||||
? "bg-muted text-gray-900"
|
||||
: "text-gray-700",
|
||||
"w-full group flex items-center px-4 py-2 text-sm"
|
||||
)}
|
||||
>
|
||||
<DocumentDuplicateIcon
|
||||
className="mr-3 h-5 w-5 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Duplicate
|
||||
</button>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
</button>
|
||||
</Menu> */}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NodeToolbarComponent;
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { useContext, useState } from "react";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { FlowType } from "../../../../types/flow";
|
||||
|
||||
import _ from "lodash";
|
||||
|
||||
export default function TabComponent({
|
||||
selected,
|
||||
flow,
|
||||
onClick,
|
||||
}: {
|
||||
flow: FlowType;
|
||||
selected: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
const { removeFlow, updateFlow, flows } = useContext(TabsContext);
|
||||
const [isRename, setIsRename] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<>
|
||||
{flow ? (
|
||||
!selected ? (
|
||||
<div
|
||||
className="dark:text-white flex justify-between select-none truncate w-44 items-center px-4 my-1.5 border-x border-x-gray-300 dark:border-x-gray-600 -ml-px"
|
||||
onClick={onClick}
|
||||
>
|
||||
<span className="w-32 truncate text-left">{flow.name}</span>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeFlow(flow.id);
|
||||
}}
|
||||
>
|
||||
<XMarkIcon className="h-4 hover:bg-white dark:hover:bg-gray-600 rounded-full" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white dark:text-white dark:bg-gray-700/60 flex select-none justify-between w-44 items-center border border-b-0 border-gray-300 dark:border-gray-600 px-4 py-1 rounded-t-xl -ml-px">
|
||||
{isRename ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28"
|
||||
onBlur={() => {
|
||||
setIsRename(false);
|
||||
if (value !== "") {
|
||||
let newFlow = _.cloneDeep(flow);
|
||||
newFlow.name = value;
|
||||
updateFlow(newFlow);
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="text-left w-32 truncate"
|
||||
onDoubleClick={() => {
|
||||
setIsRename(true);
|
||||
setValue(flow.name);
|
||||
}}
|
||||
>
|
||||
{flow.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
removeFlow(flow.id);
|
||||
}}
|
||||
>
|
||||
{flows.length > 1 && (
|
||||
<XMarkIcon className="h-4 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-full" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="h-full py-1.5 flex justify-center items-center">
|
||||
<button
|
||||
className="px-3 flex items-center h-full pb-0.5 pt-0.5 border-x-gray-300 dark:border-x-gray-600 dark:text-white -ml-px"
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusIcon className="h-5 rounded-full hover:bg-white dark:hover:bg-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { ReactFlowProvider } from "reactflow";
|
||||
import TabComponent from "../tabComponent";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import FlowPage from "../..";
|
||||
import { darkContext } from "../../../../contexts/darkContext";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpTrayIcon,
|
||||
BellIcon,
|
||||
CodeBracketSquareIcon,
|
||||
MoonIcon,
|
||||
SunIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||
import AlertDropdown from "../../../../alerts/alertDropDown";
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
import ImportModal from "../../../../modals/importModal";
|
||||
import ExportModal from "../../../../modals/exportModal";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import ApiModal from "../../../../modals/ApiModal";
|
||||
|
||||
export default function TabsManagerComponent() {
|
||||
const { flows, addFlow, tabIndex, setTabIndex, uploadFlow, downloadFlow } =
|
||||
useContext(TabsContext);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
const { templates } = useContext(typesContext);
|
||||
const AlertWidth = 384;
|
||||
const { dark, setDark } = useContext(darkContext);
|
||||
const { notificationCenter, setNotificationCenter } =
|
||||
useContext(alertContext);
|
||||
useEffect(() => {
|
||||
//create the first flow
|
||||
if (flows.length === 0 && Object.keys(templates).length > 0) {
|
||||
addFlow();
|
||||
}
|
||||
}, [addFlow, flows.length, templates]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="w-full flex pr-2 flex-row text-center items-center bg-gray-100 dark:bg-gray-800 px-2">
|
||||
{flows.map((flow, index) => {
|
||||
return (
|
||||
<TabComponent
|
||||
onClick={() => setTabIndex(index)}
|
||||
selected={index === tabIndex}
|
||||
key={index}
|
||||
flow={flow}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<TabComponent
|
||||
onClick={() => {
|
||||
addFlow();
|
||||
}}
|
||||
selected={false}
|
||||
flow={null}
|
||||
/>
|
||||
<div className="ml-auto mr-2 flex gap-3">
|
||||
<button
|
||||
onClick={() =>
|
||||
openPopUp(<ApiModal flowName={flows[tabIndex].name} />)
|
||||
}
|
||||
className="flex items-center gap-1 pr-2 border-gray-400 border-r text-sm text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
|
||||
>
|
||||
Code <CodeBracketSquareIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openPopUp(<ImportModal />)}
|
||||
className="flex items-center gap-1 pr-2 border-gray-400 border-r text-sm text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
|
||||
>
|
||||
Import <ArrowUpTrayIcon className="w-5 h-5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => openPopUp(<ExportModal />)}
|
||||
className="flex items-center gap-1 pr-2 text-sm text-gray-600 border-gray-400 border-r hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
|
||||
>
|
||||
Export <ArrowDownTrayIcon className="h-5 w-5" />
|
||||
</button>
|
||||
<button
|
||||
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200"
|
||||
onClick={() => {
|
||||
setDark(!dark);
|
||||
}}
|
||||
>
|
||||
{dark ? (
|
||||
<SunIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<MoonIcon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="text-gray-600 hover:text-gray-500 dark:text-gray-300 dark:hover:text-gray-200 relative"
|
||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
setNotificationCenter(false);
|
||||
const top = (event.target as Element).getBoundingClientRect().top;
|
||||
const left = (event.target as Element).getBoundingClientRect()
|
||||
.left;
|
||||
openPopUp(
|
||||
<>
|
||||
<div
|
||||
className="z-10 absolute"
|
||||
style={{ top: top + 34, left: left - AlertWidth }}
|
||||
>
|
||||
<AlertDropdown />
|
||||
</div>
|
||||
<div className="h-screen w-screen fixed top-0 left-0"></div>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
>
|
||||
{notificationCenter && (
|
||||
<div className="absolute w-1.5 h-1.5 rounded-full bg-red-600 right-[3px]"></div>
|
||||
)}
|
||||
<BellIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<ReactFlowProvider>
|
||||
{flows[tabIndex] ? (
|
||||
<FlowPage flow={flows[tabIndex]}></FlowPage>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,360 +1,39 @@
|
|||
import { useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
addEdge,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useReactFlow,
|
||||
updateEdge,
|
||||
EdgeChange,
|
||||
Connection,
|
||||
Edge,
|
||||
useKeyPress,
|
||||
NodeDragHandler,
|
||||
OnEdgesDelete,
|
||||
OnNodesDelete,
|
||||
SelectionDragHandler,
|
||||
useOnViewportChange,
|
||||
OnSelectionChangeParams,
|
||||
OnNodesChange,
|
||||
} from "reactflow";
|
||||
import _ from "lodash";
|
||||
import { locationContext } from "../../contexts/locationContext";
|
||||
import ExtraSidebar from "./components/extraSidebarComponent";
|
||||
import Chat from "../../components/chatComponent";
|
||||
import GenericNode from "../../CustomNodes/GenericNode";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import Page from "./components/PageComponent";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import ConnectionLineComponent from "./components/ConnectionLineComponent";
|
||||
import { FlowType, NodeType } from "../../types/flow";
|
||||
import { APIClassType } from "../../types/api";
|
||||
import { isValidConnection } from "../../utils";
|
||||
import useUndoRedo from "./hooks/useUndoRedo";
|
||||
|
||||
const nodeTypes = {
|
||||
genericNode: GenericNode,
|
||||
};
|
||||
|
||||
export default function FlowPage({ flow }: { flow: FlowType }) {
|
||||
let { updateFlow, disableCopyPaste, addFlow, getNodeId, paste } =
|
||||
useContext(TabsContext);
|
||||
const { types, reactFlowInstance, setReactFlowInstance, templates } =
|
||||
useContext(typesContext);
|
||||
const reactFlowWrapper = useRef(null);
|
||||
|
||||
const { undo, redo, canUndo, canRedo, takeSnapshot } = useUndoRedo();
|
||||
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 });
|
||||
const [lastSelection, setLastSelection] =
|
||||
useState<OnSelectionChangeParams>(null);
|
||||
|
||||
const [lastCopiedSelection, setLastCopiedSelection] = useState(null);
|
||||
import { useParams } from "react-router-dom";
|
||||
import { getVersion } from "../../controllers/API";
|
||||
|
||||
export default function FlowPage() {
|
||||
const { flows, tabId, setTabId } = useContext(TabsContext);
|
||||
const { id } = useParams();
|
||||
useEffect(() => {
|
||||
// this effect is used to attach the global event handlers
|
||||
setTabId(id);
|
||||
}, [id]);
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "c" &&
|
||||
lastSelection &&
|
||||
!disableCopyPaste
|
||||
) {
|
||||
event.preventDefault();
|
||||
setLastCopiedSelection(_.cloneDeep(lastSelection));
|
||||
}
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "v" &&
|
||||
lastCopiedSelection &&
|
||||
!disableCopyPaste
|
||||
) {
|
||||
event.preventDefault();
|
||||
let bounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
paste(lastCopiedSelection, {
|
||||
x: position.x - bounds.left,
|
||||
y: position.y - bounds.top,
|
||||
});
|
||||
}
|
||||
if (
|
||||
(event.ctrlKey || event.metaKey) &&
|
||||
event.key === "g" &&
|
||||
lastSelection
|
||||
) {
|
||||
event.preventDefault();
|
||||
// addFlow(newFlow, false);
|
||||
}
|
||||
};
|
||||
const handleMouseMove = (event) => {
|
||||
setPosition({ x: event.clientX, y: event.clientY });
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
};
|
||||
}, [position, lastCopiedSelection, lastSelection]);
|
||||
|
||||
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
|
||||
|
||||
const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||
flow.data?.nodes ?? []
|
||||
);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
||||
flow.data?.edges ?? []
|
||||
);
|
||||
const { setViewport } = useReactFlow();
|
||||
const edgeUpdateSuccessful = useRef(true);
|
||||
// Initialize state variable for the version
|
||||
const [version, setVersion] = useState("");
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance && flow) {
|
||||
flow.data = reactFlowInstance.toObject();
|
||||
updateFlow(flow);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodes, edges]);
|
||||
//update flow when tabs change
|
||||
useEffect(() => {
|
||||
setNodes(flow?.data?.nodes ?? []);
|
||||
setEdges(flow?.data?.edges ?? []);
|
||||
if (reactFlowInstance) {
|
||||
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
|
||||
}
|
||||
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
|
||||
//set extra sidebar
|
||||
useEffect(() => {
|
||||
setExtraComponent(<ExtraSidebar />);
|
||||
setExtraNavigation({ title: "Components" });
|
||||
}, [setExtraComponent, setExtraNavigation]);
|
||||
|
||||
const onEdgesChangeMod = useCallback(
|
||||
(s: EdgeChange[]) => {
|
||||
onEdgesChange(s);
|
||||
setNodes((x) => {
|
||||
let newX = _.cloneDeep(x);
|
||||
return newX;
|
||||
});
|
||||
},
|
||||
[onEdgesChange, setNodes]
|
||||
);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params: Connection) => {
|
||||
takeSnapshot();
|
||||
setEdges((eds) =>
|
||||
addEdge(
|
||||
{
|
||||
...params,
|
||||
style: { stroke: "inherit" },
|
||||
className:
|
||||
params.targetHandle.split("|")[0] === "Text"
|
||||
? "stroke-gray-800 dark:stroke-gray-300"
|
||||
: "stroke-gray-900 dark:stroke-gray-200",
|
||||
animated: params.targetHandle.split("|")[0] === "Text",
|
||||
},
|
||||
eds
|
||||
)
|
||||
);
|
||||
setNodes((x) => {
|
||||
let newX = _.cloneDeep(x);
|
||||
return newX;
|
||||
});
|
||||
},
|
||||
[setEdges, setNodes, takeSnapshot]
|
||||
);
|
||||
|
||||
const onNodeDragStart: NodeDragHandler = useCallback(() => {
|
||||
// 👇 make dragging a node undoable
|
||||
takeSnapshot();
|
||||
// 👉 you can place your event handlers here
|
||||
}, [takeSnapshot]);
|
||||
|
||||
const onSelectionDragStart: SelectionDragHandler = useCallback(() => {
|
||||
// 👇 make dragging a selection undoable
|
||||
takeSnapshot();
|
||||
}, [takeSnapshot]);
|
||||
|
||||
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
|
||||
// 👇 make deleting edges undoable
|
||||
takeSnapshot();
|
||||
}, [takeSnapshot]);
|
||||
|
||||
const onDragOver = useCallback((event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
getVersion().then((data) => {
|
||||
setVersion(data.version);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event: React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
takeSnapshot();
|
||||
|
||||
// Get the current bounds of the ReactFlow wrapper element
|
||||
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
|
||||
// Extract the data from the drag event and parse it as a JSON object
|
||||
let data: { type: string; node?: APIClassType } = JSON.parse(
|
||||
event.dataTransfer.getData("json")
|
||||
);
|
||||
|
||||
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
|
||||
// Calculate the position where the node should be created
|
||||
const position = reactFlowInstance.project({
|
||||
x: event.clientX - reactflowBounds.left,
|
||||
y: event.clientY - reactflowBounds.top,
|
||||
});
|
||||
|
||||
// Generate a unique node ID
|
||||
let newId = getNodeId();
|
||||
let newNode: NodeType;
|
||||
|
||||
if (data.type !== "groupNode") {
|
||||
// Create a new node object
|
||||
newNode = {
|
||||
id: newId,
|
||||
type: "genericNode",
|
||||
position,
|
||||
data: {
|
||||
...data,
|
||||
id: newId,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// Create a new node object
|
||||
newNode = {
|
||||
id: newId,
|
||||
type: "genericNode",
|
||||
position,
|
||||
data: {
|
||||
...data,
|
||||
id: newId,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Add the new node to the list of nodes in state
|
||||
}
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
},
|
||||
// Specify dependencies for useCallback
|
||||
[getNodeId, reactFlowInstance, setErrorData, setNodes, takeSnapshot]
|
||||
);
|
||||
|
||||
const onDelete = useCallback(
|
||||
(mynodes) => {
|
||||
takeSnapshot();
|
||||
setEdges(
|
||||
edges.filter(
|
||||
(ns) => !mynodes.some((n) => ns.source === n.id || ns.target === n.id)
|
||||
)
|
||||
);
|
||||
},
|
||||
[takeSnapshot, edges, setEdges]
|
||||
);
|
||||
|
||||
const onEdgeUpdateStart = useCallback(() => {
|
||||
edgeUpdateSuccessful.current = false;
|
||||
}, []);
|
||||
|
||||
const onEdgeUpdate = useCallback(
|
||||
(oldEdge: Edge, newConnection: Connection) => {
|
||||
if (isValidConnection(newConnection, reactFlowInstance)) {
|
||||
edgeUpdateSuccessful.current = true;
|
||||
setEdges((els) => updateEdge(oldEdge, newConnection, els));
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const onEdgeUpdateEnd = useCallback((_, edge) => {
|
||||
if (!edgeUpdateSuccessful.current) {
|
||||
setEdges((eds) => eds.filter((e) => e.id !== edge.id));
|
||||
}
|
||||
|
||||
edgeUpdateSuccessful.current = true;
|
||||
}, []);
|
||||
|
||||
const [selectionEnded, setSelectionEnded] = useState(false);
|
||||
|
||||
const onSelectionEnd = useCallback(() => {
|
||||
setSelectionEnded(true);
|
||||
}, []);
|
||||
const onSelectionStart = useCallback(() => {
|
||||
setSelectionEnded(false);
|
||||
}, []);
|
||||
|
||||
// Workaround to show the menu only after the selection has ended.
|
||||
useEffect(() => {
|
||||
if (selectionEnded && lastSelection && lastSelection.nodes.length > 1) {
|
||||
setSelectionMenuVisible(true);
|
||||
} else {
|
||||
setSelectionMenuVisible(false);
|
||||
}
|
||||
}, [selectionEnded, lastSelection]);
|
||||
|
||||
const onSelectionChange = useCallback((flow) => {
|
||||
setLastSelection(flow);
|
||||
}, []);
|
||||
|
||||
const { setDisableCopyPaste } = useContext(TabsContext);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full" ref={reactFlowWrapper}>
|
||||
{Object.keys(templates).length > 0 && Object.keys(types).length > 0 ? (
|
||||
<>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
onMove={() => {
|
||||
updateFlow({ ...flow, data: reactFlowInstance.toObject() });
|
||||
}}
|
||||
edges={edges}
|
||||
onPaneClick={() => {
|
||||
setDisableCopyPaste(false);
|
||||
}}
|
||||
onPaneMouseLeave={() => {
|
||||
setDisableCopyPaste(true);
|
||||
}}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChangeMod}
|
||||
onConnect={onConnect}
|
||||
disableKeyboardA11y={true}
|
||||
onLoad={setReactFlowInstance}
|
||||
onInit={setReactFlowInstance}
|
||||
nodeTypes={nodeTypes}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onSelectionDragStart={onSelectionDragStart}
|
||||
onSelectionEnd={onSelectionEnd}
|
||||
onSelectionStart={onSelectionStart}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
connectionLineComponent={ConnectionLineComponent}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onNodesDelete={onDelete}
|
||||
onSelectionChange={onSelectionChange}
|
||||
nodesDraggable={!disableCopyPaste}
|
||||
panOnDrag={!disableCopyPaste}
|
||||
zoomOnDoubleClick={!disableCopyPaste}
|
||||
selectNodesOnDrag={false}
|
||||
className="theme-attribution"
|
||||
>
|
||||
<Background className="dark:bg-gray-900" />
|
||||
<Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600"></Controls>
|
||||
</ReactFlow>
|
||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance} />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
{flows.length > 0 &&
|
||||
tabId !== "" &&
|
||||
flows.findIndex((flow) => flow.id === tabId) !== -1 && (
|
||||
<Page flow={flows.find((flow) => flow.id === tabId)} />
|
||||
)}
|
||||
<a
|
||||
target={"_blank"}
|
||||
href="https://logspace.ai/"
|
||||
className="absolute left-7 bottom-2 flex h-6 cursor-pointer flex-col items-center justify-start overflow-hidden rounded-lg bg-foreground px-2 text-center font-sans text-xs tracking-wide text-secondary transition-all duration-500 ease-in-out hover:h-12"
|
||||
>
|
||||
{version && <div className="mt-1">⛓️ LangFlow v{version}</div>}
|
||||
<div className={version ? "mt-2" : "mt-1"}>Created by Logspace</div>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
83
src/frontend/src/pages/MainPage/index.tsx
Normal file
83
src/frontend/src/pages/MainPage/index.tsx
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { useContext, useEffect } from "react";
|
||||
import { Download, Upload, Plus, Home, ExternalLink } from "lucide-react";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { CardComponent } from "../../components/cardComponent";
|
||||
import { USER_PROJECTS_HEADER } from "../../constants";
|
||||
export default function HomePage() {
|
||||
const { flows, setTabId, downloadFlows, uploadFlows, addFlow, removeFlow } =
|
||||
useContext(TabsContext);
|
||||
useEffect(() => {
|
||||
setTabId("");
|
||||
}, []);
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="w-full h-full flex overflow-auto flex-col bg-muted px-16">
|
||||
<div className="w-full flex justify-between py-12 pb-2 px-6">
|
||||
<span className="text-2xl flex items-center justify-center gap-2 font-semibold">
|
||||
<Home className="w-6" />
|
||||
{USER_PROJECTS_HEADER}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
downloadFlows();
|
||||
}}
|
||||
>
|
||||
<Download className="w-4 mr-2" />
|
||||
Download Collection
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
uploadFlows();
|
||||
}}
|
||||
>
|
||||
<Upload className="w-4 mr-2" />
|
||||
Upload Collection
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
addFlow(null, true).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus className="w-4 mr-2" />
|
||||
New Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<span className="flex pb-14 px-6 text-muted-foreground w-[60%]">
|
||||
Manage your personal projects. Download or upload your collection.
|
||||
</span>
|
||||
<div className="w-full p-4 grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{flows.map((flow, idx) => (
|
||||
<CardComponent
|
||||
key={idx}
|
||||
flow={flow}
|
||||
id={flow.id}
|
||||
button={
|
||||
<Link to={"/flow/" + flow.id}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="whitespace-nowrap "
|
||||
>
|
||||
<ExternalLink className="w-4 mr-2" />
|
||||
Edit Flow
|
||||
</Button>
|
||||
</Link>
|
||||
}
|
||||
onDelete={() => {
|
||||
removeFlow(flow.id);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
19
src/frontend/src/routes.tsx
Normal file
19
src/frontend/src/routes.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Route, Routes } from "react-router-dom";
|
||||
import HomePage from "./pages/MainPage";
|
||||
import FlowPage from "./pages/FlowPage";
|
||||
import CommunityPage from "./pages/CommunityPage";
|
||||
|
||||
const Router = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/community" element={<CommunityPage />} />
|
||||
<Route path="/flow/:id/">
|
||||
<Route path="" element={<FlowPage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<HomePage />} />
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default Router;
|
||||
|
|
@ -38,3 +38,11 @@ export type errorsTypeAPI = {
|
|||
imports: { errors: Array<string> };
|
||||
};
|
||||
export type PromptTypeAPI = { input_variables: Array<string> };
|
||||
|
||||
export type BuildStatusTypeAPI = {
|
||||
built: boolean;
|
||||
};
|
||||
|
||||
export type InitTypeAPI = {
|
||||
flowId: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
import {
|
||||
ForwardRefExoticComponent,
|
||||
ReactElement,
|
||||
ReactFragment,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { ForwardRefExoticComponent, ReactElement, ReactNode } from "react";
|
||||
import { NodeDataType } from "../flow/index";
|
||||
import { typesContextType } from "../typesContext";
|
||||
export type InputComponentType = {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
password: boolean;
|
||||
disableCopyPaste?: boolean;
|
||||
editNode?: boolean;
|
||||
onChangePass?: (value: boolean | boolean) => void;
|
||||
showPass?: boolean;
|
||||
};
|
||||
export type ToggleComponentType = {
|
||||
enabled: boolean;
|
||||
setEnabled: (state: boolean) => void;
|
||||
disabled: boolean;
|
||||
size: "small" | "medium" | "large";
|
||||
};
|
||||
export type DropDownComponentType = {
|
||||
value: string;
|
||||
options: string[];
|
||||
onSelect: (value: string) => void;
|
||||
editNode?: boolean;
|
||||
numberOfOptions?: number;
|
||||
};
|
||||
export type ParameterComponentType = {
|
||||
data: NodeDataType;
|
||||
|
|
@ -32,17 +34,20 @@ export type ParameterComponentType = {
|
|||
required?: boolean;
|
||||
name?: string;
|
||||
tooltipTitle: string;
|
||||
dataContext?: typesContextType;
|
||||
};
|
||||
export type InputListComponentType = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
disabled: boolean;
|
||||
editNode?: boolean;
|
||||
};
|
||||
|
||||
export type TextAreaComponentType = {
|
||||
disabled: boolean;
|
||||
onChange: (value: string[] | string) => void;
|
||||
value: string;
|
||||
editNode?: boolean;
|
||||
};
|
||||
|
||||
export type FileComponentType = {
|
||||
|
|
@ -52,10 +57,12 @@ export type FileComponentType = {
|
|||
suffixes: Array<string>;
|
||||
fileTypes: Array<string>;
|
||||
onFileChange: (value: string) => void;
|
||||
editNode?: boolean;
|
||||
};
|
||||
|
||||
export type DisclosureComponentType = {
|
||||
children: ReactNode;
|
||||
openDisc: boolean;
|
||||
button: {
|
||||
title: string;
|
||||
Icon: ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
|
||||
|
|
@ -71,6 +78,7 @@ export type FloatComponentType = {
|
|||
disabled?: boolean;
|
||||
disableCopyPaste?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
editNode?: boolean;
|
||||
};
|
||||
|
||||
export type TooltipComponentType = {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ export type FlowType = {
|
|||
id: string;
|
||||
data: ReactFlowJsonObject;
|
||||
description: string;
|
||||
style?: FlowStyleType;
|
||||
};
|
||||
export type NodeType = {
|
||||
id: string;
|
||||
|
|
@ -20,3 +21,10 @@ export type NodeDataType = {
|
|||
id: string;
|
||||
value: any;
|
||||
};
|
||||
// FlowStyleType is the type of the style object that is used to style the
|
||||
// Flow card with an emoji and a color.
|
||||
export type FlowStyleType = {
|
||||
emoji: string;
|
||||
color: string;
|
||||
flow_id: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,30 +1,37 @@
|
|||
import { Dispatch, SetStateAction } from "react";
|
||||
import { FlowType } from "../flow";
|
||||
|
||||
export type TabsContextType = {
|
||||
saveFlow: (flow: FlowType) => Promise<void>;
|
||||
save: () => void;
|
||||
tabIndex: number;
|
||||
setTabIndex: (index: number) => void;
|
||||
tabId: string;
|
||||
setTabId: (index: string) => void;
|
||||
flows: Array<FlowType>;
|
||||
removeFlow: (id: string) => void;
|
||||
addFlow: (flowData?: FlowType, newFlow?: boolean) => void;
|
||||
addFlow: (flowData?: FlowType, newProject?: boolean) => Promise<String>;
|
||||
updateFlow: (newFlow: FlowType) => void;
|
||||
incrementNodeId: () => string;
|
||||
downloadFlow: (flow: FlowType) => void;
|
||||
downloadFlows: () => void;
|
||||
uploadFlows: () => void;
|
||||
uploadFlow: (newFlow?: boolean) => void;
|
||||
hardReset: () => void;
|
||||
//disable CopyPaste
|
||||
disableCopyPaste: boolean;
|
||||
setDisableCopyPaste: (value: boolean) => void;
|
||||
getNodeId: () => string;
|
||||
getNodeId: (nodeType: string) => string;
|
||||
tabsState: TabsState;
|
||||
setTabsState: Dispatch<SetStateAction<TabsState>>;
|
||||
paste: (
|
||||
selection: { nodes: any; edges: any },
|
||||
position: { x: number; y: number }
|
||||
position: { x: number; y: number; paneX?: number; paneY?: number }
|
||||
) => void;
|
||||
lastCopiedSelection: { nodes: any; edges: any };
|
||||
setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void;
|
||||
};
|
||||
|
||||
export type LangFlowState = {
|
||||
tabIndex: number;
|
||||
flows: FlowType[];
|
||||
id: string;
|
||||
nodeId: number;
|
||||
export type TabsState = {
|
||||
[key: string]: {
|
||||
isPending: boolean;
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import {
|
|||
WrenchScrewdriverIcon,
|
||||
WrenchIcon,
|
||||
ComputerDesktopIcon,
|
||||
Bars3CenterLeftIcon,
|
||||
GiftIcon,
|
||||
PaperClipIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
|
|
@ -15,14 +14,15 @@ import {
|
|||
ScissorsIcon,
|
||||
CircleStackIcon,
|
||||
Squares2X2Icon,
|
||||
Bars3CenterLeftIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Connection, Edge, Node, ReactFlowInstance, addEdge } from "reactflow";
|
||||
import { Connection, Edge, Node, ReactFlowInstance } from "reactflow";
|
||||
import { FlowType, NodeType } from "./types/flow";
|
||||
import { APITemplateType, TemplateVariableType } from "./types/api";
|
||||
import { APITemplateType } from "./types/api";
|
||||
import _ from "lodash";
|
||||
import { ChromaIcon } from "./icons/ChromaIcon";
|
||||
import { AnthropicIcon } from "./icons/Anthropic";
|
||||
import { AirbyteIcon } from "./icons/Airbyte";
|
||||
import { AzIcon } from "./icons/AzLogo";
|
||||
import { BingIcon } from "./icons/Bing";
|
||||
import { CohereIcon } from "./icons/Cohere";
|
||||
import { EvernoteIcon } from "./icons/Evernote";
|
||||
|
|
@ -36,23 +36,18 @@ import { MetaIcon } from "./icons/Meta";
|
|||
import { MidjorneyIcon } from "./icons/Midjorney";
|
||||
import { NotionIcon } from "./icons/Notion";
|
||||
import { OpenAiIcon } from "./icons/OpenAi";
|
||||
import { PowerPointIcon } from "./icons/PowerPoint";
|
||||
import { QDrantIcon } from "./icons/QDrant";
|
||||
import { ReadTheDocsIcon } from "./icons/ReadTheDocs";
|
||||
import { SearxIcon } from "./icons/Searx";
|
||||
import { SlackIcon } from "./icons/Slack";
|
||||
import { WeaviateIcon } from "./icons/Weaviate";
|
||||
import { WikipediaIcon } from "./icons/Wikipedia";
|
||||
import { WolframIcon } from "./icons/Wolfram";
|
||||
import { WordIcon } from "./icons/Word";
|
||||
import { SerperIcon } from "./icons/Serper";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import clsx, { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ADJECTIVES, DESCRIPTIONS, NOUNS } from "./constants";
|
||||
|
||||
export function classNames(...classes: Array<string>) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export const limitScrollFieldsModal = 7;
|
||||
export const limitScrollFieldsModal = 10;
|
||||
|
||||
export enum TypeModal {
|
||||
TEXT = 1,
|
||||
|
|
@ -153,6 +148,8 @@ export const nodeIcons: {
|
|||
AirbyteJSONLoader: AirbyteIcon,
|
||||
// SerpAPIWrapper: SerperIcon,
|
||||
// AZLyricsLoader: AzIcon,
|
||||
Anthropic: AnthropicIcon,
|
||||
ChatAnthropic: AnthropicIcon,
|
||||
BingSearchAPIWrapper: BingIcon,
|
||||
BingSearchRun: BingIcon,
|
||||
Cohere: CohereIcon,
|
||||
|
|
@ -178,7 +175,7 @@ export const nodeIcons: {
|
|||
// ReadTheDocsLoader: ReadTheDocsIcon, // does not work
|
||||
Searx: SearxIcon,
|
||||
SlackDirectoryLoader: SlackIcon,
|
||||
// Weaviate: WeaviateIcon,
|
||||
// Weaviate: WeaviateIcon, // does not work
|
||||
// WikipediaAPIWrapper: WikipediaIcon,
|
||||
// WolframAlphaQueryRun: WolframIcon,
|
||||
// WolframAlphaAPIWrapper: WolframIcon,
|
||||
|
|
@ -201,6 +198,23 @@ export const nodeIcons: {
|
|||
unknown: QuestionMarkCircleIcon,
|
||||
};
|
||||
|
||||
export const gradients = [
|
||||
"bg-gradient-to-br from-gray-800 via-rose-700 to-violet-900",
|
||||
"bg-gradient-to-br from-green-200 via-green-300 to-blue-500",
|
||||
"bg-gradient-to-br from-yellow-200 via-yellow-400 to-yellow-700",
|
||||
"bg-gradient-to-br from-green-200 via-green-400 to-purple-700",
|
||||
"bg-gradient-to-br from-blue-100 via-blue-300 to-blue-500",
|
||||
"bg-gradient-to-br from-purple-400 to-yellow-400",
|
||||
"bg-gradient-to-br from-red-800 via-yellow-600 to-yellow-500",
|
||||
"bg-gradient-to-br from-blue-300 via-green-200 to-yellow-300",
|
||||
"bg-gradient-to-br from-blue-700 via-blue-800 to-gray-900",
|
||||
"bg-gradient-to-br from-green-300 to-purple-400",
|
||||
"bg-gradient-to-br from-yellow-200 via-pink-200 to-pink-400",
|
||||
"bg-gradient-to-br from-green-500 to-green-700",
|
||||
"bg-gradient-to-br from-rose-400 via-fuchsia-500 to-indigo-500",
|
||||
"bg-gradient-to-br from-sky-400 to-blue-500",
|
||||
];
|
||||
|
||||
export const bgColors = {
|
||||
white: "bg-white",
|
||||
red: "bg-red-100",
|
||||
|
|
@ -343,6 +357,10 @@ export function measureTextWidth(text: string, fontSize: number) {
|
|||
return wordWidth;
|
||||
}
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function measureTextHeight(
|
||||
text: string,
|
||||
width: number,
|
||||
|
|
@ -473,9 +491,10 @@ export function isValidConnection(
|
|||
|
||||
export function removeApiKeys(flow: FlowType): FlowType {
|
||||
let cleanFLow = _.cloneDeep(flow);
|
||||
console.log(cleanFLow);
|
||||
cleanFLow.data.nodes.forEach((node) => {
|
||||
for (const key in node.data.node.template) {
|
||||
if (key.includes("api")) {
|
||||
if (node.data.node.template[key].password) {
|
||||
node.data.node.template[key].value = "";
|
||||
}
|
||||
}
|
||||
|
|
@ -526,6 +545,13 @@ export function updateTemplate(
|
|||
if (objectToUpdate[key] && objectToUpdate[key].value) {
|
||||
clonedObject[key].value = objectToUpdate[key].value;
|
||||
}
|
||||
if (
|
||||
objectToUpdate[key] &&
|
||||
objectToUpdate[key].advanced !== null &&
|
||||
objectToUpdate[key].advanced !== undefined
|
||||
) {
|
||||
clonedObject[key].advanced = objectToUpdate[key].advanced;
|
||||
}
|
||||
}
|
||||
return clonedObject;
|
||||
}
|
||||
|
|
@ -601,9 +627,9 @@ export function checkUpperWords(str: string) {
|
|||
export function updateIds(newFlow, getNodeId) {
|
||||
let idsMap = {};
|
||||
|
||||
newFlow.nodes.forEach((n) => {
|
||||
newFlow.nodes.forEach((n: NodeType) => {
|
||||
// Generate a unique node ID
|
||||
let newId = getNodeId();
|
||||
let newId = getNodeId(n.data.type);
|
||||
idsMap[n.id] = newId;
|
||||
n.id = newId;
|
||||
n.data.id = newId;
|
||||
|
|
@ -632,3 +658,157 @@ export function updateIds(newFlow, getNodeId) {
|
|||
e.targetHandle;
|
||||
});
|
||||
}
|
||||
|
||||
export function groupByFamily(data, baseClasses) {
|
||||
let arrOfParent: string[] = [];
|
||||
let arrOfType: { family: string; type: string }[] = [];
|
||||
|
||||
Object.keys(data).map((d) => {
|
||||
Object.keys(data[d]).map((n) => {
|
||||
if (
|
||||
data[d][n].base_classes.some((r) => baseClasses.split("\n").includes(r))
|
||||
) {
|
||||
arrOfParent.push(d);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let uniq = arrOfParent.filter(
|
||||
(item, index) => arrOfParent.indexOf(item) === index
|
||||
);
|
||||
|
||||
Object.keys(data).map((d) => {
|
||||
Object.keys(data[d]).map((n) => {
|
||||
baseClasses.split("\n").forEach((tol) => {
|
||||
data[d][n].base_classes.forEach((data) => {
|
||||
if (tol == data) {
|
||||
arrOfType.push({
|
||||
family: d,
|
||||
type: data,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let groupedBy = arrOfType.filter((object, index, self) => {
|
||||
const foundIndex = self.findIndex(
|
||||
(o) => o.family === object.family && o.type === object.type
|
||||
);
|
||||
return foundIndex === index;
|
||||
});
|
||||
|
||||
let groupedObj = groupedBy.reduce((result, item) => {
|
||||
const existingGroup = result.find((group) => group.family === item.family);
|
||||
|
||||
if (existingGroup) {
|
||||
existingGroup.type += `, ${item.type}`;
|
||||
} else {
|
||||
result.push({ family: item.family, type: item.type });
|
||||
}
|
||||
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
return groupedObj;
|
||||
}
|
||||
|
||||
export function buildTweaks(flow) {
|
||||
return flow.data.nodes.reduce((acc, node) => {
|
||||
acc[node.data.id] = {};
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
export function validateNode(
|
||||
n: NodeType,
|
||||
reactFlowInstance: ReactFlowInstance
|
||||
): Array<string> {
|
||||
if (!n.data?.node?.template || !Object.keys(n.data.node.template)) {
|
||||
return [
|
||||
"We've noticed a potential issue with a node in the flow. Please review it and, if necessary, submit a bug report with your exported flow file. Thank you for your help!",
|
||||
];
|
||||
}
|
||||
|
||||
const {
|
||||
type,
|
||||
node: { template },
|
||||
} = n.data;
|
||||
|
||||
return Object.keys(template).reduce(
|
||||
(errors: Array<string>, t) =>
|
||||
errors.concat(
|
||||
template[t].required &&
|
||||
template[t].show &&
|
||||
(template[t].value === undefined ||
|
||||
template[t].value === null ||
|
||||
template[t].value === "") &&
|
||||
!reactFlowInstance
|
||||
.getEdges()
|
||||
.some(
|
||||
(e) =>
|
||||
e.targetHandle.split("|")[1] === t &&
|
||||
e.targetHandle.split("|")[2] === n.id
|
||||
)
|
||||
? [
|
||||
`${type} is missing ${
|
||||
template.display_name
|
||||
? template.display_name
|
||||
: toNormalCase(template[t].name)
|
||||
}.`,
|
||||
]
|
||||
: []
|
||||
),
|
||||
[] as string[]
|
||||
);
|
||||
}
|
||||
|
||||
export function validateNodes(reactFlowInstance: ReactFlowInstance) {
|
||||
if (reactFlowInstance.getNodes().length === 0) {
|
||||
return [
|
||||
"No nodes found in the flow. Please add at least one node to the flow.",
|
||||
];
|
||||
}
|
||||
return reactFlowInstance
|
||||
.getNodes()
|
||||
.flatMap((n: NodeType) => validateNode(n, reactFlowInstance));
|
||||
}
|
||||
|
||||
export function getRandomElement<T>(array: T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
export function getRandomDescription(): string {
|
||||
return getRandomElement(DESCRIPTIONS);
|
||||
}
|
||||
|
||||
export 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,158 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
const { fontFamily } = require("tailwindcss/defaultTheme");
|
||||
|
||||
import plugin from "tailwindcss/plugin";
|
||||
|
||||
module.exports = {
|
||||
darkMode: ["class"],
|
||||
content: ["app/**/*.{ts,tsx}", "components/**/*.{ts,tsx}"],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: `var(--radius)`,
|
||||
md: `calc(var(--radius) - 2px)`,
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,tsx,jsx}"],
|
||||
darkMode: "class",
|
||||
important: true,
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
border: "hsl(var(--border))",
|
||||
input: "hsl(var(--input))",
|
||||
ring: "hsl(var(--ring))",
|
||||
background: "hsl(var(--background))",
|
||||
foreground: "hsl(var(--foreground))",
|
||||
primary: {
|
||||
DEFAULT: "hsl(var(--primary))",
|
||||
foreground: "hsl(var(--primary-foreground))",
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: "hsl(var(--secondary))",
|
||||
foreground: "hsl(var(--secondary-foreground))",
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: "hsl(var(--destructive))",
|
||||
foreground: "hsl(var(--destructive-foreground))",
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: "hsl(var(--muted))",
|
||||
foreground: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "hsl(var(--accent))",
|
||||
foreground: "hsl(var(--accent-foreground))",
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: "hsl(var(--popover))",
|
||||
foreground: "hsl(var(--popover-foreground))",
|
||||
},
|
||||
card: {
|
||||
DEFAULT: "hsl(var(--card))",
|
||||
foreground: "hsl(var(--card-foreground))",
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: `var(--radius)`,
|
||||
md: `calc(var(--radius) - 2px)`,
|
||||
sm: "calc(var(--radius) - 4px)",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["var(--font-sans)", ...fontFamily.sans],
|
||||
},
|
||||
keyframes: {
|
||||
"accordion-down": {
|
||||
from: { height: 0 },
|
||||
to: { height: "var(--radix-accordion-content-height)" },
|
||||
},
|
||||
"accordion-up": {
|
||||
from: { height: "var(--radix-accordion-content-height)" },
|
||||
to: { height: 0 },
|
||||
},
|
||||
pulseGreen: {
|
||||
"0%": { boxShadow: "0 0 0 0 rgba(72, 187, 120, 0.7)" },
|
||||
"100%": { boxShadow: "0 0 0 10px rgba(72, 187, 120, 0)" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
"accordion-down": "accordion-down 0.2s ease-out",
|
||||
"accordion-up": "accordion-up 0.2s ease-out",
|
||||
"pulse-green": "pulseGreen 1s linear",
|
||||
"spin-once": "spin 1s linear 0.7",
|
||||
},
|
||||
borderColor: {
|
||||
"red-outline": "rgba(255, 0, 0, 0.8)",
|
||||
"green-outline": "rgba(72, 187, 120, 0.7)",
|
||||
|
|
@ -14,20 +161,10 @@ module.exports = {
|
|||
"red-outline": "0 0 5px rgba(255, 0, 0, 0.5)",
|
||||
"green-outline": "0 0 5px rgba(72, 187, 120, 0.7)",
|
||||
},
|
||||
|
||||
animation: {
|
||||
"pulse-green": "pulseGreen 1s linear",
|
||||
'spin-once': 'spin 1s linear 0.7'
|
||||
},
|
||||
keyframes: {
|
||||
pulseGreen: {
|
||||
"0%": { boxShadow: "0 0 0 0 rgba(72, 187, 120, 0.7)" },
|
||||
"100%": { boxShadow: "0 0 0 10px rgba(72, 187, 120, 0)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
require("tailwindcss-animate"),
|
||||
require("@tailwindcss/forms")({
|
||||
strategy: "class", // only generate classes
|
||||
}),
|
||||
|
|
@ -44,10 +181,19 @@ module.exports = {
|
|||
},
|
||||
},
|
||||
".truncate-multiline": {
|
||||
"display": "-webkit-box",
|
||||
"-webkit-line-clamp": "3", /* Change this number to the number of lines you want to show */
|
||||
display: "-webkit-box",
|
||||
"-webkit-line-clamp":
|
||||
"3" /* Change this number to the number of lines you want to show */,
|
||||
"-webkit-box-orient": "vertical",
|
||||
"overflow": "hidden",
|
||||
overflow: "hidden",
|
||||
"text-overflow": "ellipsis",
|
||||
},
|
||||
".truncate-doubleline": {
|
||||
display: "-webkit-box",
|
||||
"-webkit-line-clamp":
|
||||
"2" /* Change this number to the number of lines you want to show */,
|
||||
"-webkit-box-orient": "vertical",
|
||||
overflow: "hidden",
|
||||
"text-overflow": "ellipsis",
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
|
|
@ -21,7 +17,5 @@
|
|||
"jsx": "react-jsx",
|
||||
"noImplicitAny": false
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,7 @@
|
|||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
import svgr from "vite-plugin-svgr";
|
||||
const apiRoutes = [
|
||||
"/all",
|
||||
"^/upload/*",
|
||||
"/predict",
|
||||
"^/validate/*",
|
||||
"^/chat/*",
|
||||
"/version",
|
||||
"/health",
|
||||
];
|
||||
const apiRoutes = ["^/api/v1/", "/health"];
|
||||
|
||||
// Use environment variable to determine the target.
|
||||
const target = process.env.VITE_PROXY_TARGET || "http://127.0.0.1:7860";
|
||||
|
|
@ -23,7 +15,6 @@ const proxyTargets = apiRoutes.reduce((proxyObj, route) => {
|
|||
};
|
||||
return proxyObj;
|
||||
}, {});
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
build: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue