Add new files and update existing files

This commit is contained in:
Gabriel Luiz Freitas Almeida 2023-12-12 15:33:10 -03:00
commit 2f8cb0d776
135 changed files with 13454 additions and 3508 deletions

View file

@ -22,5 +22,5 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
/test-results/
/playwright-report/
/playwright-report/*/
/playwright/.cache/

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -111,13 +111,13 @@
"@types/uuid": "^9.0.2",
"@vitejs/plugin-react-swc": "^3.3.2",
"autoprefixer": "^10.4.15",
"daisyui": "^3.6.3",
"daisyui": "^4.0.4",
"postcss": "^8.4.29",
"prettier": "^2.8.8",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.3.0",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
"vite": "^4.4.9"
"vite": "^4.5.1"
}
}

View file

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

View file

@ -20,11 +20,13 @@ export default defineConfig({
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
reporter: [
["html", { open: "never", outputFolder: "playwright-report/test-results" }],
],
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
// baseURL: "http://127.0.0.1:3000",
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
@ -69,9 +71,16 @@ export default defineConfig({
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
// webServer: [
// {
// command: "npm run backend",
// reuseExistingServer: !process.env.CI,
// timeout: 120 * 1000,
// },
// {
// command: "npm run start",
// url: "http://127.0.0.1:3000",
// reuseExistingServer: !process.env.CI,
// },
// ],
});

78
src/frontend/run-tests.sh Executable file
View file

@ -0,0 +1,78 @@
#!/bin/bash
# Default value for the --ui flag
ui=false
# Parse command-line arguments
while [[ $# -gt 0 ]]; do
key="$1"
case $key in
--ui)
ui=true
shift
;;
*)
echo "Unknown option: $key"
exit 1
;;
esac
shift
done
# Function to forcibly terminate a process by port
terminate_process_by_port() {
port="$1"
echo "Terminating process on port: $port"
fuser -k -n tcp "$port" # Forcefully terminate processes using the specified port
echo "Process terminated."
}
# Trap signals to ensure cleanup on script termination
trap 'terminate_process_by_port 7860; terminate_process_by_port 3000' EXIT
# install playwright if there is not installed yet
npx playwright install
# Navigate to the project root directory (where the Makefile is located)
cd ../../
# Start the frontend using 'make frontend' in the background
make frontend &
# Give some time for the frontend to start (adjust sleep duration as needed)
sleep 10
# Navigate to the test directory
cd src/frontend
# Run frontend only Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/onlyFront npx playwright test tests/onlyFront --project=chromium
fi
# Navigate back to the project root directory
cd ../../
# Start the backend using 'make backend' in the background
make backend &
# Give some time for the backend to start (adjust sleep duration as needed)
sleep 25
# Navigate back to the test directory
cd src/frontend
# Run Playwright tests with or without UI based on the --ui flag
if [ "$ui" = true ]; then
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --ui --project=chromium
else
PLAYWRIGHT_HTML_REPORT=playwright-report/e2e npx playwright test tests/end-to-end --project=chromium
fi
npx playwright show-report
# After the tests are finished, you can add cleanup or teardown logic here if needed
# The trap will automatically terminate processes by port on script exit

View file

@ -16,8 +16,8 @@ import {
FETCH_ERROR_MESSAGE,
} from "./constants/constants";
import { alertContext } from "./contexts/alertContext";
import { FlowsContext } from "./contexts/flowsContext";
import { locationContext } from "./contexts/locationContext";
import { TabsContext } from "./contexts/tabsContext";
import { typesContext } from "./contexts/typesContext";
import Router from "./routes";
@ -30,7 +30,7 @@ export default function App() {
setShowSideBar(true);
setIsStackedOpen(true);
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
const { hardReset } = useContext(TabsContext);
const { hardReset } = useContext(FlowsContext);
const {
errorData,
@ -42,9 +42,7 @@ export default function App() {
successData,
successOpen,
setSuccessOpen,
setErrorData,
loading,
setLoading,
} = useContext(alertContext);
const navigate = useNavigate();
const { fetchError } = useContext(typesContext);

View file

@ -22,16 +22,25 @@ import PromptAreaComponent from "../../../../components/promptComponent";
import TextAreaComponent from "../../../../components/textAreaComponent";
import ToggleShadComponent from "../../../../components/toggleShadComponent";
import { Button } from "../../../../components/ui/button";
import { TOOLTIP_EMPTY } from "../../../../constants/constants";
import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { ParameterComponentType } from "../../../../types/components";
import { TabsState } from "../../../../types/tabs";
import {
LANGFLOW_SUPPORTED_TYPES,
TOOLTIP_EMPTY,
} from "../../../../constants/constants";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { postCustomComponentUpdate } from "../../../../controllers/API";
import { APIClassType } from "../../../../types/api";
import { ParameterComponentType } from "../../../../types/components";
import { NodeDataType } from "../../../../types/flow";
import {
cleanEdges,
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
isValidConnection,
scapedJSONStringfy,
} from "../../../../utils/reactflowUtils";
import {
nodeColors,
@ -44,7 +53,6 @@ export default function ParameterComponent({
left,
id,
data,
setData,
tooltipTitle,
title,
color,
@ -53,14 +61,20 @@ export default function ParameterComponent({
required = false,
optionalHandle = null,
info = "",
proxy,
showNode,
index = "",
}: ParameterComponentType): JSX.Element {
const ref = useRef<HTMLDivElement>(null);
const refHtml = useRef<HTMLDivElement & ReactNode>(null);
const infoHtml = useRef<HTMLDivElement & ReactNode>(null);
const { setErrorData, modalContextOpen } = useContext(alertContext);
const updateNodeInternals = useUpdateNodeInternals();
const [position, setPosition] = useState(0);
const { setTabsState, tabId, flows } = useContext(TabsContext);
const { setTabsState, tabId, flows, tabsState, updateFlow } =
useContext(FlowsContext);
const [dataRef, setDataRef] = useState(data);
const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null;
@ -78,41 +92,114 @@ export default function ParameterComponent({
const groupedEdge = useRef(null);
useEffect(() => {
setDataRef(data);
}, [modalContextOpen]);
const { reactFlowInstance, setFilterEdge } = useContext(typesContext);
let disabled =
reactFlowInstance?.getEdges().some((edge) => edge.targetHandle === id) ??
false;
reactFlowInstance
?.getEdges()
.some(
(edge) =>
edge.targetHandle ===
scapedJSONStringfy(proxy ? { ...id, proxy } : id)
) ?? false;
const { data: myData } = useContext(typesContext);
const { takeSnapshot } = useContext(undoRedoContext);
const handleUpdateValues = async (name: string, data: NodeDataType) => {
const code = data.node?.template["code"]?.value;
if (!code) {
console.error("Code not found in the template");
return;
}
try {
const res = await postCustomComponentUpdate(code, name);
if (res.status === 200 && data.node?.template) {
data.node!.template[name] = res.data.template[name];
}
} catch (err) {
setErrorData(err as { title: string; list?: Array<string> });
}
};
const handleOnNewValue = (
newValue: string | string[] | boolean | Object[]
): void => {
let newData = cloneDeep(data);
newData.node!.template[name].value = newValue;
setData(newData);
if (data.node!.template[name].value !== newValue) {
takeSnapshot();
}
data.node!.template[name].value = newValue;
updateNodeInternals(data.id);
setDataRef((old) => {
let newData = cloneDeep(old);
newData.node!.template[name].value = newValue;
return newData;
});
// Set state to pending
//@ts-ignore
setTabsState((prev: TabsState) => {
if (!prev[tabId]) {
return prev;
}
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
formKeysData: prev[tabId].formKeysData,
},
};
});
if (data.node!.template[name].value !== newValue) {
const tabs = cloneDeep(tabsState);
tabs[tabId].isPending = false;
tabs[tabId].formKeysData = tabsState[tabId].formKeysData;
setTabsState({
...tabs,
});
}
renderTooltips();
};
const handleNodeClass = (newNodeClass: APIClassType, code?: string): void => {
if (!data.node) return;
if (data.node!.template[name].value !== newNodeClass.template[name].value) {
takeSnapshot();
}
data.node! = {
...newNodeClass,
description: newNodeClass.description ?? data.node!.description,
display_name: newNodeClass.display_name ?? data.node!.display_name,
};
data.node!.template[name].value = code;
updateNodeInternals(data.id);
// Set state to pending
//@ts-ignore
if (data.node!.template[name].value !== code) {
const tabs = cloneDeep(tabsState);
tabs[tabId].isPending = false;
tabs[tabId].formKeysData = tabsState[tabId].formKeysData;
setTabsState({
...tabs,
});
}
renderTooltips();
let flow = flows.find((flow) => flow.id === tabId);
setTimeout(() => {
//timeout necessary because ReactFlow updates are not async
if (reactFlowInstance && flow && flow.data) {
cleanEdges({
flow: {
edges: flow.data!.edges,
nodes: flow.data!.nodes,
},
updateEdge: (edge) => {
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
});
updateFlow(flow);
}
}, 50);
};
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
useEffect(() => {
if (name === "openai_api_base") console.log(info);
// @ts-ignore
infoHtml.current = (
<div className="h-full w-full break-words">
@ -166,21 +253,40 @@ export default function ParameterComponent({
</div>
<span className="ps-2 text-xs text-foreground">
{nodeNames[item.family] ?? "Other"}{" "}
<span className="text-xs">
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
{item?.display_name && item?.display_name?.length > 0 ? (
<span className="text-xs">
{" "}
{item.display_name === "" ? "" : " - "}
{item.display_name.split(", ").length > 2
? item.display_name.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index ===
item.display_name.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.display_name}
</span>
) : (
<span className="text-xs">
{" "}
{item.type === "" ? "" : " - "}
{item.type.split(", ").length > 2
? item.type.split(", ").map((el, index) => (
<React.Fragment key={el + index}>
<span>
{index === item.type.split(", ").length - 1
? el
: (el += `, `)}
</span>
</React.Fragment>
))
: item.type}
</span>
)}
</span>
</span>
</div>
@ -197,45 +303,43 @@ export default function ParameterComponent({
}, [tooltipTitle, flow]);
return !showNode ? (
left &&
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!optionalHandle ? (
left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
) : (
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={id}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
top: position,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
</ShadTooltip>
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
<div className="flex">
<ShadTooltip
styleClasses={"tooltip-fixed-width custom-scroll nowheel"}
delayDuration={0}
content={refHtml.current}
side={left ? "left" : "right"}
>
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
className={classNames(
left ? "my-12 -ml-0.5 " : " my-12 -mr-0.5 ",
"h-3 w-3 rounded-full border-2 bg-background"
)}
style={{
borderColor: color,
top: position,
}}
onClick={() => {
setFilterEdge(groupedEdge.current);
}}
></Handle>
</ShadTooltip>
</div>
</Button>
)
) : (
<div
@ -250,7 +354,13 @@ export default function ParameterComponent({
(info !== "" ? " flex items-center" : "")
}
>
{title}
{proxy ? (
<ShadTooltip content={<span>{proxy.id}</span>}>
<span>{title}</span>
</ShadTooltip>
) : (
title
)}
<span className="text-status-red">{required ? " *" : ""}</span>
<div className="">
{info !== "" && (
@ -266,17 +376,7 @@ export default function ParameterComponent({
)}
</div>
</div>
{left &&
(type === "str" ||
type === "bool" ||
type === "float" ||
type === "code" ||
type === "prompt" ||
type === "file" ||
type === "int" ||
type === "dict" ||
type === "NestedDict") &&
!optionalHandle ? (
{left && LANGFLOW_SUPPORTED_TYPES.has(type ?? "") && !optionalHandle ? (
<></>
) : (
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
@ -290,7 +390,11 @@ export default function ParameterComponent({
<Handle
type={left ? "target" : "source"}
position={left ? Position.Left : Position.Right}
id={id}
id={
proxy
? scapedJSONStringfy({ ...id, proxy })
: scapedJSONStringfy(id)
}
isValidConnection={(connection) =>
isValidConnection(connection, reactFlowInstance!)
}
@ -331,9 +435,12 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"textarea-" + index}
data-testid={"textarea-" + index}
/>
) : (
<InputComponent
id={"input-" + index}
disabled={disabled}
password={data.node?.template[name].password ?? false}
value={data.node?.template[name].value ?? ""}
@ -344,6 +451,7 @@ export default function ParameterComponent({
) : left === true && type === "bool" ? (
<div className="mt-2 w-full">
<ToggleShadComponent
id={"toggle-" + index}
disabled={disabled}
enabled={data.node?.template[name].value ?? false}
setEnabled={(isEnabled) => {
@ -357,30 +465,49 @@ export default function ParameterComponent({
<FloatComponent
disabled={disabled}
value={data.node?.template[name].value ?? ""}
rangeSpec={data.node?.template[name].rangeSpec}
onChange={handleOnNewValue}
/>
</div>
) : left === true &&
type === "str" &&
data.node?.template[name].options ? (
<div className="mt-2 w-full">
<Dropdown
options={data.node.template[name].options}
onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"}
></Dropdown>
// TODO: Improve CSS
<div className="mt-2 flex w-full items-center">
<div className="w-5/6 flex-grow">
<Dropdown
options={data.node.template[name].options}
onSelect={handleOnNewValue}
value={data.node.template[name].value ?? "Choose an option"}
id={"dropdown-" + index}
/>
</div>
{data.node?.template[name].refresh && (
<button
className="extra-side-bar-buttons ml-2 mt-1 w-1/6"
onClick={() => {
handleUpdateValues(name, data);
}}
>
<IconComponent name="RefreshCcw" />
</button>
)}
</div>
) : left === true && type === "code" ? (
<div className="mt-2 w-full">
<CodeAreaComponent
readonly={
data.node?.flow && data.node.template[name].dynamic
? true
: false
}
dynamic={data.node?.template[name].dynamic ?? false}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
}}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"code-input-" + index}
/>
</div>
) : left === true && type === "file" ? (
@ -390,7 +517,6 @@ export default function ParameterComponent({
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
fileTypes={data.node?.template[name].fileTypes}
suffixes={data.node?.template[name].suffixes}
onFileChange={(filePath: string) => {
data.node!.template[name].file_path = filePath;
}}
@ -402,24 +528,21 @@ export default function ParameterComponent({
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={handleOnNewValue}
id={"int-input-" + index}
/>
</div>
) : left === true && type === "prompt" ? (
<div className="mt-2 w-full">
<PromptAreaComponent
readonly={data.node?.flow ? true : false}
field_name={name}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
const clone = cloneDeep(data);
clone.node = nodeClass;
setData(clone);
}}
setNodeClass={handleNodeClass}
nodeClass={data.node}
disabled={disabled}
value={data.node?.template[name].value ?? ""}
onChange={(e) => {
handleOnNewValue(e);
}}
onChange={handleOnNewValue}
id={"prompt-input-" + index}
data-testid={"prompt-input-" + index}
/>
</div>
) : left === true && type === "NestedDict" ? (
@ -439,6 +562,7 @@ export default function ParameterComponent({
data.node!.template[name].value = newValue;
handleOnNewValue(newValue);
}}
id="div-dict-input"
/>
</div>
) : left === true && type === "dict" ? (
@ -447,10 +571,10 @@ export default function ParameterComponent({
disabled={disabled}
editNode={false}
value={
data.node!.template[name].value?.length === 0 ||
!data.node!.template[name].value
dataRef.node!.template[name].value?.length === 0 ||
!dataRef.node!.template[name].value
? [{ "": "" }]
: convertObjToArray(data.node!.template[name].value)
: convertObjToArray(dataRef.node!.template[name].value)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {

View file

@ -1,38 +1,54 @@
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import Tooltip from "../../components/TooltipComponent";
import IconComponent from "../../components/genericIconComponent";
import InputComponent from "../../components/inputComponent";
import { Textarea } from "../../components/ui/textarea";
import { priorityFields } from "../../constants/constants";
import { useSSE } from "../../contexts/SSEContext";
import { TabsContext } from "../../contexts/tabsContext";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { undoRedoContext } from "../../contexts/undoRedoContext";
import NodeToolbarComponent from "../../pages/FlowPage/components/nodeToolbarComponent";
import { validationStatusType } from "../../types/components";
import { NodeDataType } from "../../types/flow";
import { cleanEdges } from "../../utils/reactflowUtils";
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
import { classNames, toTitleCase } from "../../utils/utils";
import { classNames, cn, getFieldTitle } from "../../utils/utils";
import ParameterComponent from "./components/parameterComponent";
export default function GenericNode({
data: olddata,
data,
xPos,
yPos,
selected,
}: {
data: NodeDataType;
selected: boolean;
xPos: number;
yPos: number;
}): JSX.Element {
const [data, setData] = useState(olddata);
const { updateFlow, flows, tabId } = useContext(TabsContext);
const { updateFlow, flows, tabId, saveCurrentFlow } =
useContext(FlowsContext);
const updateNodeInternals = useUpdateNodeInternals();
const { types, deleteNode, reactFlowInstance, setFilterEdge, getFilterEdge } =
useContext(typesContext);
const name = nodeIconsLucide[data.type] ? data.type : types[data.type];
const [inputName, setInputName] = useState(false);
const [nodeName, setNodeName] = useState(data.node!.display_name);
const [inputDescription, setInputDescription] = useState(false);
const [nodeDescription, setNodeDescription] = useState(
data.node?.description!
);
const [validationStatus, setValidationStatus] =
useState<validationStatusType | null>(null);
const [showNode, setShowNode] = useState<boolean>(true);
const [handles, setHandles] = useState<boolean[] | []>([]);
let numberOfInputs: boolean[] = [];
const { modalContextOpen } = useContext(alertContext);
const { takeSnapshot } = useContext(undoRedoContext);
function countHandles(): void {
numberOfInputs = Object.keys(data.node!.template)
@ -65,37 +81,18 @@ export default function GenericNode({
useEffect(() => {
countHandles();
}, []);
}, [data, data.node]);
// State for outline color
const { sseData, isBuilding } = useSSE();
useEffect(() => {
olddata.node = data.node;
let myFlow = flows.find((flow) => flow.id === tabId);
if (reactFlowInstance && myFlow) {
let flow = cloneDeep(myFlow);
flow.data = reactFlowInstance.toObject();
cleanEdges({
flow: {
edges: flow.data.edges,
nodes: flow.data.nodes,
},
updateEdge: (edge) => {
flow.data!.edges = edge;
reactFlowInstance.setEdges(edge);
updateNodeInternals(data.id);
},
});
updateFlow(flow);
}
countHandles();
}, [data]);
useEffect(() => {
setTimeout(() => {
updateNodeInternals(data.id);
}, 300);
}, [showNode]);
setNodeDescription(data.node!.description);
}, [data.node!.description]);
useEffect(() => {
setNodeName(data.node!.display_name);
}, [data.node!.display_name]);
// New useEffect to watch for changes in sseData and update validation status
useEffect(() => {
@ -107,14 +104,25 @@ export default function GenericNode({
setValidationStatus(null);
}
}, [sseData, data.id]);
const showNode = data.showNode ?? true;
const nameEditable = data.node?.flow || data.type === "CustomComponent";
return (
<>
<NodeToolbar>
<NodeToolbarComponent
position={{ x: xPos, y: yPos }}
data={data}
setData={setData}
deleteNode={deleteNode}
setShowNode={setShowNode}
deleteNode={(id) => {
takeSnapshot();
deleteNode(id);
saveCurrentFlow();
}}
setShowNode={(show: boolean) => {
data.showNode = show;
}}
numberOfHandles={handles}
showNode={showNode}
></NodeToolbarComponent>
@ -123,10 +131,7 @@ export default function GenericNode({
<div
className={classNames(
selected ? "border border-ring" : "border",
" transition-transform ",
showNode
? " w-96 scale-100 transform rounded-lg duration-500 ease-in-out "
: " transform-width w-26 h-26 scale-90 transform rounded-full duration-500 ",
showNode ? " w-96 rounded-lg" : " w-26 h-26 rounded-full",
"generic-node-div"
)}
>
@ -137,6 +142,7 @@ export default function GenericNode({
)}
<div>
<div
data-testid={"div-generic-node"}
className={
"generic-node-div-title " +
(!showNode
@ -151,20 +157,55 @@ export default function GenericNode({
}
>
<IconComponent
name={name}
name={data.node?.flow ? "group_components" : name}
className={
"generic-node-icon " +
(!showNode && "absolute inset-x-6 h-12 w-12")
(!showNode ? "absolute inset-x-6 h-12 w-12" : "")
}
iconColor={`${nodeColors[types[data.type]]}`}
/>
{showNode && (
<div className="generic-node-tooltip-div">
<ShadTooltip content={data.node?.display_name}>
<div className="generic-node-tooltip-div text-primary">
{data.node?.display_name}
{nameEditable && inputName ? (
<div>
<InputComponent
onBlur={() => {
setInputName(false);
if (nodeName.trim() !== "") {
setNodeName(nodeName);
data.node!.display_name = nodeName;
updateNodeInternals(data.id);
} else {
setNodeName(data.node!.display_name);
}
}}
value={nodeName}
onChange={setNodeName}
password={false}
blurOnEnter={true}
/>
</div>
</ShadTooltip>
) : (
<ShadTooltip content={data.node?.display_name}>
<div
className="flex"
onDoubleClick={() => {
setInputName(true);
takeSnapshot();
}}
>
<div className="generic-node-tooltip-div pr-2 text-primary">
{data.node?.display_name}
</div>
{nameEditable && (
<IconComponent
name="Pencil"
className="h-4 w-4 text-ring"
/>
)}
</div>
</ShadTooltip>
)}
</div>
)}
</div>
@ -178,18 +219,16 @@ export default function GenericNode({
data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced && (
<ParameterComponent
key={
(data.node!.template[
templateField
].input_types?.join(";") ??
data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
index={idx.toString()}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
setData={setData}
color={
nodeColors[
types[data.node?.template[templateField].type!]
@ -199,15 +238,10 @@ export default function GenericNode({
] ??
nodeColors.unknown
}
title={
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node?.template[templateField].name
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
title={getFieldTitle(
data.node?.template!,
templateField
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
@ -217,33 +251,32 @@ export default function GenericNode({
data.node?.template[templateField].type
}
required={
data.node?.template[templateField].required
}
id={
(data.node?.template[
templateField
].input_types?.join(";") ??
data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
data.node!.template[templateField].required
}
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
)
)}
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node?.output_types &&
@ -252,9 +285,11 @@ export default function GenericNode({
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={[data.type, data.id, ...data.node!.base_classes].join(
"|"
)}
id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
@ -282,9 +317,9 @@ export default function GenericNode({
) : (
<div className="max-h-96 overflow-auto">
{typeof validationStatus.params === "string"
? validationStatus.params
? `Duration: ${validationStatus.duration}\n${validationStatus.params}`
.split("\n")
.map((line: string, index: number) => (
.map((line, index) => (
<div key={index}>{line}</div>
))
: ""}
@ -329,54 +364,102 @@ export default function GenericNode({
<div
className={
showNode
? "generic-node-desc " +
(data.node?.description !== "" && showNode ? "py-5" : "pb-5")
? "overflow-hidden " +
(data.node?.description === "" && !nameEditable
? "pb-5"
: "py-5")
: ""
}
>
{data.node?.description !== "" && showNode && (
<div className="generic-node-desc-text">
{data.node?.description}
</div>
)}
<div className="generic-node-desc">
{showNode && nameEditable && inputDescription ? (
<Textarea
autoFocus
onBlur={() => {
setInputDescription(false);
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
updateNodeInternals(data.id);
}}
value={nodeDescription}
onChange={(e) => setNodeDescription(e.target.value)}
onKeyDown={(e) => {
handleKeyDown(e, nodeDescription, "");
if (
e.key === "Enter" &&
e.shiftKey === false &&
e.ctrlKey === false &&
e.altKey === false
) {
setInputDescription(false);
setNodeDescription(nodeDescription);
data.node!.description = nodeDescription;
updateNodeInternals(data.id);
}
}}
/>
) : (
<div
className={cn(
"generic-node-desc-text truncate-multiline word-break-break-word",
(data.node?.description === "" ||
!data.node?.description) &&
nameEditable
? "font-light italic"
: ""
)}
onDoubleClick={() => {
setInputDescription(true);
takeSnapshot();
}}
>
{(data.node?.description === "" || !data.node?.description) &&
nameEditable
? "Double Click to Edit Description"
: data.node?.description}
</div>
)}
</div>
<>
{Object.keys(data.node!.template)
.filter((templateField) => templateField.charAt(0) !== "_")
.sort((a, b) => {
if (priorityFields.has(a.toLowerCase())) {
return -1;
} else if (priorityFields.has(b.toLowerCase())) {
return 1;
} else {
return a.localeCompare(b);
}
})
.map((templateField: string, idx) => (
<div key={idx}>
{data.node!.template[templateField].show &&
!data.node!.template[templateField].advanced ? (
<ParameterComponent
key={
(data.node!.template[templateField].input_types?.join(
";"
) ?? data.node!.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
index={idx.toString()}
key={scapedJSONStringfy({
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
proxy: data.node!.template[templateField].proxy,
})}
data={data}
setData={setData}
color={
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors[
data.node?.template[templateField].type!
] ??
nodeColors[
types[data.node?.template[templateField].type!]
] ??
nodeColors.unknown
}
title={
data.node?.template[templateField].display_name
? data.node.template[templateField].display_name
: data.node?.template[templateField].name
? toTitleCase(
data.node.template[templateField].name
)
: toTitleCase(templateField)
}
title={getFieldTitle(
data.node?.template!,
templateField
)}
info={data.node?.template[templateField].info}
name={templateField}
tooltipTitle={
@ -384,21 +467,20 @@ export default function GenericNode({
"\n"
) ?? data.node?.template[templateField].type
}
required={data.node?.template[templateField].required}
id={
(data.node?.template[templateField].input_types?.join(
";"
) ?? data.node?.template[templateField].type) +
"|" +
templateField +
"|" +
data.id
}
required={data.node!.template[templateField].required}
id={{
inputTypes:
data.node!.template[templateField].input_types,
type: data.node!.template[templateField].type,
id: data.id,
fieldName: templateField,
}}
left={true}
type={data.node?.template[templateField].type}
optionalHandle={
data.node?.template[templateField].input_types
}
proxy={data.node?.template[templateField].proxy}
showNode={showNode}
/>
) : (
@ -414,22 +496,37 @@ export default function GenericNode({
>
{" "}
</div>
<ParameterComponent
key={[data.type, data.id, ...data.node!.base_classes].join("|")}
data={data}
setData={setData}
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
title={
data.node?.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|")
: 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}
showNode={showNode}
/>
{data.node!.base_classes.length > 0 && (
<ParameterComponent
key={scapedJSONStringfy({
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
})}
data={data}
color={
(data.node?.output_types &&
data.node.output_types.length > 0
? nodeColors[data.node.output_types[0]] ??
nodeColors[types[data.node.output_types[0]]]
: nodeColors[types[data.type]]) ?? nodeColors.unknown
}
title={
data.node?.output_types && data.node.output_types.length > 0
? data.node.output_types.join("|")
: data.type
}
tooltipTitle={data.node?.base_classes.join("\n")}
id={{
baseClasses: data.node!.base_classes,
id: data.id,
dataType: data.type,
}}
type={data.node?.base_classes.join("|")}
left={false}
showNode={showNode}
/>
)}
</>
</div>
)}

View file

@ -30,7 +30,7 @@ export default function AlertDropdown({
}}
>
<PopoverTrigger>{children}</PopoverTrigger>
<PopoverContent className="flex h-[500px] w-[500px] flex-col">
<PopoverContent className="nocopy nopan nodelete nodrag noundo flex h-[500px] w-[500px] flex-col">
<div className="text-md flex flex-row justify-between pl-3 font-medium text-foreground">
Notifications
<div className="flex gap-3 pr-3 ">

View file

@ -40,7 +40,7 @@ export default function ErrorAlert({
removeAlert(id);
}, 500);
}}
className="error-build-message"
className="error-build-message nocopy nopan nodelete nodrag noundo"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -36,7 +36,7 @@ export default function NoticeAlert({
setShow(false);
removeAlert(id);
}}
className="mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl"
className="nocopy nopan nodelete nodrag noundo mt-6 w-96 rounded-md bg-info-background p-4 shadow-xl"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -34,7 +34,7 @@ export default function SuccessAlert({
setShow(false);
removeAlert(id);
}}
className="success-alert"
className="success-alert nocopy nopan nodelete nodrag noundo"
>
<div className="flex">
<div className="flex-shrink-0">

View file

@ -21,6 +21,7 @@ export default function DropdownButton({
<DropdownMenu open={showOptions}>
<DropdownMenuTrigger asChild>
<Button
id="new-project-btn"
variant="primary"
className="relative pr-10"
onClick={(event) => {

View file

@ -1,31 +1,19 @@
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import React, { ChangeEvent, useState } from "react";
import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Textarea } from "../../components/ui/textarea";
import { readFlowsFromDatabase } from "../../controllers/API";
import { InputProps } from "../../types/components";
import { FlowType } from "../../types/flow";
import { cn } from "../../utils/utils";
export const EditFlowSettings: React.FC<InputProps> = ({
name,
invalidName,
setInvalidName,
invalidNameList,
description,
maxLength = 50,
flows,
tabId,
setName,
setDescription,
}: InputProps): JSX.Element => {
const [isMaxLength, setIsMaxLength] = useState(false);
const nameLists = useRef<string[]>([]);
useEffect(() => {
readFlowsFromDatabase().then((flows) => {
flows.forEach((flow: FlowType) => {
nameLists.current.push(flow.name);
});
});
}, []);
const handleNameChange = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
@ -34,63 +22,74 @@ export const EditFlowSettings: React.FC<InputProps> = ({
} else {
setIsMaxLength(false);
}
if (invalidName !== undefined) {
if (!nameLists.current.includes(value)) {
setInvalidName!(false);
} else {
setInvalidName!(true);
}
if (!nameLists.current.includes(value)) {
setInvalidName!(false);
} else {
setInvalidName!(true);
}
}
setName(value);
setName!(value);
};
const handleDescriptionChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
setDescription(event.target.value);
setDescription!(event.target.value);
};
//this function is necessary to select the text when double clicking, this was not working with the onFocus event
const handleFocus = (event) => event.target.select();
return (
<>
<Label>
<div className="edit-flow-arrangement">
<span className="font-medium">Name</span>{" "}
<span className="font-medium">Name{setName ? "" : ":"}</span>{" "}
{isMaxLength && (
<span className="edit-flow-span">Character limit reached</span>
)}
{invalidName && (
<span className="edit-flow-span">Name already in use</span>
)}
</div>
<Input
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
onChange={handleNameChange}
type="text"
name="name"
value={name ?? ""}
placeholder="File name"
id="name"
maxLength={maxLength}
/>
{setName ? (
<Input
className="nopan nodelete nodrag noundo nocopy mt-2 font-normal"
onChange={handleNameChange}
type="text"
name="name"
value={name ?? ""}
placeholder="Flow name"
id="name"
maxLength={maxLength}
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
/>
) : (
<span className="font-normal text-muted-foreground word-break-break-word">
{name}
</span>
)}
</Label>
<Label>
<div className="edit-flow-arrangement mt-3">
<span className="font-medium ">Description (optional)</span>
<span className="font-medium ">
Description{setDescription ? " (optional)" : ":"}
</span>
</div>
<Textarea
name="description"
id="description"
onChange={handleDescriptionChange}
value={description!}
placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal"
rows={3}
/>
{setDescription ? (
<Textarea
name="description"
id="description"
onChange={handleDescriptionChange}
value={description!}
placeholder="Flow description"
className="mt-2 max-h-[100px] font-normal"
rows={3}
onDoubleClickCapture={(event) => {
handleFocus(event);
}}
/>
) : (
<span
className={cn(
"font-normal text-muted-foreground word-break-break-word",
description === "" ? "font-light italic" : ""
)}
>
{description === "" ? "No description" : description}
</span>
)}
</Label>
</>
);

View file

@ -11,11 +11,12 @@ import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
export default function PaginatorComponent({
pageSize = 10,
pageSize = 12,
pageIndex = 1,
rowsCount = [10, 20, 50, 100],
rowsCount = [12, 24, 48, 96],
totalRowsCount = 0,
paginate,
storeComponent = false,
}: PaginatorComponentType) {
const [size, setPageSize] = useState(pageSize);
const [maxIndex, setMaxPageIndex] = useState(
@ -30,7 +31,13 @@ export default function PaginatorComponent({
<>
<div className="flex items-center justify-between px-2">
<div className="flex-1 text-sm text-muted-foreground"></div>
<div className="flex items-center space-x-6 lg:space-x-8">
<div
className={
storeComponent
? "flex items-center lg:space-x-8 "
: "flex items-center space-x-6 lg:space-x-8 "
}
>
<div className="flex items-center space-x-2">
<p className="text-sm font-medium">Rows per page</p>
<Select
@ -54,7 +61,8 @@ export default function PaginatorComponent({
</Select>
</div>
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
Page {pageIndex} of {maxIndex}
Page {pageIndex}
{!storeComponent && <> of {maxIndex}</>}
</div>
<div className="flex items-center space-x-2">
<Button

View file

@ -11,6 +11,7 @@ const SanitizedHTMLWrapper = ({
return (
<div
data-testid="edit-prompt-sanitized"
className={className}
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
suppressContentEditableWarning={suppressWarning}

View file

@ -1,8 +1,16 @@
import { useContext } from "react";
import { TabsContext } from "../../contexts/tabsContext";
import { cardComponentPropsType } from "../../types/components";
import { gradients } from "../../utils/styleUtils";
import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext";
import { getComponent, postLikeComponent } from "../../controllers/API";
import DeleteConfirmationModal from "../../modals/DeleteConfirmationModal";
import { storeComponent } from "../../types/store";
import cloneFLowWithParent from "../../utils/storeUtils";
import { cn } from "../../utils/utils";
import ShadTooltip from "../ShadTooltipComponent";
import IconComponent from "../genericIconComponent";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import {
Card,
CardDescription,
@ -11,48 +19,326 @@ import {
CardTitle,
} from "../ui/card";
export const CardComponent = ({
flow,
id,
onDelete,
export default function CollectionCardComponent({
data,
authorized = true,
disabled = false,
button,
}: cardComponentPropsType): JSX.Element => {
const { removeFlow } = useContext(TabsContext);
onDelete,
}: {
data: storeComponent;
authorized?: boolean;
disabled?: boolean;
button?: JSX.Element;
onDelete?: () => void;
}) {
const { addFlow } = useContext(FlowsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { setValidApiKey } = useContext(StoreContext);
const isStore = false;
const [loading, setLoading] = useState(false);
const [loadingLike, setLoadingLike] = useState(false);
const [liked_by_user, setLiked_by_user] = useState(
data?.liked_by_user ?? false
);
const [likes_count, setLikes_count] = useState(data?.liked_by_count ?? 0);
const [downloads_count, setDownloads_count] = useState(
data?.downloads_count ?? 0
);
const name = data.is_component ? "Component" : "Flow";
useEffect(() => {
if (data) {
setLiked_by_user(data?.liked_by_user ?? false);
setLikes_count(data?.liked_by_count ?? 0);
setDownloads_count(data?.downloads_count ?? 0);
}
}, [data, data.liked_by_count, data.liked_by_user, data.downloads_count]);
function handleInstall() {
const temp = downloads_count;
setDownloads_count((old) => Number(old) + 1);
setLoading(true);
getComponent(data.id)
.then((res) => {
const newFlow = cloneFLowWithParent(res, res.id, data.is_component);
addFlow(true, newFlow)
.then((id) => {
setSuccessData({
title: `${name} ${
isStore ? "Downloaded" : "Installed"
} Successfully.`,
});
setLoading(false);
})
.catch((error) => {
setLoading(false);
setErrorData({
title: `Error ${
isStore ? "downloading" : "installing"
} the ${name}`,
list: [error["response"]["data"]["detail"]],
});
});
})
.catch((err) => {
setLoading(false);
setErrorData({
title: `Error ${isStore ? "downloading" : "installing"} the ${name}`,
list: [err["response"]["data"]["detail"]],
});
setDownloads_count(temp);
});
}
function handleLike() {
setLoadingLike(true);
if (liked_by_user !== undefined || liked_by_user !== null) {
const temp = liked_by_user;
const tempNum = likes_count;
setLiked_by_user((prev) => !prev);
if (!temp) {
setLikes_count((prev) => Number(prev) + 1);
} else {
setLikes_count((prev) => Number(prev) - 1);
}
postLikeComponent(data.id)
.then((response) => {
setLoadingLike(false);
setLikes_count(response.data.likes_count);
setLiked_by_user(response.data.liked_by_user);
})
.catch((error) => {
setLoadingLike(false);
setLikes_count(tempNum);
setLiked_by_user(temp);
if (error.response.status === 403) {
setValidApiKey(false);
} else {
console.error(error);
setErrorData({
title: `Error liking ${name}.`,
list: [error["response"]["data"]["detail"]],
});
}
});
}
}
return (
<Card className="group">
<CardHeader>
<CardTitle className="card-component-title-display">
<span
className={
"card-component-image " +
gradients[parseInt(flow.id.slice(0, 12), 16) % gradients.length]
}
></span>
<span className="card-component-title-size">{flow.name}</span>
{onDelete && (
<button className="card-component-delete-button" onClick={onDelete}>
<Card
className={cn(
"group relative flex flex-col justify-between overflow-hidden transition-all hover:shadow-md",
disabled ? "pointer-events-none opacity-50" : ""
)}
>
<div>
<CardHeader>
<div>
<CardTitle className="flex w-full items-center justify-between gap-3 text-xl">
<IconComponent
name="Trash2"
className="card-component-delete-icon"
className={cn(
"flex-shrink-0",
data.is_component
? "mx-0.5 h-6 w-6 text-component-icon"
: "h-7 w-7 flex-shrink-0 text-flow-icon"
)}
name={data.is_component ? "ToyBrick" : "Group"}
/>
</button>
)}
</CardTitle>
<CardDescription className="card-component-desc">
<div className="card-component-desc-text">
{flow.description}
{/* {flow.description} */}
<ShadTooltip content={data.name}>
<div className="w-full truncate">{data.name}</div>
</ShadTooltip>
{data?.metadata !== undefined && (
<div className="flex gap-3">
{data.private && (
<ShadTooltip content="Private">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Lock" className="h-4 w-4" />
</span>
</ShadTooltip>
)}
{!data.is_component && (
<ShadTooltip content="Components">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="ToyBrick" className="h-4 w-4" />
{data?.metadata?.total ?? 0}
</span>
</ShadTooltip>
)}
<ShadTooltip content="Likes">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="Heart" className={cn("h-4 w-4 ")} />
{likes_count ?? 0}
</span>
</ShadTooltip>
<ShadTooltip content="Downloads">
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
<IconComponent name="DownloadCloud" className="h-4 w-4" />
{downloads_count ?? 0}
</span>
</ShadTooltip>
</div>
)}
{onDelete && data?.metadata === undefined && (
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<IconComponent
name="Trash2"
className="h-5 w-5 text-primary opacity-0 transition-all hover:text-destructive group-hover:opacity-100"
/>
</DeleteConfirmationModal>
)}
</CardTitle>
</div>
</CardDescription>
</CardHeader>
{data.user_created && data.user_created.username && (
<span className="text-sm text-primary">
by <b>{data.user_created.username}</b>
{data.last_tested_version && (
<>
{" "}
|{" "}
<span className="text-xs">
{" "}
v{data.last_tested_version}
</span>
</>
)}
</span>
)}
<CardDescription className="pb-2 pt-2">
<div className="truncate-doubleline">{data.description}</div>
</CardDescription>
</CardHeader>
</div>
<CardFooter>
<div className="card-component-footer-arrangement">
<div className="card-component-footer"></div>
{button && button}
<div className="flex w-full items-center justify-between gap-2">
<div className="flex w-full flex-wrap items-end justify-between gap-2">
<div className="flex w-full flex-1 flex-wrap gap-2">
{data.tags &&
data.tags.length > 0 &&
data.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
size="xq"
className="text-muted-foreground"
>
{tag.name}
</Badge>
))}
</div>
{data.liked_by_count != undefined && (
<div className="flex gap-0.5">
{onDelete && data?.metadata !== undefined ? (
<ShadTooltip
content={
authorized ? "Delete" : "Please review your API key."
}
>
<DeleteConfirmationModal
onConfirm={() => {
onDelete();
}}
>
<Button
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</DeleteConfirmationModal>
</ShadTooltip>
) : (
<ShadTooltip
content={
authorized ? "Like" : "Please review your API key."
}
>
<Button
disabled={loadingLike}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "")
}
onClick={() => {
if (!authorized) {
return;
}
handleLike();
}}
>
<IconComponent
name="Heart"
className={cn(
"h-5 w-5",
liked_by_user
? "fill-destructive stroke-destructive"
: "",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</ShadTooltip>
)}
<ShadTooltip
content={
authorized
? isStore
? "Download"
: "Install Locally"
: "Please review your API key."
}
>
<Button
disabled={loading}
variant="ghost"
size="xs"
className={
"whitespace-nowrap" +
(!authorized ? " cursor-not-allowed" : "") +
(!loading ? " p-0.5" : "")
}
onClick={() => {
if (loading || !authorized) {
return;
}
handleInstall();
}}
>
<IconComponent
name={loading ? "Loader2" : isStore ? "Download" : "Plus"}
className={cn(
loading ? "h-5 w-5 animate-spin" : "h-5 w-5",
!authorized ? " text-ring" : ""
)}
/>
</Button>
</ShadTooltip>
</div>
)}
{button && button}
</div>
</div>
</CardFooter>
</Card>
);
};
}

View file

@ -0,0 +1,63 @@
import { useState } from "react";
import IconComponent from "../../components/genericIconComponent";
export default function CardsWrapComponent({
onFileDrop,
children,
dragMessage,
}: {
onFileDrop?: (e: any) => void;
children: JSX.Element | JSX.Element[];
dragMessage?: string;
}) {
const [isDragging, setIsDragging] = useState(false);
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files") && onFileDrop) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((types) => types === "Files") && onFileDrop) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = (e) => {
e.preventDefault();
if (onFileDrop) setIsDragging(false);
};
const onDrop = (e) => {
e.preventDefault();
if (onFileDrop) onFileDrop(e);
setIsDragging(false);
};
return (
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={onDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
}
>
{isDragging ? (
<>
<IconComponent name="ArrowUpToLine" className="h-12 w-12 stroke-1" />
{dragMessage ? dragMessage : "Drop your file here"}
</>
) : (
children
)}
</div>
);
}

View file

@ -7,9 +7,9 @@ import { typesContext } from "../../../contexts/typesContext";
import { postBuildInit } from "../../../controllers/API";
import { FlowType } from "../../../types/flow";
import { TabsContext } from "../../../contexts/tabsContext";
import { FlowsContext } from "../../../contexts/flowsContext";
import { parsedDataType } from "../../../types/components";
import { TabsState } from "../../../types/tabs";
import { FlowsState } from "../../../types/tabs";
import { validateNodes } from "../../../utils/reactflowUtils";
import RadialProgressComponent from "../../RadialProgress";
import IconComponent from "../../genericIconComponent";
@ -26,7 +26,7 @@ export default function BuildTrigger({
}): JSX.Element {
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
const { reactFlowInstance } = useContext(typesContext);
const { setTabsState } = useContext(TabsContext);
const { setTabsState, saveFlow } = useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const [isIconTouched, setIsIconTouched] = useState(false);
const eventClick = isBuilding ? "pointer-events-none" : "";
@ -76,65 +76,59 @@ export default function BuildTrigger({
}
async function streamNodeData(flow: FlowType) {
// Step 1: Make a POST request to send the flow data and receive a unique session ID
const id = saveFlow(flow, true);
const response = await postBuildInit(flow);
const { flowId } = response.data;
// Step 2: Use the session ID to establish an SSE connection using EventSource
let validationResults: boolean[] = [];
let finished = false;
const apiUrl = `/api/v1/build/stream/${flowId}`;
const eventSource = new EventSource(apiUrl);
return new Promise<boolean>((resolve, reject) => {
const eventSource = new EventSource(apiUrl);
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
// Close the connection and finish
finished = true;
eventSource.onmessage = (event) => {
// If the event is parseable, return
if (!event.data) {
return;
}
const parsedData = JSON.parse(event.data);
// if the event is the end of the stream, close the connection
if (parsedData.end_of_stream) {
eventSource.close();
resolve(validationResults.every((result) => result));
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: FlowsState) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
eventSource.close();
return;
} else if (parsedData.log) {
// If the event is a log, log it
setSuccessData({ title: parsedData.log });
} else if (parsedData.input_keys !== undefined) {
//@ts-ignore
setTabsState((old: TabsState) => {
return {
...old,
[flowId]: {
...old[flowId],
formKeysData: parsedData,
},
};
});
} else {
// Otherwise, process the data
const isValid = processStreamResult(parsedData);
setProgress(parsedData.progress);
validationResults.push(isValid);
}
};
eventSource.onerror = (error: any) => {
console.error("EventSource failed:", error);
eventSource.close();
if (error.data) {
const parsedData = JSON.parse(error.data);
setErrorData({ title: parsedData.error });
setIsBuilding(false);
}
};
// 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);
reject(new Error("Streaming failed"));
};
});
}
function processStreamResult(parsedData: parsedDataType) {

View file

@ -5,7 +5,7 @@ import BuildTrigger from "./buildTrigger";
import ChatTrigger from "./chatTrigger";
import * as _ from "lodash";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { getBuildStatus } from "../../controllers/API";
import FormModal from "../../modals/formModal";
import { NodeType } from "../../types/flow";
@ -13,7 +13,7 @@ import { NodeType } from "../../types/flow";
export default function Chat({ flow }: ChatType): JSX.Element {
const [open, setOpen] = useState(false);
const [canOpen, setCanOpen] = useState(false);
const { tabsState, isBuilt, setIsBuilt } = useContext(TabsContext);
const { tabsState, isBuilt, setIsBuilt } = useContext(FlowsContext);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
@ -36,7 +36,7 @@ export default function Chat({ flow }: ChatType): JSX.Element {
// Define an async function within the useEffect hook
const fetchBuildStatus = async () => {
const response = await getBuildStatus(flow.id);
setIsBuilt(response.built);
setIsBuilt(response.data.built);
};
// Call the async function

View file

@ -12,6 +12,8 @@ export default function CodeAreaComponent({
nodeClass,
dynamic,
setNodeClass,
id = "",
readonly = false,
}: CodeAreaComponentType) {
const [myValue, setMyValue] = useState(
typeof value == "string" ? value : JSON.stringify(value)
@ -30,6 +32,7 @@ export default function CodeAreaComponent({
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<CodeAreaModal
readonly={readonly}
dynamic={dynamic}
value={myValue}
nodeClass={nodeClass}
@ -41,6 +44,8 @@ export default function CodeAreaComponent({
>
<div className="flex w-full items-center">
<span
id={id}
data-testid={id}
className={
editNode
? "input-edit-node input-dialog"

View file

@ -27,6 +27,7 @@ import {
TabsList,
TabsTrigger,
} from "../../components/ui/tabs";
import { LANGFLOW_SUPPORTED_TYPES } from "../../constants/constants";
import { darkContext } from "../../contexts/darkContext";
import { typesContext } from "../../contexts/typesContext";
import { codeTabsPropsType } from "../../types/components";
@ -63,7 +64,7 @@ export default function CodeTabsComponent({
}, [flow]);
useEffect(() => {
if (tweaks) {
if (tweaks && data) {
unselectAllNodes({
data,
updateNodes: (nodes) => {
@ -242,24 +243,10 @@ export default function CodeTabsComponent({
templateField.charAt(0) !== "_" &&
node.data.node.template[templateField]
.show &&
(node.data.node.template[templateField]
.type === "str" ||
LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField]
.type === "bool" ||
node.data.node.template[templateField]
.type === "float" ||
node.data.node.template[templateField]
.type === "code" ||
node.data.node.template[templateField]
.type === "prompt" ||
node.data.node.template[templateField]
.type === "file" ||
node.data.node.template[templateField]
.type === "int" ||
node.data.node.template[templateField]
.type === "dict" ||
node.data.node.template[templateField]
.type === "NestedDict")
.type
)
)
.map((templateField, indx) => {
return (
@ -457,11 +444,6 @@ export default function CodeTabsComponent({
templateField
].fileTypes
}
suffixes={
node.data.node.template[
templateField
].suffixes
}
onFileChange={(
value: any
) => {
@ -490,6 +472,11 @@ export default function CodeTabsComponent({
templateField
].value
}
rangeSpec={
node.data.node.template[
templateField
].rangeSpec
}
onChange={(target) => {
setData((old) => {
let newInputList =
@ -604,6 +591,14 @@ export default function CodeTabsComponent({
].type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
editNode={true}
disabled={false}
value={
@ -646,6 +641,14 @@ export default function CodeTabsComponent({
<CodeAreaComponent
disabled={false}
editNode={true}
readonly={
node.data.node?.flow &&
node.data.node.template[
templateField
].dynamic
? true
: false
}
value={
!node.data.node.template[
templateField

View file

@ -10,6 +10,7 @@ export default function DictComponent({
onChange,
disabled,
editNode = false,
id = "",
}: DictComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
@ -22,7 +23,6 @@ export default function DictComponent({
}, [value]);
const ref = useRef(value);
debugger;
return (
<div
className={classNames(
@ -31,7 +31,7 @@ export default function DictComponent({
)}
>
{
<div className="flex w-full gap-3">
<div className="flex w-full gap-3" data-testid={id}>
<DictAreaModal
value={ref.current}
onChange={(obj) => {
@ -46,6 +46,7 @@ export default function DictComponent({
: "input-disable pointer-events-none cursor-pointer"
}
placeholder="Click to edit your dictionary..."
data-testid="dict-input"
/>
</DictAreaModal>
</div>

View file

@ -11,6 +11,7 @@ export default function Dropdown({
editNode = false,
numberOfOptions = 0,
apiModal = false,
id = "",
}: DropDownComponentType): JSX.Element {
let [internalValue, setInternalValue] = useState(
value === "" || !value ? "Choose an option" : value
@ -31,15 +32,19 @@ export default function Dropdown({
>
{({ open }) => (
<>
<div className={editNode ? "mt-1" : "relative mt-1"}>
<div className={"relative mt-1"}>
<Listbox.Button
data-test={`${id ?? ""}`}
className={
editNode
? "dropdown-component-outline"
: "dropdown-component-false-outline"
}
>
<span className="dropdown-component-display">
<span
className="dropdown-component-display"
data-testid={`${id ?? ""}-display`}
>
{internalValue}
</span>
<span className={"dropdown-component-arrow"}>
@ -63,7 +68,7 @@ export default function Dropdown({
editNode
? "dropdown-component-true-options nowheel custom-scroll"
: "dropdown-component-false-options nowheel custom-scroll",
apiModal ? "mb-2 w-[250px]" : "absolute"
apiModal ? "mb-2 w-[250px]" : "absolute w-full"
)}
>
{options.map((option, id) => (
@ -86,6 +91,7 @@ export default function Dropdown({
selected ? "font-semibold" : "font-normal",
"block truncate "
)}
data-testid={`${option}-${id ?? ""}-option`}
>
{option}
</span>

View file

@ -7,12 +7,12 @@ export default function FloatComponent({
value,
onChange,
disabled,
rangeSpec,
editNode = false,
}: FloatComponentType): JSX.Element {
const step = 0.1;
const min = -2;
const max = 2;
const step = rangeSpec?.step ?? 0.1;
const min = rangeSpec?.min ?? -2;
const max = rangeSpec?.max ?? 2;
// Clear component state
useEffect(() => {
if (disabled) {
@ -23,6 +23,7 @@ export default function FloatComponent({
return (
<div className="w-full">
<Input
id="float-input"
type="number"
step={step}
min={min}
@ -39,7 +40,9 @@ export default function FloatComponent({
disabled={disabled}
className={editNode ? "input-edit-node" : ""}
placeholder={
editNode ? "Number -2 to 2" : "Type a number from minus two to two"
editNode
? `Enter a value between ${min} and ${max}`
: `Enter a value between ${min} and ${max}`
}
onChange={(event) => {
onChange(event.target.value);

View file

@ -3,14 +3,31 @@ import { IconComponentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
const ForwardedIconComponent = forwardRef(
({ name, className, iconColor }: IconComponentProps, ref) => {
(
{
name,
className,
iconColor,
stroke,
strokeWidth,
id = "",
}: IconComponentProps,
ref
) => {
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
const style = {
strokeWidth: strokeWidth ?? 1.5,
...(stroke && { stroke: stroke }),
...(iconColor && { color: iconColor, stroke: stroke }),
};
return (
<TargetIcon
strokeWidth={1.5}
className={className}
style={iconColor ? { color: iconColor } : {}}
style={style}
ref={ref}
data-testid={id ? `${id}-${name}` : `icon-${name}`}
/>
);
}

View file

@ -1,5 +1,5 @@
import { useContext, useState } from "react";
import { TabsContext } from "../../../../contexts/tabsContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import {
DropdownMenu,
DropdownMenuContent,
@ -8,7 +8,7 @@ import {
DropdownMenuTrigger,
} from "../../../ui/dropdown-menu";
import { Link, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
import { alertContext } from "../../../../contexts/alertContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import FlowSettingsModal from "../../../../modals/flowSettingsModal";
@ -17,7 +17,7 @@ import IconComponent from "../../../genericIconComponent";
import { Button } from "../../../ui/button";
export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
const { addFlow } = useContext(TabsContext);
const { addFlow } = useContext(FlowsContext);
const { setErrorData } = useContext(alertContext);
const { undo, redo } = useContext(undoRedoContext);
const [openSettings, setOpenSettings] = useState(false);
@ -26,7 +26,7 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
function handleAddFlow() {
try {
addFlow(undefined, true).then((id) => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
// saveFlowStyleInDataBase();
@ -38,9 +38,13 @@ export const MenuBar = ({ flows, tabId }: menuBarPropsType): JSX.Element => {
return (
<div className="round-button-div">
<Link to="/">
<button
onClick={() => {
navigate(-1);
}}
>
<IconComponent name="ChevronLeft" className="w-4" />
</Link>
</button>
<div className="header-menu-bar">
<DropdownMenu>
<DropdownMenuTrigger asChild>

View file

@ -6,7 +6,9 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { darkContext } from "../../contexts/darkContext";
import { TabsContext } from "../../contexts/tabsContext";
import { StoreContext } from "../../contexts/storeContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { gradients } from "../../utils/styleUtils";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";
@ -22,17 +24,18 @@ import { Separator } from "../ui/separator";
import MenuBar from "./components/menuBar";
export default function Header(): JSX.Element {
const { flows, tabId } = useContext(TabsContext);
const { flows, tabId } = useContext(FlowsContext);
const { dark, setDark } = useContext(darkContext);
const { notificationCenter } = useContext(alertContext);
const location = useLocation();
const { logout, autoLogin, isAdmin, userData } = useContext(AuthContext);
const { hasStore } = useContext(StoreContext);
const { stars, gradientIndex } = useContext(darkContext);
const navigate = useNavigate();
return (
<div className="header-arrangement">
<div className="header-start-display">
<div className="header-start-display lg:w-[30%]">
<Link to="/">
<span className="ml-4 text-2xl"></span>
</Link>
@ -45,14 +48,19 @@ export default function Header(): JSX.Element {
<Link to="/">
<Button
className="gap-2"
variant={location.pathname === "/" ? "primary" : "secondary"}
variant={
location.pathname === "/flows" ||
location.pathname === "/components"
? "primary"
: "secondary"
}
size="sm"
>
<IconComponent name="Home" className="h-4 w-4" />
<div className="flex-1">{USER_PROJECTS_HEADER}</div>
<div className="hidden flex-1 md:block">{USER_PROJECTS_HEADER}</div>
</Button>
</Link>
<Link to="/community">
{/* <Link to="/community">
<Button
className="gap-2"
variant={
@ -63,18 +71,30 @@ export default function Header(): JSX.Element {
<IconComponent name="Users2" className="h-4 w-4" />
<div className="flex-1">Community Examples</div>
</Button>
</Link>
</Link> */}
{hasStore && (
<Link to="/store">
<Button
className="gap-2"
variant={location.pathname === "/store" ? "primary" : "secondary"}
size="sm"
>
<IconComponent name="Store" className="h-4 w-4" />
<div className="flex-1">Store</div>
</Button>
</Link>
)}
</div>
<div className="header-end-division">
<div className="header-end-division lg:w-[30%]">
<div className="header-end-display">
<a
href="https://github.com/logspace-ai/langflow"
target="_blank"
rel="noreferrer"
className="header-github-link"
className="header-github-link gap-2"
>
<FaGithub className="mr-2 h-5 w-5" />
Star
<FaGithub className="h-5 w-5" />
<div className="hidden lg:block">Star</div>
<div className="header-github-display">{stars}</div>
</a>
<a

View file

@ -1,11 +1,13 @@
import * as Form from "@radix-ui/react-form";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { InputComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import { Input } from "../ui/input";
export default function InputComponent({
autoFocus = false,
onBlur,
value,
onChange,
disabled,
@ -15,9 +17,11 @@ export default function InputComponent({
editNode = false,
placeholder = "Type something...",
className,
id = "",
blurOnEnter = false,
}: InputComponentType): JSX.Element {
const [pwdVisible, setPwdVisible] = useState(false);
const refInput = useRef<HTMLInputElement>(null);
// Clear component state
useEffect(() => {
if (disabled) {
@ -30,6 +34,10 @@ export default function InputComponent({
{isForm ? (
<Form.Control asChild>
<Input
id={"form-" + id}
ref={refInput}
onBlur={onBlur}
autoFocus={autoFocus}
type={password && !pwdVisible ? "password" : "text"}
value={value}
disabled={disabled}
@ -47,15 +55,26 @@ export default function InputComponent({
onChange={(e) => {
onChange(e.target.value);
}}
onCopy={(e) => {
e.preventDefault();
}}
onKeyDown={(e) => {
if (e.ctrlKey && e.key === "c") {
// Perform any actions you need when Ctrl+C is detected
}
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
/>
</Form.Control>
) : (
<Input
id={id}
ref={refInput}
type="text"
onBlur={onBlur}
value={value}
autoFocus={autoFocus}
disabled={disabled}
required={required}
className={classNames(
@ -73,6 +92,7 @@ export default function InputComponent({
}}
onKeyDown={(e) => {
handleKeyDown(e, value, "");
if (blurOnEnter && e.key === "Enter") refInput.current?.blur();
}}
/>
)}

View file

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from "react";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { uploadFile } from "../../controllers/API";
import { FileComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
@ -9,7 +9,6 @@ export default function InputFileComponent({
value,
onChange,
disabled,
suffixes,
fileTypes,
onFileChange,
editNode = false,
@ -17,7 +16,7 @@ export default function InputFileComponent({
const [myValue, setMyValue] = useState(value);
const [loading, setLoading] = useState(false);
const { setErrorData } = useContext(alertContext);
const { tabId } = useContext(TabsContext);
const { tabId } = useContext(FlowsContext);
// Clear component state
useEffect(() => {
@ -29,8 +28,9 @@ export default function InputFileComponent({
}, [disabled, onChange]);
function checkFileType(fileName: string): boolean {
for (let index = 0; index < suffixes.length; index++) {
if (fileName.endsWith(suffixes[index])) {
if (fileTypes === undefined) return true;
for (let index = 0; index < fileTypes.length; index++) {
if (fileName.endsWith(fileTypes[index])) {
return true;
}
}
@ -45,7 +45,7 @@ export default function InputFileComponent({
// Create a file input element
const input = document.createElement("input");
input.type = "file";
input.accept = suffixes.join(",");
input.accept = fileTypes?.join(",");
input.style.display = "none"; // Hidden from view
input.multiple = false; // Allow only one file selection

View file

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { FloatComponentType } from "../../types/components";
import { IntComponentType } from "../../types/components";
import { handleKeyDown } from "../../utils/reactflowUtils";
import { Input } from "../ui/input";
@ -8,7 +8,8 @@ export default function IntComponent({
onChange,
disabled,
editNode = false,
}: FloatComponentType): JSX.Element {
id = "",
}: IntComponentType): JSX.Element {
const min = 0;
// Clear component state
@ -21,6 +22,7 @@ export default function IntComponent({
return (
<div className="w-full">
<Input
id={id}
onKeyDown={(event) => {
if (
event.key !== "Backspace" &&
@ -34,6 +36,8 @@ export default function IntComponent({
event.key !== "c" &&
event.key !== "v" &&
event.key !== "a" &&
event.key !== "ArrowUp" &&
event.key !== "ArrowDown" &&
!/^[-]?\d*$/.test(event.key)
) {
event.preventDefault();

View file

@ -12,8 +12,6 @@ export default function KeypairListComponent({
disabled,
editNode = false,
duplicateKey,
advanced = false,
dataValue,
}: KeyPairListComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
@ -55,6 +53,10 @@ export default function KeypairListComponent({
return (
<div key={idx} className="flex w-full gap-2">
<Input
data-testid={
editNode ? "editNodekeypair" + index : "keypair" + index
}
id={editNode ? "editNodekeypair" + index : "keypair" + index}
type="text"
value={key.trim()}
className={classNames(
@ -72,6 +74,16 @@ export default function KeypairListComponent({
/>
<Input
data-testid={
editNode
? "editNodekeypair" + (index + 100).toString()
: "keypair" + (index + 100).toString()
}
id={
editNode
? "editNodekeypair" + (index + 100).toString()
: "keypair" + (index + 100).toString()
}
type="text"
value={obj[key]}
className={editNode ? "input-edit-node" : ""}
@ -88,6 +100,16 @@ export default function KeypairListComponent({
newInputList.push({ "": "" });
onChange(newInputList);
}}
id={
editNode
? "editNodeplusbtn" + index.toString()
: "plusbtn" + index.toString()
}
data-testid={
editNode
? "editNodeplusbtn" + index.toString()
: "plusbtn" + index.toString()
}
>
<IconComponent
name="Plus"
@ -101,6 +123,16 @@ export default function KeypairListComponent({
newInputList.splice(index, 1);
onChange(newInputList);
}}
data-testid={
editNode
? "editNodeminusbtn" + index.toString()
: "minusbtn" + index.toString()
}
id={
editNode
? "editNodeminusbtn" + index.toString()
: "minusbtn" + index.toString()
}
>
<IconComponent
name="X"

View file

@ -0,0 +1,31 @@
import Header from "../headerComponent";
import { Separator } from "../ui/separator";
export default function PageLayout({
title,
description,
children,
button,
}: {
title: string;
description: string;
children: React.ReactNode;
button?: React.ReactNode;
}) {
return (
<div className="flex h-screen w-full flex-col">
<Header />
<div className="flex h-full w-full flex-col justify-between overflow-auto bg-background px-16">
<div className="flex w-full items-center justify-between gap-4 space-y-0.5 py-8 pb-2">
<div className="flex w-full flex-col">
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
<p className="text-muted-foreground">{description}</p>
</div>
<div className="flex-shrink-0">{button && button}</div>
</div>
<Separator className="my-6 flex" />
{children}
</div>
</div>
);
}

View file

@ -1,7 +1,6 @@
import { useEffect } from "react";
import { TypeModal } from "../../constants/enums";
import { postValidatePrompt } from "../../controllers/API";
import GenericModal from "../../modals/genericModal";
import { PromptAreaComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
@ -14,39 +13,32 @@ export default function PromptAreaComponent({
onChange,
disabled,
editNode = false,
}: PromptAreaComponentType) {
id = "",
readonly = false,
}: PromptAreaComponentType): JSX.Element {
useEffect(() => {
if (disabled) {
onChange("");
}
}, [disabled]);
useEffect(() => {
if (value !== "" && !editNode) {
postValidatePrompt(field_name!, value, nodeClass!).then((apiReturn) => {
if (apiReturn.data) {
setNodeClass!(apiReturn.data.frontend_node);
// need to update reactFlowInstance to re-render the nodes.
}
});
}
}, []);
return (
<div className={disabled ? "pointer-events-none w-full " : " w-full"}>
<GenericModal
id={id}
readonly={readonly}
type={TypeModal.PROMPT}
value={value}
buttonText="Check & Save"
modalTitle="Edit Prompt"
setValue={(value: string) => {
onChange(value);
}}
setValue={onChange}
nodeClass={nodeClass}
setNodeClass={setNodeClass}
>
<div className="flex w-full items-center">
<span
id={id}
data-testid={id}
className={
editNode
? "input-edit-node input-dialog"
@ -58,6 +50,7 @@ export default function PromptAreaComponent({
</span>
{!editNode && (
<IconComponent
id={id}
name="ExternalLink"
className={
"icons-parameters-comp" +

View file

@ -0,0 +1,47 @@
import { Link, useLocation } from "react-router-dom";
import { cn } from "../../utils/utils";
import { buttonVariants } from "../ui/button";
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
items: {
href: string;
title: string;
icon: React.ReactNode;
}[];
}
export default function SidebarNav({
className,
items,
...props
}: SidebarNavProps) {
const location = useLocation();
const pathname = location.pathname;
return (
<nav
className={cn(
"flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
className
)}
{...props}
>
{items.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
buttonVariants({ variant: "ghost" }),
pathname === item.href
? "border border-border bg-muted hover:bg-muted"
: "border border-transparent hover:border-border hover:bg-transparent",
"justify-start gap-2"
)}
>
{item.icon}
{item.title}
</Link>
))}
</nav>
);
}

View file

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

View file

@ -0,0 +1,12 @@
import { useContext } from "react";
import { Navigate } from "react-router-dom";
import { StoreContext } from "../../contexts/storeContext";
export const StoreGuard = ({ children }) => {
const { hasStore } = useContext(StoreContext);
if (!hasStore) {
return <Navigate to="/flows" replace />;
}
return children;
};

View file

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

View file

@ -10,6 +10,7 @@ export default function TextAreaComponent({
onChange,
disabled,
editNode = false,
id = "",
}: TextAreaComponentType): JSX.Element {
// Clear text area
useEffect(() => {
@ -19,28 +20,39 @@ export default function TextAreaComponent({
}, [disabled]);
return (
<div className="flex w-full items-center">
<Input
<div
className={
"flex w-full items-center " + (disabled ? "pointer-events-none" : "")
}
>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
disabled={disabled}
className={editNode ? "input-edit-node" : ""}
placeholder={"Type something..."}
onChange={(event) => {
onChange(event.target.value);
setValue={(value: string) => {
onChange(value);
}}
/>
<div>
<GenericModal
type={TypeModal.TEXT}
buttonText="Finishing Editing"
modalTitle="Edit Text"
value={value}
setValue={(value: string) => {
onChange(value);
}}
>
>
<div className="flex w-full items-center" data-testid={"div-" + id}>
<Input
id={id}
data-testid={id}
value={value}
disabled={disabled}
className={
editNode
? "input-edit-node pointer-events-none "
: " pointer-events-none"
}
placeholder={"Type something..."}
onChange={(event) => {
onChange(event.target.value);
}}
/>
{!editNode && (
<IconComponent
id={id}
name="ExternalLink"
className={
"icons-parameters-comp" +
@ -48,8 +60,8 @@ export default function TextAreaComponent({
}
/>
)}
</GenericModal>
</div>
</div>
</GenericModal>
</div>
);
}

View file

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

View file

@ -14,12 +14,14 @@ const badgeVariants = cva(
"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",
outline: "text-primary/80 hover:bg-muted border-ring/60",
},
size: {
sm: "h-4 text-xs",
md: "h-5 text-sm",
lg: "h-6 text-base",
sq: "h-6 text-sm font-normal rounded-md",
xq: "h-5 text-xs font-normal rounded-md",
},
},
defaultVariants: {

View file

@ -14,15 +14,16 @@ const buttonVariants = cva(
outline:
"border border-input hover:bg-accent hover:text-accent-foreground",
primary:
"border bg-background text-secondary-foreground hover:bg-background/80 dark:hover:bg-background/10 hover:shadow-sm",
"border bg-background text-secondary-foreground hover:bg-secondary-foreground/5 dark:hover:bg-background/10 hover:shadow-sm",
secondary:
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary/80",
"border border-muted bg-muted text-secondary-foreground hover:bg-secondary-foreground/5",
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",
xs: "py-1 px-1 rounded-md",
lg: "h-11 px-8 rounded-md",
},
},

View file

@ -8,7 +8,7 @@ const Card = React.forwardRef<
<div
ref={ref}
className={cn(
"flex flex-col justify-between rounded-lg border bg-card text-card-foreground shadow-sm transition-all hover:shadow-lg",
"flex flex-col justify-between rounded-lg border bg-muted text-card-foreground shadow-sm transition-all hover:shadow-lg",
className
)}
{...props}

View file

@ -0,0 +1,79 @@
"use client";
import { Check, ChevronsUpDown } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils/utils";
import { Button } from "./button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "./command";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
export function Combobox({
items,
onChange,
}: {
items: { value: string; label: string }[];
onChange: (value: string[]) => void;
}) {
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState<string[]>([]);
React.useEffect(() => {
onChange(value);
}, [value]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
>
{value
? items.find((framework) => value.includes(framework.value))?.label
: "Select filter..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search filters..." />
<CommandEmpty>No filters found.</CommandEmpty>
<CommandGroup>
{items.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue((old) => {
if (old.includes(currentValue)) {
return old.filter((item) => item !== currentValue);
}
return [...old, currentValue];
});
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value.includes(framework.value)
? "opacity-100"
: "opacity-0"
)}
/>
{framework.label}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}

View file

@ -0,0 +1,155 @@
"use client";
import { type DialogProps } from "@radix-ui/react-dialog";
import { Command as CommandPrimitive } from "cmdk";
import { Search } from "lucide-react";
import * as React from "react";
import { cn } from "../../utils/utils";
import { Dialog, DialogContent } from "./dialog";
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
/>
));
Command.displayName = CommandPrimitive.displayName;
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
</Dialog>
);
};
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
));
CommandInput.displayName = CommandPrimitive.Input.displayName;
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
));
CommandList.displayName = CommandPrimitive.List.displayName;
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
));
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
));
CommandGroup.displayName = CommandPrimitive.Group.displayName;
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
));
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
/>
));
CommandItem.displayName = CommandPrimitive.Item.displayName;
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}
/>
);
};
CommandShortcut.displayName = "CommandShortcut";
export {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
CommandShortcut,
};

View file

@ -18,14 +18,14 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex h-8 items-center justify-between rounded-md border border-ring/60 px-4 py-2 text-sm text-primary ring-offset-background placeholder:text-muted-foreground hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
<ChevronDown className="ml-2 h-4 w-4" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
));
@ -80,7 +80,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full 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",
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}

View file

@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"overflow-y-auto 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",
"z-45 overflow-y-auto 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}

View file

@ -517,16 +517,19 @@ export const ADMIN_HEADER_TITLE = "Admin Page";
export const ADMIN_HEADER_DESCRIPTION =
"Navigate through this section to efficiently oversee all application users. From here, you can seamlessly manage user accounts.";
export const BASE_URL_API = "/api/v1/";
/**
* URLs excluded from error retries.
* @constant
*
*/
export const URL_EXCLUDED_FROM_ERROR_RETRIES = [
"/api/v1/validate/code",
"/api/v1/custom_component",
"/api/v1/validate/prompt",
"http://localhost:7860/login",
`${BASE_URL_API}validate/code`,
`${BASE_URL_API}custom_component`,
`${BASE_URL_API}validate/prompt`,
`http://localhost:7860/login`,
`${BASE_URL_API}api_key/store`,
];
export const skipNodeUpdate = [
@ -561,10 +564,6 @@ export const CONTROL_NEW_USER = {
is_superuser: false,
};
export const CONTROL_NEW_API_KEY = {
apikeyname: "",
};
export const tabsCode = [];
export function tabsArray(codes: string[], method: number) {
@ -649,8 +648,6 @@ export const FETCH_ERROR_MESSAGE = "Couldn't establish a connection.";
export const FETCH_ERROR_DESCRIPION =
"Check if everything is working properly and try again.";
export const BASE_URL_API = "/api/v1/";
export const SIGN_UP_SUCCESS = "Account created! Await admin activation. ";
export const API_PAGE_PARAGRAPH_1 =
@ -666,3 +663,17 @@ export const LAST_USED_SPAN_1 = "The last time this key was used.";
export const LAST_USED_SPAN_2 =
"Accurate to within the hour from the most recent usage.";
export const LANGFLOW_SUPPORTED_TYPES = new Set([
"str",
"bool",
"float",
"code",
"prompt",
"file",
"int",
"dict",
"NestedDict",
]);
export const priorityFields = new Set(["code", "template"]);

View file

@ -26,6 +26,8 @@ const initialValue: alertContextType = {
pushNotificationList: () => {},
clearNotificationList: () => {},
removeFromNotificationList: () => {},
modalContextOpen: false,
setModalContextOpen: (open: boolean) => {},
};
export const alertContext = createContext<alertContextType>(initialValue);
@ -49,6 +51,7 @@ export function AlertProvider({ children }: { children: ReactNode }) {
const [notificationCenter, setNotificationCenter] = useState(false);
const [notificationList, setNotificationList] = useState<AlertItemType[]>([]);
const [isTweakPage, setIsTweakPage] = useState<boolean>(false);
const [modalContextOpen, setModalContextOpen] = useState<boolean>(false);
const pushNotificationList = (notification: AlertItemType) => {
setNotificationList((old) => {
let newNotificationList = _.cloneDeep(old);
@ -141,6 +144,8 @@ export function AlertProvider({ children }: { children: ReactNode }) {
setSuccessData,
successOpen,
setSuccessOpen,
modalContextOpen,
setModalContextOpen,
}}
>
{children}

View file

@ -19,6 +19,9 @@ const initialValue: AuthContextType = {
authenticationErrorCount: 0,
autoLogin: false,
setAutoLogin: () => {},
setApiKey: () => {},
apiKey: null,
storeApiKey: () => {},
};
export const AuthContext = createContext<AuthContextType>(initialValue);
@ -36,6 +39,10 @@ export function AuthProvider({ children }): React.ReactElement {
const [userData, setUserData] = useState<Users | null>(null);
const [autoLogin, setAutoLogin] = useState<boolean>(false);
const { setLoading } = useContext(alertContext);
const [apiKey, setApiKey] = useState<string | null>(
cookies.get("apikey_tkn_lflw")
);
useEffect(() => {
const storedAccessToken = cookies.get("access_tkn_lflw");
if (storedAccessToken) {
@ -43,6 +50,13 @@ export function AuthProvider({ children }): React.ReactElement {
}
}, []);
useEffect(() => {
const apiKey = cookies.get("apikey_tkn_lflw");
if (apiKey) {
setApiKey(apiKey);
}
}, []);
useEffect(() => {
const isLoginPage = location.pathname.includes("login");
@ -74,7 +88,7 @@ export function AuthProvider({ children }): React.ReactElement {
setLoading(false);
}
});
}, []);
}, [setUserData, setLoading, autoLogin, setIsAdmin]);
function getAuthentication() {
const storedRefreshToken = cookies.get("refresh_tkn_lflw");
@ -94,6 +108,7 @@ export function AuthProvider({ children }): React.ReactElement {
function logout() {
cookies.remove("access_tkn_lflw", { path: "/" });
cookies.remove("refresh_tkn_lflw", { path: "/" });
cookies.remove("apikey_tkn_lflw", { path: "/" });
setIsAdmin(false);
setUserData(null);
setAccessToken(null);
@ -101,6 +116,11 @@ export function AuthProvider({ children }): React.ReactElement {
setIsAuthenticated(false);
}
function storeApiKey(apikey: string) {
cookies.set("apikey_tkn_lflw", apikey, { path: "/" });
setApiKey(apikey);
}
return (
// !! to convert string to boolean
<AuthContext.Provider
@ -118,6 +138,9 @@ export function AuthProvider({ children }): React.ReactElement {
authenticationErrorCount: 0,
setAutoLogin,
autoLogin,
setApiKey,
apiKey,
storeApiKey,
}}
>
{children}

View file

@ -1,5 +1,5 @@
import { AxiosError } from "axios";
import _ from "lodash";
import _, { cloneDeep } from "lodash";
import {
ReactNode,
createContext,
@ -8,12 +8,18 @@ import {
useRef,
useState,
} from "react";
import { Edge, Node, ReactFlowJsonObject, addEdge } from "reactflow";
import {
Edge,
Node,
ReactFlowJsonObject,
XYPosition,
addEdge,
} from "reactflow";
import ShortUniqueId from "short-unique-id";
import { skipNodeUpdate } from "../constants/constants";
import {
deleteFlowFromDatabase,
downloadFlowsFromDatabase,
getVersion,
readFlowsFromDatabase,
saveFlowToDatabase,
updateFlowInDatabase,
@ -21,27 +27,47 @@ import {
} from "../controllers/API";
import { APIClassType, APITemplateType } from "../types/api";
import { tweakType } from "../types/components";
import { FlowType, NodeDataType, NodeType } from "../types/flow";
import { TabsContextType, TabsState } from "../types/tabs";
import {
FlowType,
NodeDataType,
NodeType,
sourceHandleType,
targetHandleType,
} from "../types/flow";
import { FlowsContextType, FlowsState } from "../types/tabs";
import {
addVersionToDuplicates,
checkOldEdgesHandles,
createFlowComponent,
removeFileNameFromComponents,
scapeJSONParse,
scapedJSONStringfy,
updateEdgesHandleIds,
updateIds,
updateTemplate,
} from "../utils/reactflowUtils";
import { getRandomDescription, getRandomName } from "../utils/utils";
import {
createRandomKey,
getRandomDescription,
getRandomName,
} from "../utils/utils";
import { alertContext } from "./alertContext";
import { AuthContext } from "./authContext";
import { typesContext } from "./typesContext";
const uid = new ShortUniqueId({ length: 5 });
const TabsContextInitialValue: TabsContextType = {
const FlowsContextInitialValue: FlowsContextType = {
tabId: "",
setTabId: (index: string) => {},
isLoading: true,
flows: [],
removeFlow: (id: string) => {},
addFlow: async (flowData?: any) => "",
addFlow: async (
newProject: boolean,
flowData?: FlowType,
override?: boolean
) => "",
updateFlow: (newFlow: FlowType) => {},
incrementNodeId: () => uid(),
downloadFlow: (flow: FlowType) => {},
@ -55,7 +81,8 @@ const TabsContextInitialValue: TabsContextType = {
lastCopiedSelection: null,
setLastCopiedSelection: (selection: any) => {},
tabsState: {},
setTabsState: (state: TabsState) => {},
setTabsState: (state: FlowsState) => {},
saveCurrentFlow: () => {},
getNodeId: (nodeType: string) => "",
setTweak: (tweak: any) => {},
getTweak: [],
@ -63,29 +90,35 @@ const TabsContextInitialValue: TabsContextType = {
selection: { nodes: any; edges: any },
position: { x: number; y: number; paneX?: number; paneY?: number }
) => {},
saveComponent: async (component: NodeDataType, override: boolean) => "",
deleteComponent: (key: string) => {},
version: "",
nodesOnFlow: "",
setNodesOnFlow: (nodes: string) => "",
};
export const TabsContext = createContext<TabsContextType>(
TabsContextInitialValue
export const FlowsContext = createContext<FlowsContextType>(
FlowsContextInitialValue
);
export function TabsProvider({ children }: { children: ReactNode }) {
export function FlowsProvider({ children }: { children: ReactNode }) {
const { setErrorData, setNoticeData, setSuccessData } =
useContext(alertContext);
const { getAuthentication, isAuthenticated } = useContext(AuthContext);
const [tabId, setTabId] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [nodesOnFlow, setNodesOnFlow] = useState("");
const [flows, setFlows] = useState<Array<FlowType>>([]);
const [id, setId] = useState(uid());
const { templates, reactFlowInstance } = useContext(typesContext);
const { reactFlowInstance, setData, data } = useContext(typesContext);
const [lastCopiedSelection, setLastCopiedSelection] = useState<{
nodes: any;
edges: any;
} | null>(null);
const [tabsState, setTabsState] = useState<TabsState>({});
const [tabsState, setTabsState] = useState<FlowsState>({});
const [getTweak, setTweak] = useState<tweakType>([]);
useEffect(() => {
@ -103,9 +136,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
function refreshFlows() {
setIsLoading(true);
getTabsDataFromDB().then((DbData) => {
if (DbData && Object.keys(templates).length > 0) {
if (DbData) {
try {
processDBData(DbData);
processFlows(DbData, false);
updateStateWithDbData(DbData);
setIsLoading(false);
} catch (e) {}
@ -117,31 +150,52 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// If the user is authenticated, fetch the types. This code is important to check if the user is auth because of the execution order of the useEffect hooks.
if (getAuthentication() === true) {
// get data from db
//get tabs locally saved
// let tabsData = getLocalStorageTabsData();
refreshFlows();
}
}, [templates, getAuthentication()]);
}, [getAuthentication(), tabId]);
function getTabsDataFromDB() {
//get tabs from db
return readFlowsFromDatabase();
}
function processDBData(DbData: FlowType[]) {
function processFlows(DbData: FlowType[], skipUpdate = true) {
let savedComponents: { [key: string]: APIClassType } = {};
DbData.forEach((flow: FlowType) => {
try {
if (!flow.data) {
return;
}
processFlowEdges(flow);
processFlowNodes(flow);
} catch (e) {}
if (flow.data && flow.is_component) {
(flow.data.nodes[0].data as NodeDataType).node!.display_name =
flow.name;
savedComponents[
createRandomKey(
(flow.data.nodes[0].data as NodeDataType).type,
uid()
)
] = _.cloneDeep((flow.data.nodes[0].data as NodeDataType).node!);
return;
}
if (!skipUpdate) processDataFromFlow(flow, false);
} catch (e) {
console.log(e);
}
});
setData((prev) => {
let newData = cloneDeep(prev);
newData["saved_components"] = cloneDeep(savedComponents);
return newData;
});
}
function processFlowEdges(flow: FlowType) {
if (!flow.data || !flow.data.edges) return;
if (checkOldEdgesHandles(flow.data.edges)) {
const newEdges = updateEdgesHandleIds(flow.data);
flow.data.edges = newEdges;
}
//update edges colors
flow.data.edges.forEach((edge) => {
edge.className = "";
edge.style = { stroke: "#555" };
@ -156,26 +210,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
node.data.node!.documentation = template["documentation"];
}
function processFlowNodes(flow: FlowType) {
if (!flow.data || !flow.data.nodes) return;
flow.data.nodes.forEach((node: NodeType) => {
if (skipNodeUpdate.includes(node.data.type)) return;
const template = templates[node.data.type];
if (!template) {
setErrorData({ title: `Unknown node type: ${node.data.type}` });
return;
}
if (Object.keys(template["template"]).length > 0) {
updateDisplay_name(node, template);
updateNodeBaseClasses(node, template);
updateNodeEdges(flow, node, template);
updateNodeDescription(node, template);
updateNodeTemplate(node, template);
updateNodeDocumentation(node, template);
}
});
}
function updateNodeBaseClasses(node: NodeType, template: APIClassType) {
node.data.node!.base_classes = template["base_classes"];
}
@ -187,11 +221,12 @@ export function TabsProvider({ children }: { children: ReactNode }) {
) {
flow.data!.edges.forEach((edge) => {
if (edge.source === node.id) {
edge.sourceHandle = edge.sourceHandle
?.split("|")
.slice(0, 2)
.concat(template["base_classes"])
.join("|");
let sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
sourceHandleObject.baseClasses = template["base_classes"];
edge.data.sourceHandle = sourceHandleObject;
edge.sourceHandle = scapedJSONStringfy(sourceHandleObject);
}
});
}
@ -227,9 +262,15 @@ export function TabsProvider({ children }: { children: ReactNode }) {
flowName: string,
flowDescription?: string
) {
let clonedFlow = cloneDeep(flow);
removeFileNameFromComponents(clonedFlow);
// create a data URI with the current flow data
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
JSON.stringify({ ...flow, name: flowName, description: flowDescription })
JSON.stringify({
...clonedFlow,
name: flowName,
description: flowDescription,
})
)}`;
// create a link element and set its properties
@ -243,9 +284,6 @@ export function TabsProvider({ children }: { children: ReactNode }) {
// simulate a click on the link element to trigger the download
link.click();
setNoticeData({
title: "Warning: Critical data, JSON file may include API keys.",
});
}
function downloadFlows() {
@ -273,46 +311,70 @@ 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.
*/
async function uploadFlow(
newProject?: boolean,
file?: File
): Promise<String | undefined> {
let id;
if (file) {
let text = await file.text();
let fileData = JSON.parse(text);
if (fileData.flows) {
fileData.flows.forEach((flow: FlowType) => {
id = addFlow(flow, newProject);
});
}
// parse the text into a JSON object
let flow: FlowType = JSON.parse(text);
id = await addFlow(flow, newProject);
} else {
// create a file input
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
// add a change event listener to the file input
id = await new Promise((resolve) => {
async function uploadFlow({
newProject,
file,
isComponent = false,
position = { x: 10, y: 10 },
}: {
newProject: boolean;
file?: File;
isComponent?: boolean;
position?: XYPosition;
}): Promise<String | never> {
return new Promise(async (resolve, reject) => {
let id;
if (file) {
let text = await file.text();
let fileData = JSON.parse(text);
if (
newProject &&
((!fileData.is_component && isComponent === true) ||
(fileData.is_component !== undefined &&
fileData.is_component !== isComponent))
) {
reject("You cannot upload a component as a flow or vice versa");
} else {
if (fileData.flows) {
fileData.flows.forEach((flow: FlowType) => {
id = addFlow(newProject, flow, undefined, position);
});
resolve("");
} else {
id = await addFlow(newProject, fileData, undefined, position);
resolve(id);
}
}
} else {
// 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 = async (e: Event) => {
if (
(e.target as HTMLInputElement).files![0].type === "application/json"
) {
const currentfile = (e.target as HTMLInputElement).files![0];
let text = await currentfile.text();
let flow: FlowType = JSON.parse(text);
const flowId = await addFlow(flow, newProject);
resolve(flowId);
let fileData: FlowType = await JSON.parse(text);
if (
(!fileData.is_component && isComponent === true) ||
(fileData.is_component !== undefined &&
fileData.is_component !== isComponent)
) {
reject("You cannot upload a component as a flow or vice versa");
} else {
id = await addFlow(newProject, fileData);
resolve(id);
}
}
};
// trigger the file input click event to open the file dialog
input.click();
});
}
return id;
}
});
}
function uploadFlows() {
@ -343,19 +405,19 @@ export function TabsProvider({ children }: { children: ReactNode }) {
* Updates the state of flows and tabIndex using setFlows and setTabIndex hooks.
* @param {string} id - The id of the flow to remove.
*/
function removeFlow(id: string) {
async function removeFlow(id: string) {
const index = flows.findIndex((flow) => flow.id === id);
if (index >= 0) {
deleteFlowFromDatabase(id).then(() => {
setFlows(flows.filter((flow) => flow.id !== id));
});
await deleteFlowFromDatabase(id);
//removes component from data if there is any
setFlows(flows.filter((flow) => flow.id !== id));
processFlows(flows.filter((flow) => flow.id !== id));
}
}
/**
* Add a new flow to the list of flows.
* @param flow Optional flow to add.
*/
function paste(
selectionInstance: { nodes: Node[]; edges: Edge[] },
position: { x: number; y: number; paneX?: number; paneY?: number }
@ -376,7 +438,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
const insidePosition = position.paneX
? { x: position.paneX + position.x, y: position.paneY! + position.y }
: reactFlowInstance!.project({ x: position.x, y: position.y });
: reactFlowInstance!.screenToFlowPosition({
x: position.x,
y: position.y,
});
selectionInstance.nodes.forEach((node: NodeType) => {
// Generate a unique node ID
@ -404,19 +469,28 @@ export function TabsProvider({ children }: { children: ReactNode }) {
});
reactFlowInstance!.setNodes(nodes);
selectionInstance.edges.forEach((edge) => {
selectionInstance.edges.forEach((edge: Edge) => {
let source = idsMap[edge.source];
let target = idsMap[edge.target];
let sourceHandleSplitted = edge.sourceHandle!.split("|");
let sourceHandle =
sourceHandleSplitted[0] +
"|" +
source +
"|" +
sourceHandleSplitted.slice(2).join("|");
let targetHandleSplitted = edge.targetHandle!.split("|");
let targetHandle =
targetHandleSplitted.slice(0, -1).join("|") + "|" + target;
const sourceHandleObject: sourceHandleType = scapeJSONParse(
edge.sourceHandle!
);
let sourceHandle = scapedJSONStringfy({
...sourceHandleObject,
id: source,
});
sourceHandleObject.id = source;
edge.data.sourceHandle = sourceHandleObject;
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
let targetHandle = scapedJSONStringfy({
...targetHandleObject,
id: target,
});
targetHandleObject.id = target;
edge.data.targetHandle = targetHandleObject;
let id =
"reactflow__edge-" +
source +
@ -431,12 +505,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
sourceHandle,
targetHandle,
id,
data: cloneDeep(edge.data),
style: { stroke: "#555" },
className:
targetHandle.split("|")[0] === "Text"
targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ",
animated: targetHandle.split("|")[0] === "Text",
animated: targetHandleObject.type === "Text",
selected: false,
},
edges.map((edge) => ({ ...edge, selected: false }))
@ -446,24 +521,36 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
const addFlow = async (
newProject: Boolean,
flow?: FlowType,
newProject?: Boolean
override?: boolean,
position?: XYPosition
): Promise<String | undefined> => {
if (newProject) {
let flowData = extractDataFromFlow(flow!);
if (flowData.description == "") {
flowData.description = getRandomDescription();
}
let flowData = flow
? processDataFromFlow(flow)
: { nodes: [], edges: [], viewport: { zoom: 1, x: 0, y: 0 } };
// Create a new flow with a default name if no flow is provided.
if (override) {
deleteComponent(flow!.name);
const newFlow = createNewFlow(flowData, flow!);
const { id } = await saveFlowToDatabase(newFlow);
newFlow.id = id;
//setTimeout to prevent update state with wrong state
setTimeout(() => {
addFlowToLocalState(newFlow);
}, 200);
// addFlowToLocalState(newFlow);
return;
}
const newFlow = createNewFlow(flowData, flow!);
processFlowEdges(newFlow);
processFlowNodes(newFlow);
const flowName = addVersionToDuplicates(newFlow, flows);
newFlow.name = flowName;
const newName = addVersionToDuplicates(newFlow, flows);
newFlow.name = newName;
try {
const { id } = await saveFlowToDatabase(newFlow);
// Change the id to the new id.
@ -481,76 +568,59 @@ export function TabsProvider({ children }: { children: ReactNode }) {
} else {
paste(
{ nodes: flow!.data!.nodes, edges: flow!.data!.edges },
{ x: 10, y: 10 }
position ?? { x: 10, y: 10 }
);
}
};
const extractDataFromFlow = (flow: FlowType) => {
const processDataFromFlow = (flow: FlowType, refreshIds = true) => {
let data = flow?.data ? flow.data : null;
const description = flow?.description ? flow.description : "";
if (data) {
processFlowEdges(flow);
//prevent node update for now
// processFlowNodes(flow);
//add animation to text type edges
updateEdges(data.edges);
updateNodes(data.nodes, data.edges);
updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
// updateNodes(data.nodes, data.edges);
if (refreshIds) updateIds(data, getNodeId); // Assuming updateIds is defined elsewhere
}
return { data, description };
return data;
};
const updateEdges = (edges: Edge[]) => {
edges.forEach((edge) => {
edge.className =
(edge.targetHandle!.split("|")[0] === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection";
edge.animated = edge.targetHandle!.split("|")[0] === "Text";
});
};
const updateNodes = (nodes: Node[], edges: Edge[]) => {
nodes.forEach((node) => {
if (skipNodeUpdate.includes(node.data.type)) return;
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
if (edges)
edges.forEach((edge) => {
const targetHandleObject: targetHandleType = scapeJSONParse(
edge.targetHandle!
);
}
});
edge.className =
(targetHandleObject.type === "Text"
? "stroke-gray-800 "
: "stroke-gray-900 ") + " stroke-connection";
edge.animated = targetHandleObject.type === "Text";
});
};
const createNewFlow = (
flowData: { data: ReactFlowJsonObject | null; description: string },
flowData: ReactFlowJsonObject | null,
flow: FlowType
) => ({
description: flowData.description,
description: flow?.description ?? getRandomDescription(),
name: flow?.name ?? getRandomName(),
data: flowData.data,
data: flowData,
id: "",
is_component: flow?.is_component ?? false,
});
const addFlowToLocalState = (newFlow: FlowType) => {
let newFlows: FlowType[] = [];
setFlows((prevState) => {
newFlows = newFlows.concat(prevState);
newFlows.push(newFlow);
return [...prevState, newFlow];
});
processFlows(newFlows);
};
/**
@ -566,10 +636,20 @@ export function TabsProvider({ children }: { children: ReactNode }) {
newFlows[index].data = newFlow.data;
newFlows[index].name = newFlow.name;
}
newFlow = {
...newFlow,
};
return newFlows;
});
}
function saveCurrentFlow() {
const currentFlow = flows.find((flow) => flow.id === tabId);
if (currentFlow && reactFlowInstance && currentFlow.data) {
updateFlow({ ...currentFlow, data: reactFlowInstance?.toObject()! });
}
}
async function saveFlow(newFlow: FlowType, silent?: boolean) {
try {
// updates flow in db
@ -608,16 +688,41 @@ export function TabsProvider({ children }: { children: ReactNode }) {
}
}
function saveComponent(component: NodeDataType, override: boolean) {
component.node!.official = false;
return addFlow(true, createFlowComponent(component, version), override);
}
function deleteComponent(key: string) {
let componentFlow = flows.find(
(componentFlow) =>
componentFlow.is_component && componentFlow.name === key
);
if (componentFlow) {
removeFlow(componentFlow.id);
}
}
const [isBuilt, setIsBuilt] = useState(false);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return (
<TabsContext.Provider
<FlowsContext.Provider
value={{
version,
saveFlow,
isBuilt,
setIsBuilt,
lastCopiedSelection,
setLastCopiedSelection,
saveCurrentFlow,
hardReset,
tabId,
setTabId,
@ -637,9 +742,13 @@ export function TabsProvider({ children }: { children: ReactNode }) {
getTweak,
setTweak,
isLoading,
saveComponent,
deleteComponent,
nodesOnFlow,
setNodesOnFlow,
}}
>
{children}
</TabsContext.Provider>
</FlowsContext.Provider>
);
}

View file

@ -7,8 +7,10 @@ import { SSEProvider } from "./SSEContext";
import { AlertProvider } from "./alertContext";
import { AuthProvider } from "./authContext";
import { DarkProvider } from "./darkContext";
import { FlowsProvider } from "./flowsContext";
import { LocationProvider } from "./locationContext";
import { TabsProvider } from "./tabsContext";
import { StoreProvider } from "./storeContext";
import { TypesProvider } from "./typesContext";
import { UndoRedoProvider } from "./undoRedoContext";
@ -26,9 +28,11 @@ export default function ContextWrapper({ children }: { children: ReactNode }) {
<LocationProvider>
<ApiInterceptor />
<SSEProvider>
<TabsProvider>
<UndoRedoProvider>{children}</UndoRedoProvider>
</TabsProvider>
<FlowsProvider>
<UndoRedoProvider>
<StoreProvider>{children}</StoreProvider>
</UndoRedoProvider>
</FlowsProvider>
</SSEProvider>
</LocationProvider>
</TypesProvider>

View file

@ -0,0 +1,74 @@
import { createContext, useContext, useEffect, useState } from "react";
import { checkHasApiKey, checkHasStore } from "../controllers/API";
import { storeContextType } from "../types/contexts/store";
import { AuthContext } from "./authContext";
//store context to share user components and update them
const initialValue = {
hasStore: true,
setHasStore: () => {},
validApiKey: false,
setValidApiKey: () => {},
hasApiKey: false,
setHasApiKey: () => {},
loadingApiKey: true,
};
export const StoreContext = createContext<storeContextType>(initialValue);
export function StoreProvider({ children }) {
const [hasStore, setHasStore] = useState(false);
const [loadingApiKey, setLoadingApiKey] = useState(true);
const [hasApiKey, setHasApiKey] = useState(true);
const [validApiKey, setValidApiKey] = useState(false);
const [storeChecked, setStoreChecked] = useState(false);
const { apiKey } = useContext(AuthContext);
useEffect(() => {
const fetchStoreData = async () => {
try {
if (storeChecked) return;
const res = await checkHasStore();
setHasStore(res?.enabled ?? false);
setStoreChecked(true);
} catch (e) {
console.log(e);
}
};
fetchStoreData();
}, []);
const fetchApiData = async () => {
setLoadingApiKey(true);
try {
const res = await checkHasApiKey();
setHasApiKey(res?.has_api_key ?? false);
setValidApiKey(res?.is_valid ?? false);
setLoadingApiKey(false);
} catch (e) {
setLoadingApiKey(false);
console.log(e);
}
};
useEffect(() => {
fetchApiData();
}, [storeChecked, apiKey]);
return (
<StoreContext.Provider
value={{
hasStore,
setHasStore,
hasApiKey,
setHasApiKey,
validApiKey,
setValidApiKey,
loadingApiKey,
}}
>
{children}
</StoreContext.Provider>
);
}

View file

@ -1,3 +1,4 @@
import _ from "lodash";
import {
createContext,
ReactNode,
@ -5,7 +6,7 @@ import {
useEffect,
useState,
} from "react";
import { Edge, Node, ReactFlowInstance } from "reactflow";
import { ReactFlowInstance } from "reactflow";
import { getAll, getHealth } from "../controllers/API";
import { APIKindType } from "../types/api";
import { typesContextType } from "../types/typesContext";
@ -59,11 +60,13 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Make sure to only update the state if the component is still mounted.
if (isMounted && result?.status === 200) {
setLoading(false);
setData(result.data);
let { data } = _.cloneDeep(result);
setData((old) => ({ ...old, ...data }));
setTemplates(
Object.keys(result.data).reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
acc[c] = result.data[curr][c];
Object.keys(data).reduce((acc, curr) => {
Object.keys(data[curr]).forEach((c: keyof APIKindType) => {
//prevent wrong overwriting of the component template by a group of the same type
if (!data[curr][c].flow) acc[c] = data[curr][c];
});
return acc;
}, {})
@ -71,13 +74,13 @@ export function TypesProvider({ children }: { children: ReactNode }) {
// Set the types by reducing over the keys of the result data and updating the accumulator.
setTypes(
// Reverse the keys so the tool world does not overlap
Object.keys(result.data)
Object.keys(data)
.reverse()
.reduce((acc, curr) => {
Object.keys(result.data[curr]).forEach((c: keyof APIKindType) => {
Object.keys(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) => {
data[curr][c].base_classes?.forEach((b) => {
acc[b] = curr;
});
});
@ -87,6 +90,7 @@ export function TypesProvider({ children }: { children: ReactNode }) {
}
} catch (error) {
console.error("An error has occurred while fetching types.");
console.log(error);
await getHealth().catch((e) => {
setFetchError(true);
});
@ -94,31 +98,25 @@ export function TypesProvider({ children }: { children: ReactNode }) {
}
function deleteNode(idx: string | Array<string>) {
reactFlowInstance!.setNodes(
reactFlowInstance!
.getNodes()
.filter((node: Node) =>
typeof idx === "string" ? node.id !== idx : !idx.includes(node.id)
)
);
reactFlowInstance!.setEdges(
reactFlowInstance!
.getEdges()
.filter((edge) =>
typeof idx === "string"
? edge.source !== idx && edge.target !== idx
: !idx.includes(edge.source) && !idx.includes(edge.target)
)
);
if (reactFlowInstance === null) return;
const edges = reactFlowInstance!
.getEdges()
.filter((edge) =>
typeof idx === "string"
? edge.source == idx || edge.target == idx
: idx.includes(edge.source) || idx.includes(edge.target)
);
reactFlowInstance!.deleteElements({
nodes:
typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })),
edges,
});
}
function deleteEdge(idx: string | Array<string>) {
reactFlowInstance!.setEdges(
reactFlowInstance!
.getEdges()
.filter((edge: Edge) =>
typeof idx === "string" ? edge.id !== idx : !idx.includes(edge.id)
)
);
reactFlowInstance!.deleteElements({
edges:
typeof idx === "string" ? [{ id: idx }] : idx.map((id) => ({ id })),
});
}
return (

View file

@ -13,7 +13,7 @@ import {
undoRedoContextType,
} from "../types/typesContext";
import { isWrappedWithClass } from "../utils/utils";
import { TabsContext } from "./tabsContext";
import { FlowsContext } from "./flowsContext";
const initialValue = {
undo: () => {},
@ -29,7 +29,7 @@ const defaultOptions: UseUndoRedoOptions = {
export const undoRedoContext = createContext<undoRedoContextType>(initialValue);
export function UndoRedoProvider({ children }) {
const { tabId, flows } = useContext(TabsContext);
const { tabId, flows } = useContext(FlowsContext);
const [past, setPast] = useState<HistoryItem[][]>(flows.map(() => []));
const [future, setFuture] = useState<HistoryItem[][]>(flows.map(() => []));
@ -52,15 +52,23 @@ export function UndoRedoProvider({ children }) {
const takeSnapshot = useCallback(() => {
// push the current graph to the past state
setPast((old) => {
let newPast = cloneDeep(old);
newPast[tabIndex] = old[tabIndex].slice(
old[tabIndex].length - defaultOptions.maxHistorySize + 1,
old[tabIndex].length
let newPast = cloneDeep(past);
let newState = {
nodes: cloneDeep(getNodes()),
edges: cloneDeep(getEdges()),
};
if (
past[tabIndex] &&
JSON.stringify(past[tabIndex][past[tabIndex].length - 1]) !==
JSON.stringify(newState)
) {
newPast[tabIndex] = past[tabIndex].slice(
past[tabIndex].length - defaultOptions.maxHistorySize + 1,
past[tabIndex].length
);
newPast[tabIndex].push({ nodes: getNodes(), edges: getEdges() });
return newPast;
});
newPast[tabIndex].push(newState);
}
setPast(newPast);
// whenever we take a new snapshot, the redo operations need to be cleared to avoid state mismatches
setFuture((old) => {

View file

@ -4,6 +4,7 @@ import { BASE_URL_API } from "../../constants/constants";
import { api } from "../../controllers/API/api";
import {
APIObjectType,
Component,
LoginType,
Users,
changeUser,
@ -12,6 +13,7 @@ import {
} from "../../types/api/index";
import { UserInputType } from "../../types/components";
import { FlowStyleType, FlowType } from "../../types/flow";
import { StoreComponentResponse } from "../../types/store";
import {
APIClassType,
BuildStatusTypeAPI,
@ -70,10 +72,10 @@ export async function postValidatePrompt(
template: string,
frontend_node: APIClassType
): Promise<AxiosResponse<PromptTypeAPI>> {
return await api.post(`${BASE_URL_API}validate/prompt`, {
name: name,
template: template,
frontend_node: frontend_node,
return api.post(`${BASE_URL_API}validate/prompt`, {
name,
template,
frontend_node,
});
}
@ -112,12 +114,14 @@ export async function saveFlowToDatabase(newFlow: {
data: ReactFlowJsonObject | null;
description: string;
style?: FlowStyleType;
is_component?: boolean;
}): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}flows/`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
});
if (response.status !== 201) {
@ -353,7 +357,21 @@ export async function postCustomComponent(
code: string,
apiClass: APIClassType
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component`, { code });
// let template = apiClass.template;
return await api.post(`${BASE_URL_API}custom_component`, {
code,
frontend_node: apiClass,
});
}
export async function postCustomComponentUpdate(
code: string,
field: string
): Promise<AxiosResponse<APIClassType>> {
return await api.post(`${BASE_URL_API}custom_component/update`, {
code,
field,
});
}
export async function onLogin(user: LoginType) {
@ -522,3 +540,323 @@ export async function deleteApiKey(api_key: string) {
throw error;
}
}
export async function addApiKeyStore(key: string) {
try {
const res = await api.post(`${BASE_URL_API}api_key/store`, {
api_key: key,
});
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
/**
* 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 saveFlowStore(
newFlow: {
name?: string;
data: ReactFlowJsonObject | null;
description?: string;
style?: FlowStyleType;
is_component?: boolean;
parent?: string;
last_tested_version?: string;
},
tags: string[],
publicFlow = false
): Promise<FlowType> {
try {
const response = await api.post(`${BASE_URL_API}store/components/`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
parent: newFlow.parent,
tags: tags,
private: !publicFlow,
status: publicFlow ? "Public" : "Private",
last_tested_version: newFlow.last_tested_version,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}
/**
* Fetches the flows from the store.
* @returns {Promise<>} A promise that resolves to an AxiosResponse containing the build status.
*
*/
export async function getFlowsStore(): Promise<AxiosResponse<FlowType[]>> {
return await api.get(`${BASE_URL_API}store/`);
}
export async function getStoreComponents({
component_id = null,
page = 1,
limit = 9999999,
is_component = null,
sort = "-count(liked_by)",
tags = [] || null,
liked = null,
isPrivate = null,
search = null,
filterByUser = null,
fields = null,
}: {
component_id?: string | null;
page?: number;
limit?: number;
is_component?: boolean | null;
sort?: string;
tags?: string[] | null;
liked?: boolean | null;
isPrivate?: boolean | null;
search?: string | null;
filterByUser?: boolean | null;
fields?: Array<string> | null;
}): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
const queryParams: any = [];
if (component_id !== undefined && component_id !== null) {
queryParams.push(`component_id=${component_id}`);
}
if (search !== undefined && search !== null) {
queryParams.push(`search=${search}`);
}
if (isPrivate !== undefined && isPrivate !== null) {
queryParams.push(`private=${isPrivate}`);
}
if (tags !== undefined && tags !== null && tags.length > 0) {
queryParams.push(`tags=${tags.join(encodeURIComponent(","))}`);
}
if (fields !== undefined && fields !== null && fields.length > 0) {
queryParams.push(`fields=${fields.join(encodeURIComponent(","))}`);
}
if (sort !== undefined && sort !== null) {
queryParams.push(`sort=${sort}`);
} else {
queryParams.push(`sort=-count(liked_by)`); // default sort
}
if (liked !== undefined && liked !== null) {
queryParams.push(`liked=${liked}`);
}
if (filterByUser !== undefined && filterByUser !== null) {
queryParams.push(`filter_by_user=${filterByUser}`);
}
if (page !== undefined) {
queryParams.push(`page=${page ?? 1}`);
}
if (limit !== undefined) {
queryParams.push(`limit=${limit ?? 9999999}`);
}
if (is_component !== null && is_component !== undefined) {
queryParams.push(`is_component=${is_component}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function postStoreComponents(component: Component) {
try {
const res = await api.post(`${BASE_URL_API}store/components/`, component);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getComponent(component_id: string) {
try {
const res = await api.get(
`${BASE_URL_API}store/components/${component_id}`
);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function searchComponent(
query: string | null,
page?: number | null,
limit?: number | null,
status?: string | null,
tags?: string[]
): Promise<StoreComponentResponse | undefined> {
try {
let url = `${BASE_URL_API}store/components/`;
const queryParams: any = [];
if (query !== undefined && query !== null) {
queryParams.push(`search=${query}`);
}
if (page !== undefined && page !== null) {
queryParams.push(`page=${page}`);
}
if (limit !== undefined && limit !== null) {
queryParams.push(`limit=${limit}`);
}
if (status !== undefined && status !== null) {
queryParams.push(`status=${status}`);
}
if (tags !== undefined && tags !== null) {
queryParams.push(`tags=${tags}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function checkHasApiKey() {
try {
const res = await api.get(`${BASE_URL_API}store/check/api_key`);
if (res?.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function checkHasStore() {
try {
const res = await api.get(`${BASE_URL_API}store/check/`);
if (res?.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getCountComponents(is_component?: boolean | null) {
try {
let url = `${BASE_URL_API}store/components/count`;
const queryParams: any = [];
if (is_component !== undefined) {
queryParams.push(`is_component=${is_component}`);
}
if (queryParams.length > 0) {
url += `?${queryParams.join("&")}`;
}
const res = await api.get(url);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export async function getStoreTags() {
try {
const res = await api.get(`${BASE_URL_API}store/tags`);
if (res.status === 200) {
return res.data;
}
} catch (error) {
console.log("Error:", error);
throw error;
}
}
export const postLikeComponent = (componentId: string) => {
return api.post(`${BASE_URL_API}store/users/likes/${componentId}`);
};
/**
* Updates an existing flow in the Store.
*
* @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 updateFlowStore(
newFlow: {
name?: string;
data: ReactFlowJsonObject | null;
description?: string;
style?: FlowStyleType;
is_component?: boolean;
parent?: string;
last_tested_version?: string;
},
tags: string[],
publicFlow = false,
id: string
): Promise<FlowType> {
try {
const response = await api.patch(`${BASE_URL_API}store/components/${id}`, {
name: newFlow.name,
data: newFlow.data,
description: newFlow.description,
is_component: newFlow.is_component,
parent: newFlow.parent,
tags: tags,
private: !publicFlow,
last_tested_version: newFlow.last_tested_version,
});
if (response.status !== 201) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.data;
} catch (error) {
console.error(error);
throw error;
}
}

View file

@ -0,0 +1,31 @@
const SvgAWS = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
xmlSpace="preserve"
id="Layer_1"
x={0}
y={0}
style={{
enableBackground: "new 0 0 304 182",
}}
viewBox="0 0 304 182"
{...props}
>
<style>{".st1{fill-rule:evenodd;clip-rule:evenodd;fill:#f90}"}</style>
<path
d="M86.4 66.4c0 3.7.4 6.7 1.1 8.9.8 2.2 1.8 4.6 3.2 7.2.5.8.7 1.6.7 2.3 0 1-.6 2-1.9 3L83.2 92c-.9.6-1.8.9-2.6.9-1 0-2-.5-3-1.4-1.4-1.5-2.6-3.1-3.6-4.7-1-1.7-2-3.6-3.1-5.9-7.8 9.2-17.6 13.8-29.4 13.8-8.4 0-15.1-2.4-20-7.2-4.9-4.8-7.4-11.2-7.4-19.2 0-8.5 3-15.4 9.1-20.6 6.1-5.2 14.2-7.8 24.5-7.8 3.4 0 6.9.3 10.6.8 3.7.5 7.5 1.3 11.5 2.2v-7.3c0-7.6-1.6-12.9-4.7-16-3.2-3.1-8.6-4.6-16.3-4.6-3.5 0-7.1.4-10.8 1.3-3.7.9-7.3 2-10.8 3.4-1.6.7-2.8 1.1-3.5 1.3-.7.2-1.2.3-1.6.3-1.4 0-2.1-1-2.1-3.1v-4.9c0-1.6.2-2.8.7-3.5.5-.7 1.4-1.4 2.8-2.1 3.5-1.8 7.7-3.3 12.6-4.5C41 1.9 46.2 1.3 51.7 1.3c11.9 0 20.6 2.7 26.2 8.1 5.5 5.4 8.3 13.6 8.3 24.6v32.4zM45.8 81.6c3.3 0 6.7-.6 10.3-1.8 3.6-1.2 6.8-3.4 9.5-6.4 1.6-1.9 2.8-4 3.4-6.4.6-2.4 1-5.3 1-8.7v-4.2c-2.9-.7-6-1.3-9.2-1.7-3.2-.4-6.3-.6-9.4-.6-6.7 0-11.6 1.3-14.9 4-3.3 2.7-4.9 6.5-4.9 11.5 0 4.7 1.2 8.2 3.7 10.6 2.4 2.5 5.9 3.7 10.5 3.7zm80.3 10.8c-1.8 0-3-.3-3.8-1-.8-.6-1.5-2-2.1-3.9L96.7 10.2c-.6-2-.9-3.3-.9-4 0-1.6.8-2.5 2.4-2.5h9.8c1.9 0 3.2.3 3.9 1 .8.6 1.4 2 2 3.9l16.8 66.2 15.6-66.2c.5-2 1.1-3.3 1.9-3.9.8-.6 2.2-1 4-1h8c1.9 0 3.2.3 4 1 .8.6 1.5 2 1.9 3.9l15.8 67 17.3-67c.6-2 1.3-3.3 2-3.9.8-.6 2.1-1 3.9-1h9.3c1.6 0 2.5.8 2.5 2.5 0 .5-.1 1-.2 1.6-.1.6-.3 1.4-.7 2.5l-24.1 77.3c-.6 2-1.3 3.3-2.1 3.9-.8.6-2.1 1-3.8 1h-8.6c-1.9 0-3.2-.3-4-1-.8-.7-1.5-2-1.9-4L156 23l-15.4 64.4c-.5 2-1.1 3.3-1.9 4-.8.7-2.2 1-4 1h-8.6zm128.5 2.7c-5.2 0-10.4-.6-15.4-1.8-5-1.2-8.9-2.5-11.5-4-1.6-.9-2.7-1.9-3.1-2.8-.4-.9-.6-1.9-.6-2.8v-5.1c0-2.1.8-3.1 2.3-3.1.6 0 1.2.1 1.8.3.6.2 1.5.6 2.5 1 3.4 1.5 7.1 2.7 11 3.5 4 .8 7.9 1.2 11.9 1.2 6.3 0 11.2-1.1 14.6-3.3 3.4-2.2 5.2-5.4 5.2-9.5 0-2.8-.9-5.1-2.7-7-1.8-1.9-5.2-3.6-10.1-5.2L246 52c-7.3-2.3-12.7-5.7-16-10.2-3.3-4.4-5-9.3-5-14.5 0-4.2.9-7.9 2.7-11.1 1.8-3.2 4.2-6 7.2-8.2 3-2.3 6.4-4 10.4-5.2 4-1.2 8.2-1.7 12.6-1.7 2.2 0 4.5.1 6.7.4 2.3.3 4.4.7 6.5 1.1 2 .5 3.9 1 5.7 1.6 1.8.6 3.2 1.2 4.2 1.8 1.4.8 2.4 1.6 3 2.5.6.8.9 1.9.9 3.3v4.7c0 2.1-.8 3.2-2.3 3.2-.8 0-2.1-.4-3.8-1.2-5.7-2.6-12.1-3.9-19.2-3.9-5.7 0-10.2.9-13.3 2.8-3.1 1.9-4.7 4.8-4.7 8.9 0 2.8 1 5.2 3 7.1 2 1.9 5.7 3.8 11 5.5l14.2 4.5c7.2 2.3 12.4 5.5 15.5 9.6 3.1 4.1 4.6 8.8 4.6 14 0 4.3-.9 8.2-2.6 11.6-1.8 3.4-4.2 6.4-7.3 8.8-3.1 2.5-6.8 4.3-11.1 5.6-4.5 1.4-9.2 2.1-14.3 2.1z"
style={{
fill: "#252f3e",
}}
/>
<path
d="M273.5 143.7c-32.9 24.3-80.7 37.2-121.8 37.2-57.6 0-109.5-21.3-148.7-56.7-3.1-2.8-.3-6.6 3.4-4.4 42.4 24.6 94.7 39.5 148.8 39.5 36.5 0 76.6-7.6 113.5-23.2 5.5-2.5 10.2 3.6 4.8 7.6z"
className="st1"
/>
<path
d="M287.2 128.1c-4.2-5.4-27.8-2.6-38.5-1.3-3.2.4-3.7-2.4-.8-4.5 18.8-13.2 49.7-9.4 53.3-5 3.6 4.5-1 35.4-18.6 50.2-2.7 2.3-5.3 1.1-4.1-1.9 4-9.9 12.9-32.2 8.7-37.5z"
className="st1"
/>
</svg>
);
export default SvgAWS;

View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 304 182" style="enable-background:new 0 0 304 182;" xml:space="preserve">
<style type="text/css">
.st0{fill:#252F3E;}
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#FF9900;}
</style>
<g>
<path class="st0" d="M86.4,66.4c0,3.7,0.4,6.7,1.1,8.9c0.8,2.2,1.8,4.6,3.2,7.2c0.5,0.8,0.7,1.6,0.7,2.3c0,1-0.6,2-1.9,3l-6.3,4.2
c-0.9,0.6-1.8,0.9-2.6,0.9c-1,0-2-0.5-3-1.4C76.2,90,75,88.4,74,86.8c-1-1.7-2-3.6-3.1-5.9c-7.8,9.2-17.6,13.8-29.4,13.8
c-8.4,0-15.1-2.4-20-7.2c-4.9-4.8-7.4-11.2-7.4-19.2c0-8.5,3-15.4,9.1-20.6c6.1-5.2,14.2-7.8,24.5-7.8c3.4,0,6.9,0.3,10.6,0.8
c3.7,0.5,7.5,1.3,11.5,2.2v-7.3c0-7.6-1.6-12.9-4.7-16c-3.2-3.1-8.6-4.6-16.3-4.6c-3.5,0-7.1,0.4-10.8,1.3c-3.7,0.9-7.3,2-10.8,3.4
c-1.6,0.7-2.8,1.1-3.5,1.3c-0.7,0.2-1.2,0.3-1.6,0.3c-1.4,0-2.1-1-2.1-3.1v-4.9c0-1.6,0.2-2.8,0.7-3.5c0.5-0.7,1.4-1.4,2.8-2.1
c3.5-1.8,7.7-3.3,12.6-4.5c4.9-1.3,10.1-1.9,15.6-1.9c11.9,0,20.6,2.7,26.2,8.1c5.5,5.4,8.3,13.6,8.3,24.6V66.4z M45.8,81.6
c3.3,0,6.7-0.6,10.3-1.8c3.6-1.2,6.8-3.4,9.5-6.4c1.6-1.9,2.8-4,3.4-6.4c0.6-2.4,1-5.3,1-8.7v-4.2c-2.9-0.7-6-1.3-9.2-1.7
c-3.2-0.4-6.3-0.6-9.4-0.6c-6.7,0-11.6,1.3-14.9,4c-3.3,2.7-4.9,6.5-4.9,11.5c0,4.7,1.2,8.2,3.7,10.6
C37.7,80.4,41.2,81.6,45.8,81.6z M126.1,92.4c-1.8,0-3-0.3-3.8-1c-0.8-0.6-1.5-2-2.1-3.9L96.7,10.2c-0.6-2-0.9-3.3-0.9-4
c0-1.6,0.8-2.5,2.4-2.5h9.8c1.9,0,3.2,0.3,3.9,1c0.8,0.6,1.4,2,2,3.9l16.8,66.2l15.6-66.2c0.5-2,1.1-3.3,1.9-3.9c0.8-0.6,2.2-1,4-1
h8c1.9,0,3.2,0.3,4,1c0.8,0.6,1.5,2,1.9,3.9l15.8,67l17.3-67c0.6-2,1.3-3.3,2-3.9c0.8-0.6,2.1-1,3.9-1h9.3c1.6,0,2.5,0.8,2.5,2.5
c0,0.5-0.1,1-0.2,1.6c-0.1,0.6-0.3,1.4-0.7,2.5l-24.1,77.3c-0.6,2-1.3,3.3-2.1,3.9c-0.8,0.6-2.1,1-3.8,1h-8.6c-1.9,0-3.2-0.3-4-1
c-0.8-0.7-1.5-2-1.9-4L156,23l-15.4,64.4c-0.5,2-1.1,3.3-1.9,4c-0.8,0.7-2.2,1-4,1H126.1z M254.6,95.1c-5.2,0-10.4-0.6-15.4-1.8
c-5-1.2-8.9-2.5-11.5-4c-1.6-0.9-2.7-1.9-3.1-2.8c-0.4-0.9-0.6-1.9-0.6-2.8v-5.1c0-2.1,0.8-3.1,2.3-3.1c0.6,0,1.2,0.1,1.8,0.3
c0.6,0.2,1.5,0.6,2.5,1c3.4,1.5,7.1,2.7,11,3.5c4,0.8,7.9,1.2,11.9,1.2c6.3,0,11.2-1.1,14.6-3.3c3.4-2.2,5.2-5.4,5.2-9.5
c0-2.8-0.9-5.1-2.7-7c-1.8-1.9-5.2-3.6-10.1-5.2L246,52c-7.3-2.3-12.7-5.7-16-10.2c-3.3-4.4-5-9.3-5-14.5c0-4.2,0.9-7.9,2.7-11.1
c1.8-3.2,4.2-6,7.2-8.2c3-2.3,6.4-4,10.4-5.2c4-1.2,8.2-1.7,12.6-1.7c2.2,0,4.5,0.1,6.7,0.4c2.3,0.3,4.4,0.7,6.5,1.1
c2,0.5,3.9,1,5.7,1.6c1.8,0.6,3.2,1.2,4.2,1.8c1.4,0.8,2.4,1.6,3,2.5c0.6,0.8,0.9,1.9,0.9,3.3v4.7c0,2.1-0.8,3.2-2.3,3.2
c-0.8,0-2.1-0.4-3.8-1.2c-5.7-2.6-12.1-3.9-19.2-3.9c-5.7,0-10.2,0.9-13.3,2.8c-3.1,1.9-4.7,4.8-4.7,8.9c0,2.8,1,5.2,3,7.1
c2,1.9,5.7,3.8,11,5.5l14.2,4.5c7.2,2.3,12.4,5.5,15.5,9.6c3.1,4.1,4.6,8.8,4.6,14c0,4.3-0.9,8.2-2.6,11.6
c-1.8,3.4-4.2,6.4-7.3,8.8c-3.1,2.5-6.8,4.3-11.1,5.6C264.4,94.4,259.7,95.1,254.6,95.1z"/>
<g>
<path class="st1" d="M273.5,143.7c-32.9,24.3-80.7,37.2-121.8,37.2c-57.6,0-109.5-21.3-148.7-56.7c-3.1-2.8-0.3-6.6,3.4-4.4
c42.4,24.6,94.7,39.5,148.8,39.5c36.5,0,76.6-7.6,113.5-23.2C274.2,133.6,278.9,139.7,273.5,143.7z"/>
<path class="st1" d="M287.2,128.1c-4.2-5.4-27.8-2.6-38.5-1.3c-3.2,0.4-3.7-2.4-0.8-4.5c18.8-13.2,49.7-9.4,53.3-5
c3.6,4.5-1,35.4-18.6,50.2c-2.7,2.3-5.3,1.1-4.1-1.9C282.5,155.7,291.4,133.4,287.2,128.1z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

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

View file

@ -1,7 +1,8 @@
import { InfinityIcon } from "lucide-react";
import { Code } from "lucide-react";
import { forwardRef } from "react";
import ForwardedIconComponent from "../../components/genericIconComponent";
export const GradientSparkles = forwardRef<
export const GradientInfinity = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
@ -15,7 +16,63 @@ export const GradientSparkles = forwardRef<
</linearGradient>
</defs>
</svg>
<InfinityIcon stroke="url(#grad1)" ref={ref} {...props} />
<Code stroke="url(#grad1)" ref={ref} {...props} />
</>
);
});
export const GradientSave = forwardRef<
SVGSVGElement,
React.PropsWithChildren<{}>
>((props, ref) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad2" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent
name="Save"
stroke="url(#grad2)"
ref={ref}
{...props}
/>
</>
);
});
export const GradientGroup = (props) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad3" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent name="Combine" stroke="url(#grad3)" {...props} />
</>
);
};
export const GradientUngroup = (props) => {
return (
<>
<svg width="0" height="0" style={{ position: "absolute" }}>
<defs>
<linearGradient id="grad4" x1="0%" y1="0%" x2="100%" y2="0%">
<stop className="gradient-start" offset="0%" />
<stop className="gradient-end" offset="100%" />
</linearGradient>
</defs>
</svg>
<ForwardedIconComponent name="Ungroup" stroke="url(#grad4)" {...props} />
</>
);
};

View file

@ -0,0 +1,9 @@
const SvgShare = (props) => (
<svg width="1em" height="1em" {...props} xmlns="http://www.w3.org/2000/svg">
<path
d="M11.995 19.5a7.232 7.232 0 0 0 2.898-.585 7.582 7.582 0 0 0 2.392-1.627 7.748 7.748 0 0 0 1.625-2.398c.393-.904.59-1.868.59-2.89a7.172 7.172 0 0 0-.59-2.89 7.708 7.708 0 0 0-4.026-4.024 7.231 7.231 0 0 0-2.898-.586 7.209 7.209 0 0 0-2.888.586c-.905.39-1.7.932-2.387 1.626A7.788 7.788 0 0 0 5.09 9.11 7.172 7.172 0 0 0 4.5 12c0 1.022.197 1.986.59 2.89a7.748 7.748 0 0 0 1.625 2.398 7.582 7.582 0 0 0 2.392 1.627c.904.39 1.867.585 2.888.585Zm-3.687-4.238a.423.423 0 0 1-.316-.149c-.093-.099-.14-.247-.14-.446 0-.966.14-1.812.419-2.537.279-.725.721-1.29 1.328-1.696.607-.406 1.406-.609 2.396-.609h.093V8.366c0-.142.051-.266.154-.372a.514.514 0 0 1 .385-.158c.105 0 .198.024.279.07.08.047.182.129.306.246l3.167 2.956c.068.068.118.14.149.214.03.074.046.148.046.223a.578.578 0 0 1-.046.223.668.668 0 0 1-.149.213l-3.167 2.984c-.21.198-.409.297-.594.297a.52.52 0 0 1-.53-.511v-1.487h-.093c-.736 0-1.354.115-1.853.344-.498.229-.93.675-1.295 1.338-.075.136-.16.223-.256.26a.78.78 0 0 1-.283.056Z"
fill="currentColor"
></path>
</svg>
);
export default SvgShare;

View file

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

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.995 19.5a7.232 7.232 0 0 0 2.898-.585 7.582 7.582 0 0 0 2.392-1.627 7.748 7.748 0 0 0 1.625-2.398c.393-.904.59-1.868.59-2.89a7.172 7.172 0 0 0-.59-2.89 7.708 7.708 0 0 0-4.026-4.024 7.231 7.231 0 0 0-2.898-.586 7.209 7.209 0 0 0-2.888.586c-.905.39-1.7.932-2.387 1.626A7.788 7.788 0 0 0 5.09 9.11 7.172 7.172 0 0 0 4.5 12c0 1.022.197 1.986.59 2.89a7.748 7.748 0 0 0 1.625 2.398 7.582 7.582 0 0 0 2.392 1.627c.904.39 1.867.585 2.888.585Zm-3.687-4.238a.423.423 0 0 1-.316-.149c-.093-.099-.14-.247-.14-.446 0-.966.14-1.812.419-2.537.279-.725.721-1.29 1.328-1.696.607-.406 1.406-.609 2.396-.609h.093V8.366c0-.142.051-.266.154-.372a.514.514 0 0 1 .385-.158c.105 0 .198.024.279.07.08.047.182.129.306.246l3.167 2.956c.068.068.118.14.149.214.03.074.046.148.046.223a.578.578 0 0 1-.046.223.668.668 0 0 1-.149.213l-3.167 2.984c-.21.198-.409.297-.594.297a.52.52 0 0 1-.53-.511v-1.487h-.093c-.736 0-1.354.115-1.853.344-.498.229-.93.675-1.295 1.338-.075.136-.16.223-.256.26a.78.78 0 0 1-.283.056Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,17 @@
const SvgShare2 = (props) => (
<svg
xmlns="http://www.w3.org/2000/svg"
x="0px"
y="0px"
width="1em"
height="1em"
viewBox="0 0 485.213 485.212"
fill="currentColor"
{...props}
>
<g>
<path d="M394.236,212.282L272.934,333.584V272.93c0,0-121.304-30.324-181.955,60.654c0-100.483,81.469-181.956,181.955-181.956 V90.978L394.236,212.282z M485.212,242.606c0,133.976-108.627,242.606-242.604,242.606c-133.994,0-242.606-108.631-242.606-242.606 C0.001,108.628,108.613,0,242.607,0C376.585,0,485.212,108.628,485.212,242.606z M454.89,242.606 c0-117.038-95.241-212.279-212.282-212.279c-117.055,0-212.28,95.241-212.28,212.279c0,117.039,95.225,212.28,212.28,212.28 C359.648,454.886,454.89,359.645,454.89,242.606z" />
</g>
</svg>
);
export default SvgShare2;

View file

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

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M11.995 19.5a7.232 7.232 0 0 0 2.898-.585 7.582 7.582 0 0 0 2.392-1.627 7.748 7.748 0 0 0 1.625-2.398c.393-.904.59-1.868.59-2.89a7.172 7.172 0 0 0-.59-2.89 7.708 7.708 0 0 0-4.026-4.024 7.231 7.231 0 0 0-2.898-.586 7.209 7.209 0 0 0-2.888.586c-.905.39-1.7.932-2.387 1.626A7.788 7.788 0 0 0 5.09 9.11 7.172 7.172 0 0 0 4.5 12c0 1.022.197 1.986.59 2.89a7.748 7.748 0 0 0 1.625 2.398 7.582 7.582 0 0 0 2.392 1.627c.904.39 1.867.585 2.888.585Zm-3.687-4.238a.423.423 0 0 1-.316-.149c-.093-.099-.14-.247-.14-.446 0-.966.14-1.812.419-2.537.279-.725.721-1.29 1.328-1.696.607-.406 1.406-.609 2.396-.609h.093V8.366c0-.142.051-.266.154-.372a.514.514 0 0 1 .385-.158c.105 0 .198.024.279.07.08.047.182.129.306.246l3.167 2.956c.068.068.118.14.149.214.03.074.046.148.046.223a.578.578 0 0 1-.046.223.668.668 0 0 1-.149.213l-3.167 2.984c-.21.198-.409.297-.594.297a.52.52 0 0 1-.53-.511v-1.487h-.093c-.736 0-1.354.115-1.853.344-.498.229-.93.675-1.295 1.338-.075.136-.16.223-.256.26a.78.78 0 0 1-.283.056Z" fill="#000"></path></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -13,9 +13,12 @@ import {
// import "ace-builds/webpack-resolver";
import CodeTabsComponent from "../../components/codeTabsComponent";
import IconComponent from "../../components/genericIconComponent";
import { EXPORT_CODE_DIALOG } from "../../constants/constants";
import {
EXPORT_CODE_DIALOG,
LANGFLOW_SUPPORTED_TYPES,
} from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { TemplateVariableType } from "../../types/api";
import { tweakType, uniqueTweakType } from "../../types/components";
import { FlowType, NodeType } from "../../types/flow/index";
@ -45,7 +48,7 @@ const ApiModal = forwardRef(
const [activeTab, setActiveTab] = useState("0");
const tweak = useRef<tweakType>([]);
const tweaksList = useRef<string[]>([]);
const { setTweak, getTweak, tabsState } = useContext(TabsContext);
const { setTweak, getTweak, tabsState } = useContext(FlowsContext);
const pythonApiCode = getPythonApiCode(
flow,
autoLogin,
@ -99,15 +102,9 @@ const ApiModal = forwardRef(
(templateField) =>
templateField.charAt(0) !== "_" &&
node.data.node.template[templateField].show &&
(node.data.node.template[templateField].type === "str" ||
node.data.node.template[templateField].type === "bool" ||
node.data.node.template[templateField].type === "float" ||
node.data.node.template[templateField].type === "code" ||
node.data.node.template[templateField].type === "prompt" ||
node.data.node.template[templateField].type === "file" ||
node.data.node.template[templateField].type === "int" ||
node.data.node.template[templateField].type === "dict" ||
node.data.node.template[templateField].type === "NestedDict")
LANGFLOW_SUPPORTED_TYPES.has(
node.data.node.template[templateField].type
)
)
.map((n, i) => {
arrNodesWithValues.push(node["id"]);
@ -146,9 +143,9 @@ const ApiModal = forwardRef(
);
if (existingTweak) {
existingTweak[tw][template["name"]] = changes as string;
existingTweak[tw][template["name"]!] = changes as string;
if (existingTweak[tw][template["name"]] == template.value) {
if (existingTweak[tw][template["name"]!] == template.value) {
tweak.current.forEach((element) => {
if (element[tw] && Object.keys(element[tw])?.length === 0) {
tweak.current = tweak.current.filter((obj) => {
@ -161,7 +158,7 @@ const ApiModal = forwardRef(
} else {
const newTweak = {
[tw]: {
[template["name"]]: changes,
[template["name"]!]: changes,
},
} as uniqueTweakType;
tweak.current.push(newTweak);

View file

@ -1,30 +1,65 @@
import { useState } from "react";
import React, { useEffect, useState } from "react";
import ShadTooltip from "../../components/ShadTooltipComponent";
import { Button } from "../../components/ui/button";
import { ConfirmationModalType } from "../../types/components";
import { ConfirmationModalType, ContentProps } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal";
export default function ConfirmationModal({
const Content: React.FC<ContentProps> = ({ children }) => {
return <div className="h-full w-full">{children}</div>;
};
const Trigger: React.FC<ContentProps> = ({
children,
tolltipContent,
side,
}) => {
return tolltipContent ? (
<ShadTooltip side={side} content={tolltipContent}>
<div className="h-full w-full">{children}</div>
</ShadTooltip>
) : (
<div className="h-full w-full">{children}</div>
);
};
function ConfirmationModal({
title,
asChild,
titleHeader,
modalContent,
modalContentTitle,
cancelText,
confirmationText,
children,
destructive = false,
icon,
data,
index,
onConfirm,
size,
open,
onClose,
onCancel,
}: ConfirmationModalType) {
const Icon: any = nodeIconsLucide[icon];
const [modalOpen, setModalOpen] = useState(open ?? false);
useEffect(() => {
if (open) setModalOpen(open);
}, [open]);
useEffect(() => {
if (onClose) onClose!(modalOpen);
}, [modalOpen]);
const triggerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Trigger
);
const ContentChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Content
);
const [open, setOpen] = useState(false);
return (
<BaseModal size="x-small" open={open} setOpen={setOpen}>
<BaseModal.Trigger asChild={asChild}>{children}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader}>
<BaseModal size={size} open={modalOpen} setOpen={setModalOpen}>
<BaseModal.Trigger asChild={asChild}>{triggerChild}</BaseModal.Trigger>
<BaseModal.Header description={titleHeader ?? null}>
<span className="pr-2">{title}</span>
<Icon
name="icon"
@ -33,20 +68,21 @@ export default function ConfirmationModal({
/>
</BaseModal.Header>
<BaseModal.Content>
{modalContentTitle != "" && (
{modalContentTitle && modalContentTitle != "" && (
<>
<strong>{modalContentTitle}</strong>
<br></br>
</>
)}
<span>{modalContent}</span>
{ContentChild}
</BaseModal.Content>
<BaseModal.Footer>
<Button
className="ml-3"
variant={destructive ? "destructive" : "default"}
onClick={() => {
setOpen(false);
setModalOpen(false);
onConfirm(index, data);
}}
>
@ -54,9 +90,11 @@ export default function ConfirmationModal({
</Button>
<Button
className=""
variant="outline"
onClick={() => {
setOpen(false);
if (onCancel) onCancel();
setModalOpen(false);
}}
>
{cancelText}
@ -65,3 +103,7 @@ export default function ConfirmationModal({
</BaseModal>
);
}
ConfirmationModal.Content = Content;
ConfirmationModal.Trigger = Trigger;
export default ConfirmationModal;

View file

@ -0,0 +1,62 @@
import { DialogClose } from "@radix-ui/react-dialog";
import { Trash2 } from "lucide-react";
import { Button } from "../../components/ui/button";
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
export default function DeleteConfirmationModal({
children,
onConfirm,
description,
}: {
children: JSX.Element;
onConfirm: () => void;
description?: string;
}) {
return (
<Dialog>
<DialogTrigger tabIndex={-1}>{children}</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
<div className="flex items-center">
<span className="pr-2">Delete</span>
<Trash2
className="h-6 w-6 pl-1 text-foreground"
strokeWidth={1.5}
/>
</div>
</DialogTitle>
</DialogHeader>
<span>
Are you sure you want to delete this {description ?? "component"}?
<br></br>
This action cannot be undone.
</span>
<DialogFooter>
<DialogClose>
<Button className="mr-3" variant="outline">
Cancel
</Button>
<Button
type="submit"
variant="destructive"
onClick={() => {
onConfirm();
}}
>
Delete
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -1,12 +1,7 @@
import { cloneDeep } from "lodash";
import {
ReactNode,
forwardRef,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { forwardRef, useContext, useEffect, useState } from "react";
import { useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CodeAreaComponent from "../../components/codeAreaComponent";
import DictComponent from "../../components/dictComponent";
import Dropdown from "../../components/dropdownComponent";
@ -30,15 +25,20 @@ import {
TableHeader,
TableRow,
} from "../../components/ui/table";
import { limitScrollFieldsModal } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext";
import {
LANGFLOW_SUPPORTED_TYPES,
limitScrollFieldsModal,
} from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { NodeDataType } from "../../types/flow";
import { TabsState } from "../../types/tabs";
import { FlowsState } from "../../types/tabs";
import {
convertObjToArray,
convertValuesToNumbers,
hasDuplicateKeys,
scapedJSONStringfy,
} from "../../utils/reactflowUtils";
import { classNames } from "../../utils/utils";
import BaseModal from "../baseModal";
@ -47,66 +47,68 @@ const EditNodeModal = forwardRef(
(
{
data,
setData,
nodeLength,
children,
open,
onClose,
setOpen,
}: {
data: NodeDataType;
setData: (data: NodeDataType) => void;
nodeLength: number;
children: ReactNode;
open?: boolean;
onClose?: (close: boolean) => void;
open: boolean;
setOpen: (open: boolean) => void;
},
ref
) => {
const [modalOpen, setModalOpen] = useState(open ?? false);
const updateNodeInternals = useUpdateNodeInternals();
const myData = useRef(data);
const [myData, setMyData] = useState(data);
const { setTabsState, tabId } = useContext(TabsContext);
const { setTabsState, tabId } = useContext(FlowsContext);
const { reactFlowInstance } = useContext(typesContext);
let disabled =
reactFlowInstance
?.getEdges()
.some((edge) => edge.targetHandle === data.id) ?? false;
const { setModalContextOpen } = useContext(alertContext);
function changeAdvanced(n) {
myData.current.node!.template[n].advanced =
!myData.current.node!.template[n].advanced;
setAdv(!adv);
setMyData((old) => {
let newData = cloneDeep(old);
newData.node!.template[n].advanced =
!newData.node!.template[n].advanced;
return newData;
});
}
const handleOnNewValue = (newValue: any, name) => {
myData.current.node!.template[name].value = newValue;
setDataValue(newValue);
setMyData((old) => {
let newData = cloneDeep(old);
newData.node!.template[name].value = newValue;
return newData;
});
updateNodeInternals(data.id);
};
useEffect(() => {
myData.current = data; // reset data to what it is on node when opening modal
onClose!(modalOpen);
}, [modalOpen]);
if (open) {
setMyData(data); // reset data to what it is on node when opening modal
}
setModalContextOpen(open);
}, [open]);
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
const [adv, setAdv] = useState<boolean | null>(null);
const [dataValue, setDataValue] = useState(data);
return (
<BaseModal
key={data.id}
size="large-h-full"
open={modalOpen}
setOpen={setModalOpen}
open={open}
setOpen={setOpen}
onChangeOpenModal={(open) => {
myData.current = data;
setMyData(data);
}}
>
<BaseModal.Trigger>{children}</BaseModal.Trigger>
<BaseModal.Header description={myData.current.node?.description!}>
<span className="pr-2">{myData.current.type}</span>
<Badge variant="secondary">ID: {myData.current.id}</Badge>
<BaseModal.Trigger>
<></>
</BaseModal.Trigger>
<BaseModal.Header description={myData.node?.description!}>
<span className="pr-2">{myData.type}</span>
<Badge variant="secondary">ID: {myData.id}</Badge>
</BaseModal.Header>
<BaseModal.Content>
<div className="flex pb-2">
@ -123,7 +125,7 @@ const EditNodeModal = forwardRef(
"edit-node-modal-box",
nodeLength > limitScrollFieldsModal
? "overflow-scroll overflow-x-hidden custom-scroll"
: "overflow-hidden"
: ""
)}
>
{nodeLength > 0 && (
@ -139,349 +141,387 @@ const EditNodeModal = forwardRef(
</TableRow>
</TableHeader>
<TableBody className="p-0">
{Object.keys(myData.current.node!.template)
{Object.keys(myData.node!.template)
.filter(
(templateParam) =>
templateParam.charAt(0) !== "_" &&
myData.current.node?.template[templateParam].show &&
(myData.current.node.template[templateParam]
.type === "str" ||
myData.current.node.template[templateParam]
.type === "bool" ||
myData.current.node.template[templateParam]
.type === "float" ||
myData.current.node.template[templateParam]
.type === "code" ||
myData.current.node.template[templateParam]
.type === "prompt" ||
myData.current.node.template[templateParam]
.type === "file" ||
myData.current.node.template[templateParam]
.type === "int" ||
myData.current.node.template[templateParam]
.type === "dict" ||
myData.current.node.template[templateParam]
.type === "NestedDict")
myData.node?.template[templateParam].show &&
LANGFLOW_SUPPORTED_TYPES.has(
myData.node.template[templateParam].type
)
)
.map((templateParam, index) => (
<TableRow key={index} className="h-10">
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
{myData.current.node?.template[templateParam].name
? myData.current.node.template[templateParam]
.name
: myData.current.node?.template[templateParam]
.display_name}
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.current.node?.template[templateParam]
.type === "str" &&
!myData.current.node.template[templateParam]
.options ? (
<div className="mx-auto">
{myData.current.node.template[templateParam]
.list ? (
<InputListComponent
editNode={true}
disabled={disabled}
value={
!myData.current.node.template[
templateParam
].value ||
myData.current.node.template[
templateParam
].value === ""
? [""]
: myData.current.node.template[
templateParam
].value
.map((templateParam, index) => {
let id = {
inputTypes:
myData.node!.template[templateParam].input_types,
type: myData.node!.template[templateParam].type,
id: myData.id,
fieldName: templateParam,
};
let disabled =
reactFlowInstance?.getEdges().some(
(edge) =>
edge.targetHandle ===
scapedJSONStringfy(
myData.node!.template[templateParam].proxy
? {
...id,
proxy:
myData.node?.template[templateParam]
.proxy,
}
onChange={(value: string[]) => {
handleOnNewValue(value, templateParam);
: id
)
) ?? false;
return (
<TableRow key={index} className="h-10">
<TableCell className="truncate p-0 text-center text-sm text-foreground sm:px-3">
<ShadTooltip
content={
myData.node?.template[templateParam].proxy
? myData.node?.template[templateParam]
.proxy?.id
: null
}
>
<span>
{myData.node?.template[templateParam]
.display_name
? myData.node.template[templateParam]
.display_name
: myData.node?.template[templateParam]
.name}
</span>
</ShadTooltip>
</TableCell>
<TableCell className="w-[300px] p-0 text-center text-xs text-foreground ">
{myData.node?.template[templateParam].type ===
"str" &&
!myData.node.template[templateParam].options ? (
<div className="mx-auto">
{myData.node.template[templateParam]
.list ? (
<InputListComponent
editNode={true}
disabled={disabled}
value={
!myData.node.template[templateParam]
.value ||
myData.node.template[templateParam]
.value === ""
? [""]
: myData.node.template[
templateParam
].value
}
onChange={(value: string[]) => {
handleOnNewValue(
value,
templateParam
);
}}
/>
) : myData.node.template[templateParam]
.multiline ? (
<TextAreaComponent
id={"textarea-edit-" + index}
data-testid={"textarea-edit-" + index}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(
value: string | string[]
) => {
handleOnNewValue(
value,
templateParam
);
}}
/>
) : (
<InputComponent
id={"input-" + index}
editNode={true}
disabled={disabled}
password={
myData.node.template[templateParam]
.password ?? false
}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value) => {
handleOnNewValue(
value,
templateParam
);
}}
/>
)}
</div>
) : myData.node?.template[templateParam]
.type === "NestedDict" ? (
<div className=" w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={
myData.node!.template[
templateParam
]?.value?.toString() === "{}"
? {
yourkey: "value",
}
: myData.node!.template[templateParam]
.value
}
onChange={(newValue) => {
myData.node!.template[
templateParam
].value = newValue;
handleOnNewValue(
newValue,
templateParam
);
}}
id="editnode-div-dict-input"
/>
</div>
) : myData.node?.template[templateParam]
.type === "dict" ? (
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
myData.node!.template[templateParam].value
?.length > 1
? "my-3"
: ""
)}
>
<KeypairListComponent
disabled={disabled}
editNode={true}
value={
myData.node!.template[templateParam]
.value?.length === 0 ||
!myData.node!.template[templateParam]
.value
? [{ "": "" }]
: convertObjToArray(
myData.node!.template[
templateParam
].value
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers =
convertValuesToNumbers(newValue);
myData.node!.template[
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers)
);
handleOnNewValue(
valueToNumbers,
templateParam
);
}}
/>
) : myData.current.node.template[
templateParam
].multiline ? (
<TextAreaComponent
</div>
) : myData.node?.template[templateParam]
.type === "bool" ? (
<div className="ml-auto">
{" "}
<ToggleShadComponent
id={"toggle-edit-" + index}
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
enabled={
myData.node.template[templateParam]
.value
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
setEnabled={(isEnabled) => {
handleOnNewValue(
isEnabled,
templateParam
);
}}
size="small"
/>
) : (
<InputComponent
editNode={true}
</div>
) : myData.node?.template[templateParam]
.type === "float" ? (
<div className="mx-auto">
<FloatComponent
disabled={disabled}
password={
myData.current.node.template[
templateParam
].password ?? false
editNode={true}
rangeSpec={
myData.node!.template[templateParam]
.rangeSpec
}
value={
myData.current.node.template[
templateParam
].value ?? ""
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
)}
</div>
) : myData.current.node?.template[templateParam]
.type === "NestedDict" ? (
<div className=" w-full">
<DictComponent
disabled={disabled}
editNode={true}
value={
myData.current.node!.template[
templateParam
].value.toString() === "{}"
? {
yourkey: "value",
}
: myData.current.node!.template[
templateParam
].value
}
onChange={(newValue) => {
myData.current.node!.template[
templateParam
].value = newValue;
handleOnNewValue(newValue, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "dict" ? (
<div
className={classNames(
"max-h-48 w-full overflow-auto custom-scroll",
myData.current.node!.template[templateParam]
.value?.length > 1
? "my-3"
: ""
)}
>
<KeypairListComponent
dataValue={dataValue}
advanced={adv}
disabled={disabled}
editNode={true}
value={
myData.current.node!.template[
templateParam
].value?.length === 0 ||
!myData.current.node!.template[
templateParam
].value
? [{ "": "" }]
: convertObjToArray(
myData.current.node!.template[
templateParam
].value
)
}
duplicateKey={errorDuplicateKey}
onChange={(newValue) => {
const valueToNumbers =
convertValuesToNumbers(newValue);
myData.current.node!.template[
templateParam
].value = valueToNumbers;
setErrorDuplicateKey(
hasDuplicateKeys(valueToNumbers)
);
handleOnNewValue(
valueToNumbers,
templateParam
);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "bool" ? (
<div className="ml-auto">
{" "}
</div>
) : myData.node?.template[templateParam]
.type === "str" &&
myData.node.template[templateParam]
.options ? (
<div className="mx-auto">
<Dropdown
numberOfOptions={nodeLength}
editNode={true}
options={
myData.node.template[templateParam]
.options
}
onSelect={(value) =>
handleOnNewValue(value, templateParam)
}
value={
myData.node.template[templateParam]
.value ?? "Choose an option"
}
id={"dropdown-edit-" + index}
></Dropdown>
</div>
) : myData.node?.template[templateParam]
.type === "int" ? (
<div className="mx-auto">
<IntComponent
id={"edit-int-input-" + index}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.node?.template[templateParam]
.type === "file" ? (
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.node.template[templateParam]
.fileTypes
}
onFileChange={(filePath: string) => {
data.node!.template[
templateParam
].file_path = filePath;
}}
></InputFileComponent>
</div>
) : myData.node?.template[templateParam]
.type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
readonly={
myData.node?.flow ? true : false
}
field_name={templateParam}
editNode={true}
disabled={disabled}
nodeClass={myData.node}
setNodeClass={(nodeClass) => {
myData.node = nodeClass;
}}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={"prompt-area-edit" + index}
data-testid={
"modal-prompt-input-" + index
}
/>
</div>
) : myData.node?.template[templateParam]
.type === "code" ? (
<div className="mx-auto">
<CodeAreaComponent
readonly={
myData.node?.flow &&
myData.node.template[templateParam]
.dynamic
? true
: false
}
dynamic={
data.node!.template[templateParam]
.dynamic ?? false
}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
}}
nodeClass={data.node}
disabled={disabled}
editNode={true}
value={
myData.node.template[templateParam]
.value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
id={"code-area-edit" + index}
/>
</div>
) : myData.node?.template[templateParam]
.type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</TableCell>
<TableCell className="p-0 text-right">
<div className="items-center text-center">
<ToggleShadComponent
disabled={disabled}
enabled={
myData.current.node.template[
templateParam
].value
id={
"show" +
myData.node?.template[templateParam].name
}
setEnabled={(isEnabled) => {
handleOnNewValue(
isEnabled,
templateParam
);
enabled={
!myData.node?.template[templateParam]
.advanced
}
setEnabled={(e) => {
changeAdvanced(templateParam);
}}
disabled={disabled}
size="small"
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "float" ? (
<div className="mx-auto">
<FloatComponent
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "str" &&
myData.current.node.template[templateParam]
.options ? (
<div className="mx-auto">
<Dropdown
numberOfOptions={nodeLength}
editNode={true}
options={
myData.current.node.template[
templateParam
].options
}
onSelect={(value) =>
handleOnNewValue(value, templateParam)
}
value={
myData.current.node.template[
templateParam
].value ?? "Choose an option"
}
></Dropdown>
</div>
) : myData.current.node?.template[templateParam]
.type === "int" ? (
<div className="mx-auto">
<IntComponent
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "file" ? (
<div className="mx-auto">
<InputFileComponent
editNode={true}
disabled={disabled}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
fileTypes={
myData.current.node.template[
templateParam
].fileTypes
}
suffixes={
myData.current.node.template[
templateParam
].suffixes
}
onFileChange={(filePath: string) => {
data.node!.template[
templateParam
].file_path = filePath;
}}
></InputFileComponent>
</div>
) : myData.current.node?.template[templateParam]
.type === "prompt" ? (
<div className="mx-auto">
<PromptAreaComponent
field_name={templateParam}
editNode={true}
disabled={disabled}
nodeClass={myData.current.node}
setNodeClass={(nodeClass) => {
myData.current.node = nodeClass;
}}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "code" ? (
<div className="mx-auto">
<CodeAreaComponent
dynamic={
data.node!.template[templateParam]
.dynamic ?? false
}
setNodeClass={(nodeClass) => {
data.node = nodeClass;
}}
nodeClass={data.node}
disabled={disabled}
editNode={true}
value={
myData.current.node.template[
templateParam
].value ?? ""
}
onChange={(value: string | string[]) => {
handleOnNewValue(value, templateParam);
}}
/>
</div>
) : myData.current.node?.template[templateParam]
.type === "Any" ? (
"-"
) : (
<div className="hidden"></div>
)}
</TableCell>
<TableCell className="p-0 text-right">
<div className="items-center text-center">
<ToggleShadComponent
enabled={
!myData.current.node?.template[
templateParam
].advanced
}
setEnabled={(e) => {
changeAdvanced(templateParam);
}}
disabled={disabled}
size="small"
/>
</div>
</TableCell>
</TableRow>
))}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
@ -492,12 +532,12 @@ const EditNodeModal = forwardRef(
<BaseModal.Footer>
<Button
id={"saveChangesBtn"}
className="mt-3"
onClick={() => {
const newData = cloneDeep(myData.current);
myData.current = newData;
data.node = myData.node;
//@ts-ignore
setTabsState((prev: TabsState) => {
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
@ -506,7 +546,7 @@ const EditNodeModal = forwardRef(
},
};
});
setModalOpen(false);
setOpen(false);
}}
type="submit"
>

View file

@ -3,14 +3,9 @@ import { useContext, useEffect, useRef, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { CONTROL_NEW_API_KEY } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { createApiKey } from "../../controllers/API";
import {
ApiKeyInputType,
ApiKeyType,
inputHandlerEventType,
} from "../../types/components";
import { ApiKeyType } from "../../types/components";
import { nodeIconsLucide } from "../../utils/styleUtils";
import BaseModal from "../baseModal";
@ -27,19 +22,11 @@ export default function SecretKeyModal({
const [open, setOpen] = useState(false);
const [apiKeyName, setApiKeyName] = useState(data?.apikeyname ?? "");
const [apiKeyValue, setApiKeyValue] = useState("");
const [inputState, setInputState] =
useState<ApiKeyInputType>(CONTROL_NEW_API_KEY);
const [renderKey, setRenderKey] = useState(false);
const [textCopied, setTextCopied] = useState(true);
const { setSuccessData } = useContext(alertContext);
const inputRef = useRef<HTMLInputElement | null>(null);
function handleInput({
target: { name, value },
}: inputHandlerEventType): void {
setInputState((prev) => ({ ...prev, [name]: value }));
}
useEffect(() => {
if (open) {
setRenderKey(false);
@ -101,14 +88,7 @@ export default function SecretKeyModal({
</span>
<div className="flex pt-3">
<div className="w-full">
<Input
ref={inputRef}
onChange={(event) => {
setApiKeyValue(event.target.value);
}}
readOnly={true}
value={apiKeyValue}
/>
<Input ref={inputRef} readOnly={true} value={apiKeyValue} />
</div>
<div>
@ -153,7 +133,6 @@ export default function SecretKeyModal({
<Form.Control asChild>
<input
onChange={({ target: { value } }) => {
handleInput({ target: { name: "apikeyname", value } });
setApiKeyName(value);
}}
value={apiKeyName}

View file

@ -0,0 +1,124 @@
import * as Form from "@radix-ui/react-form";
import { useContext, useState } from "react";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { StoreContext } from "../../contexts/storeContext";
import { addApiKeyStore } from "../../controllers/API";
import { StoreApiKeyType } from "../../types/components";
import BaseModal from "../baseModal";
export default function StoreApiKeyModal({
children,
disabled = false,
}: StoreApiKeyType) {
if (disabled) return <>{children}</>;
const [open, setOpen] = useState(false);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { storeApiKey } = useContext(AuthContext);
const { hasApiKey, validApiKey } = useContext(StoreContext);
const [apiKeyValue, setApiKeyValue] = useState("");
const handleSaveKey = () => {
if (apiKeyValue) {
addApiKeyStore(apiKeyValue).then(
() => {
setSuccessData({
title: "Success! Your API Key has been saved.",
});
storeApiKey(apiKeyValue);
setOpen(false);
},
(error) => {
setErrorData({
title: "There was an error saving the API Key, please try again.",
list: [error["response"]["data"]["detail"]],
});
}
);
}
};
return (
<BaseModal size="small-h-full" open={open && !disabled} setOpen={setOpen}>
<BaseModal.Trigger asChild>{children}</BaseModal.Trigger>
<BaseModal.Header
description={
(hasApiKey && !validApiKey
? "Your API key is not valid. "
: !hasApiKey
? "You don't have an API key. "
: "") + "Insert your Langflow API key."
}
>
<span className="pr-2">API Key</span>
<IconComponent
name="Key"
className="h-6 w-6 pl-1 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<Form.Root
onSubmit={(event) => {
event.preventDefault();
}}
>
<div className="grid gap-5">
<Form.Field name="apikey">
<div className="flex items-center justify-between gap-2">
<Form.Control asChild>
<Input
//fake api key
value={apiKeyValue}
type="password"
onChange={({ target: { value } }) => {
setApiKeyValue(value);
}}
placeholder="Insert your API Key"
/>
</Form.Control>
</div>
</Form.Field>
</div>
<div className="flex items-end justify-between">
<span className="pr-1 text-xs text-muted-foreground">
Dont have an API key? Sign up at{" "}
<a
className="text-high-indigo underline"
href="https://langflow.store/"
target="_blank"
>
langflow.store
</a>
</span>
<div className="">
<Button
className="mr-3"
variant="outline"
onClick={() => {
setOpen(false);
}}
>
Cancel
</Button>
<Form.Submit asChild>
<Button
className="mt-8"
onClick={() => {
handleSaveKey();
}}
>
Save
</Button>
</Form.Submit>
</div>
</div>
</Form.Root>
</BaseModal.Content>
</BaseModal>
);
}

View file

@ -67,7 +67,8 @@ interface BaseModalProps {
| "large"
| "large-h-full"
| "small-h-full"
| "medium-h-full";
| "medium-h-full"
| "smaller-h-full";
disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void;
@ -98,11 +99,15 @@ function BaseModal({
switch (size) {
case "x-small":
minWidth = "min-w-[20vw]";
height = "h-[10vh]";
height = " ";
break;
case "smaller":
minWidth = "min-w-[40vw]";
height = "h-[27vh]";
height = "h-[11rem]";
break;
case "smaller-h-full":
minWidth = "min-w-[40vw]";
height = "h-full";
break;
case "small":
minWidth = "min-w-[40vw]";
@ -145,9 +150,7 @@ function BaseModal({
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div className={`mt-2 flex flex-col ${height!} w-full `}>
{ContentChild}
</div>
<div className={`flex flex-col ${height!} w-full `}>{ContentChild}</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}

View file

@ -8,6 +8,7 @@ import { useContext, useEffect, useState } from "react";
import AceEditor from "react-ace";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { CODE_PROMPT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { darkContext } from "../../contexts/darkContext";
@ -23,6 +24,7 @@ export default function CodeAreaModal({
setNodeClass,
children,
dynamic,
readonly = false,
}: codeAreaModalPropsType): JSX.Element {
const [code, setCode] = useState(value);
const { dark } = useContext(darkContext);
@ -86,8 +88,7 @@ export default function CodeAreaModal({
.then((apiReturn) => {
const { data } = apiReturn;
if (data) {
setNodeClass(data);
setValue(code);
setNodeClass(data, code);
setError({ detail: { error: undefined, traceback: undefined } });
setOpen(false);
}
@ -143,9 +144,15 @@ export default function CodeAreaModal({
/>
</BaseModal.Header>
<BaseModal.Content>
<Input
value={code}
className="absolute left-[500%] top-[500%]"
id="codeValue"
/>
<div className="flex h-full w-full flex-col transition-all">
<div className="h-full w-full">
<AceEditor
readOnly={readonly}
value={code}
mode="python"
height={height ?? "100%"}
@ -180,7 +187,13 @@ export default function CodeAreaModal({
</div>
</div>
<div className="flex h-fit w-full justify-end">
<Button className="mt-3" onClick={handleClick} type="submit">
<Button
className="mt-3"
onClick={handleClick}
type="submit"
id="checkAndSaveBtn"
disabled={readonly}
>
Check & Save
</Button>
</div>

View file

@ -54,6 +54,7 @@ export default function DictAreaModal({
/>
<div className="flex h-fit w-full justify-end">
<Button
data-testid="save-dict-button"
className="mt-3"
type="submit"
onClick={() => {

View file

@ -4,14 +4,18 @@ import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { EXPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { typesContext } from "../../contexts/typesContext";
import { removeApiKeys } from "../../utils/reactflowUtils";
import BaseModal from "../baseModal";
const ExportModal = forwardRef(
(props: { children: ReactNode }, ref): JSX.Element => {
const { flows, tabId, downloadFlow } = useContext(TabsContext);
const [checked, setChecked] = useState(false);
const { flows, tabId, downloadFlow, version } = useContext(FlowsContext);
const { reactFlowInstance } = useContext(typesContext);
const { setNoticeData } = useContext(alertContext);
const [checked, setChecked] = useState(true);
const flow = flows.find((f) => f.id === tabId);
useEffect(() => {
setName(flow!.name);
@ -22,7 +26,7 @@ const ExportModal = forwardRef(
const [open, setOpen] = useState(false);
return (
<BaseModal size="smaller" open={open} setOpen={setOpen}>
<BaseModal size="smaller-h-full" open={open} setOpen={setOpen}>
<BaseModal.Trigger>{props.children}</BaseModal.Trigger>
<BaseModal.Header description={EXPORT_DIALOG_SUBTITLE}>
<span className="pr-2">Export</span>
@ -36,14 +40,13 @@ const ExportModal = forwardRef(
<EditFlowSettings
name={name}
description={description}
flows={flows}
tabId={tabId}
setName={setName}
setDescription={setDescription}
/>
<div className="mt-3 flex items-center space-x-2">
<Checkbox
id="terms"
checked={checked}
onCheckedChange={(event: boolean) => {
setChecked(event);
}}
@ -52,20 +55,42 @@ const ExportModal = forwardRef(
Save with my API keys
</label>
</div>
<span className=" text-xs text-destructive ">
Caution: Uncheck this box only removes API keys from fields
specifically designated for API keys.
</span>
</BaseModal.Content>
<BaseModal.Footer>
<Button
onClick={() => {
if (checked)
if (checked) {
downloadFlow(
flows.find((flow) => flow.id === tabId)!,
{
id: tabId,
data: flow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
},
name!,
description
);
else
setNoticeData({
title:
"Warning: Critical data, JSON file may include API keys.",
});
} else
downloadFlow(
removeApiKeys(flows.find((flow) => flow.id === tabId)!),
removeApiKeys({
id: tabId,
data: flow!.data!,
description,
name,
last_tested_version: version,
is_component: false,
}),
name!,
description
);

View file

@ -3,15 +3,16 @@ import EditFlowSettings from "../../components/EditFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
import { Button } from "../../components/ui/button";
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { FlowSettingsPropsType } from "../../types/components";
import { FlowType } from "../../types/flow";
import BaseModal from "../baseModal";
export default function FlowSettingsModal({
open,
setOpen,
}: FlowSettingsPropsType): JSX.Element {
const { flows, tabId, updateFlow, saveFlow } = useContext(TabsContext);
const { flows, tabId, saveFlow } = useContext(FlowsContext);
const flow = flows.find((f) => f.id === tabId);
useEffect(() => {
setName(flow!.name);
@ -19,7 +20,6 @@ export default function FlowSettingsModal({
}, [flow!.name, flow!.description]);
const [name, setName] = useState(flow!.name);
const [description, setDescription] = useState(flow!.description);
const [invalidName, setInvalidName] = useState(false);
function handleClick(): void {
let savedFlow = flows.find((flow) => flow.id === tabId);
@ -28,6 +28,17 @@ export default function FlowSettingsModal({
saveFlow(savedFlow!);
setOpen(false);
}
const [nameLists, setNameList] = useState<string[]>([]);
useEffect(() => {
const tempNameList: string[] = [];
flows.forEach((flow: FlowType) => {
if ((flow.is_component ?? false) === false) tempNameList.push(flow.name);
});
setNameList(tempNameList.filter((name) => name !== flow!.name));
}, [flows]);
return (
<BaseModal open={open} setOpen={setOpen} size="smaller">
<BaseModal.Header description={SETTINGS_DIALOG_SUBTITLE}>
@ -36,19 +47,20 @@ export default function FlowSettingsModal({
</BaseModal.Header>
<BaseModal.Content>
<EditFlowSettings
invalidName={invalidName}
setInvalidName={setInvalidName}
invalidNameList={nameLists}
name={name}
description={description}
flows={flows}
tabId={tabId}
setName={setName}
setDescription={setDescription}
/>
</BaseModal.Content>
<BaseModal.Footer>
<Button disabled={invalidName} onClick={handleClick} type="submit">
<Button
disabled={nameLists.includes(name) && name !== flow!.name}
onClick={handleClick}
type="submit"
>
Save
</Button>
</BaseModal.Footer>

View file

@ -24,9 +24,9 @@ import {
import { Textarea } from "../../components/ui/textarea";
import { CHAT_FORM_DIALOG_SUBTITLE } from "../../constants/constants";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { getBuildStatus } from "../../controllers/API";
import { TabsState } from "../../types/tabs";
import { FlowsState } from "../../types/tabs";
import { validateNodes } from "../../utils/reactflowUtils";
export default function FormModal({
@ -38,7 +38,7 @@ export default function FormModal({
setOpen: (open: boolean) => void;
flow: FlowType;
}): JSX.Element {
const { tabsState, setTabsState } = useContext(TabsContext);
const { tabsState, setTabsState } = useContext(FlowsContext);
const [chatValue, setChatValue] = useState(() => {
try {
const { formKeysData } = tabsState[flow.id];
@ -393,7 +393,7 @@ export default function FormModal({
const message = inputs;
addChatHistory(message!, true, chatKey!, template.current);
sendAll({
...reactFlowInstance?.toObject()!,
...flow.data!,
inputs: inputs!,
chatHistory,
name: flow.name,
@ -401,7 +401,7 @@ export default function FormModal({
chatKey: chatKey!,
});
//@ts-ignore
setTabsState((old: TabsState) => {
setTabsState((old: FlowsState) => {
if (!chatKey) return old;
let newTabsState = _.cloneDeep(old);
newTabsState[id.current].formKeysData.input_keys![chatKey] = "";
@ -522,7 +522,7 @@ export default function FormModal({
}
onChange={(e) => {
//@ts-ignore
setTabsState((old: TabsState) => {
setTabsState((old: FlowsState) => {
let newTabsState = _.cloneDeep(old);
newTabsState[
id.current
@ -634,7 +634,7 @@ export default function FormModal({
setChatValue={(value) => {
setChatValue(value);
//@ts-ignore
setTabsState((old: TabsState) => {
setTabsState((old: FlowsState) => {
let newTabsState = _.cloneDeep(old);
newTabsState[id.current].formKeysData.input_keys![
chatKey!

View file

@ -30,6 +30,8 @@ export default function GenericModal({
nodeClass,
setNodeClass,
children,
id = "",
readonly = false,
}: genericModalPropsType): JSX.Element {
const [myButtonText] = useState(buttonText);
const [myModalTitle] = useState(modalTitle);
@ -38,7 +40,7 @@ export default function GenericModal({
const [inputValue, setInputValue] = useState(value);
const [isEdit, setIsEdit] = useState(true);
const [wordsHighlight, setWordsHighlight] = useState<string[]>([]);
const { setErrorData, setSuccessData, setNoticeData } =
const { setErrorData, setSuccessData, setNoticeData, setModalContextOpen } =
useContext(alertContext);
const ref = useRef();
const divRef = useRef(null);
@ -104,15 +106,14 @@ export default function GenericModal({
: "code-nohighlight";
}
// Function need some review, working for now
function validatePrompt(closeModal: boolean): void {
//nodeClass is always null on tweaks
postValidatePrompt(field_name, inputValue, nodeClass!)
.then((apiReturn) => {
// if field_name is an empty string, then we need to set it
// to the first key of the custom_fields object
if (field_name === "") {
console.log(apiReturn.data?.frontend_node?.custom_fields);
field_name = Array.isArray(
apiReturn.data?.frontend_node?.custom_fields?.[""]
)
@ -121,38 +122,23 @@ export default function GenericModal({
}
if (apiReturn.data) {
let inputVariables = apiReturn.data.input_variables ?? [];
if (inputVariables && inputVariables.length === 0) {
if (!inputVariables || inputVariables.length === 0) {
setIsEdit(true);
setNoticeData({
title: "Your template does not have any variables.",
});
setModalOpen(false);
if (
JSON.stringify(apiReturn.data?.frontend_node) !==
JSON.stringify({})
)
setNodeClass!(apiReturn.data?.frontend_node);
setModalOpen(closeModal);
setValue(inputValue);
if (field_name !== "") {
apiReturn.data.frontend_node["template"][field_name]["value"] =
inputValue;
}
} else {
setIsEdit(false);
setSuccessData({
title: "Prompt is ready",
});
if (
JSON.stringify(apiReturn.data?.frontend_node) !==
JSON.stringify({})
)
setNodeClass!(apiReturn.data?.frontend_node);
setModalOpen(closeModal);
setValue(inputValue);
if (field_name !== "") {
apiReturn.data.frontend_node["template"][field_name]["value"] =
inputValue;
) {
setNodeClass!(apiReturn.data?.frontend_node, inputValue);
setModalOpen(closeModal);
setIsEdit(false);
setSuccessData({
title: "Prompt is ready",
});
}
}
} else {
@ -172,6 +158,10 @@ export default function GenericModal({
});
}
useEffect(() => {
setModalContextOpen(modalOpen);
}, [modalOpen]);
return (
<BaseModal
onChangeOpenModal={(open) => {}}
@ -193,7 +183,9 @@ export default function GenericModal({
}
})()}
>
<span className="pr-2">{myModalTitle}</span>
<span className="pr-2" data-testid="modal-title">
{myModalTitle}
</span>
<IconComponent
name="FileText"
className="h-6 w-6 pl-1 text-primary "
@ -208,8 +200,10 @@ export default function GenericModal({
"flex h-full w-full"
)}
>
{type === TypeModal.PROMPT && isEdit ? (
{type === TypeModal.PROMPT && isEdit && !readonly ? (
<Textarea
id={"modal-" + id}
data-testid={"modal-" + id}
ref={divRefPrompt}
className="form-input h-full w-full rounded-lg custom-scroll focus-visible:ring-1"
value={inputValue}
@ -226,7 +220,7 @@ export default function GenericModal({
handleKeyDown(e, inputValue, "");
}}
/>
) : type === TypeModal.PROMPT && !isEdit ? (
) : type === TypeModal.PROMPT && (!isEdit || readonly) ? (
<SanitizedHTMLWrapper
className={getClassByNumberLength()}
content={coloredContent}
@ -248,6 +242,9 @@ export default function GenericModal({
onKeyDown={(e) => {
handleKeyDown(e, value, "");
}}
readOnly={readonly}
id={"text-area-modal"}
data-testid={"text-area-modal"}
/>
) : (
<></>
@ -284,7 +281,7 @@ export default function GenericModal({
className="m-1 max-w-[40vw] cursor-default truncate p-2.5 text-sm"
>
<div className="relative bottom-[1px]">
<span>
<span id={"badge" + index.toString()}>
{word.replace(/[{}]/g, "").length > 59
? word.replace(/[{}]/g, "").slice(0, 56) +
"..."
@ -304,6 +301,9 @@ export default function GenericModal({
)}
</div>
<Button
data-testid="genericModalBtnSave"
id="genericModalBtnSave"
disabled={readonly}
onClick={() => {
switch (myModalType) {
case TypeModal.TEXT:

View file

@ -1,101 +0,0 @@
import { buttonBoxPropsType } from "../../../types/components";
import { classNames } from "../../../utils/utils";
export default function ButtonBox({
onClick,
title,
description,
icon,
bgColor,
textColor,
deactivate,
size,
}: buttonBoxPropsType): JSX.Element {
let bigCircle: string;
let smallCircle: string;
let titleFontSize: string;
let descriptionFontSize: string;
let padding: string;
let marginTop: string;
let height: string;
let width: string;
let textHeight: number;
let textWidth: number;
switch (size) {
case "small":
bigCircle = "h-12 w-12";
smallCircle = "h-8 w-8";
titleFontSize = "text-sm";
descriptionFontSize = "text-xs";
padding = "p-2 py-3";
marginTop = "mt-2";
height = "h-36";
width = "w-32";
break;
case "medium":
bigCircle = "h-16 w-16";
smallCircle = "h-12 w-12";
titleFontSize = "text-base";
descriptionFontSize = "text-sm";
padding = "p-4 py-5";
marginTop = "mt-3";
height = "h-44";
width = "w-36";
break;
case "big":
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
default:
bigCircle = "h-20 w-20";
smallCircle = "h-16 w-16";
titleFontSize = "text-lg";
descriptionFontSize = "text-sm";
padding = "p-8 py-10";
marginTop = "mt-6";
height = "h-56";
width = "w-44";
break;
}
return (
<button disabled={deactivate} onClick={onClick}>
<div
className={classNames(
"button-box-modal-div",
bgColor,
height,
width,
padding
)}
>
<div
className={`flex items-center justify-center ${bigCircle} mb-1 rounded-full bg-background/30`}
>
<div
className={`flex items-center justify-center ${smallCircle} rounded-full bg-background`}
>
<div className={textColor}>{icon}</div>
</div>
</div>
<div className="mb-auto mt-auto w-full">
<h3
className={classNames(
"w-full font-semibold text-background truncate-multiline word-break-break-word",
titleFontSize,
marginTop
)}
>
{title}
</h3>
</div>
</div>
</button>
);
}

View file

@ -1,190 +0,0 @@
import {
ArrowLeftIcon,
ArrowUpTrayIcon,
ComputerDesktopIcon,
DocumentDuplicateIcon,
} from "@heroicons/react/24/outline";
import { useContext, useRef, useState } from "react";
import LoadingComponent from "../../components/loadingComponent";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";
import { IMPORT_DIALOG_SUBTITLE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
import { classNames } from "../../utils/utils";
import ButtonBox from "./buttonBox";
export default function ImportModal(): JSX.Element {
const [open, setOpen] = useState(true);
const { setErrorData } = useContext(alertContext);
const ref = useRef();
const [showExamples, setShowExamples] = useState(false);
const [loadingExamples, setLoadingExamples] = useState(false);
const [examples, setExamples] = useState<FlowType[]>([]);
const { uploadFlow, addFlow } = useContext(TabsContext);
function handleExamples(): void {
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 [modalOpen, setModalOpen] = useState(false);
return (
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
<DialogTrigger></DialogTrigger>
<DialogContent
className={classNames(
showExamples
? "h-[600px] lg:max-w-[650px]"
: "h-[450px] lg:max-w-[650px]"
)}
>
<DialogHeader>
<DialogTitle className="flex items-center">
{showExamples && (
<>
<div className="dialog-header-modal-div">
<button
type="button"
className="dialog-header-modal-button disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
onClick={() => {
setShowExamples(false);
}}
>
<ArrowLeftIcon
className="ml-1 h-5 w-5 text-foreground"
aria-hidden="true"
/>
</button>
</div>
</>
)}
<span className={classNames(showExamples ? "pl-8 pr-2" : "pr-2")}>
{showExamples ? "Select an example" : "Import"}
</span>
<ArrowUpTrayIcon
className="ml-1 h-5 w-5 text-foreground"
aria-hidden="true"
/>
</DialogTitle>
<DialogDescription>{IMPORT_DIALOG_SUBTITLE}</DialogDescription>
</DialogHeader>
<div
className={classNames(
"dialog-modal-examples-div",
showExamples && !loadingExamples
? "dialog-modal-example-true"
: "dialog-modal-example-false"
)}
>
{!showExamples && (
<div className="dialog-modal-button-box-div">
<ButtonBox
size="big"
bgColor="bg-medium-emerald "
description="Prebuilt Examples"
icon={<DocumentDuplicateIcon className="document-icon" />}
onClick={() => {
setShowExamples(true);
handleExamples();
}}
textColor="text-medium-emerald "
title="Examples"
></ButtonBox>
<ButtonBox
size="big"
bgColor="bg-almost-dark-blue "
description="Import from Local"
icon={<ComputerDesktopIcon className="document-icon" />}
onClick={() => {
uploadFlow();
setModalOpen(false);
}}
textColor="text-almost-dark-blue "
title="Local File"
></ButtonBox>
</div>
)}
{showExamples && loadingExamples && (
<div className="loading-component-div">
<LoadingComponent remSize={30} />
</div>
)}
{showExamples &&
!loadingExamples &&
examples.map((example, index) => {
return (
<div key={example.name} className="m-2">
{" "}
<ButtonBox
size="small"
bgColor="bg-medium-emerald "
description={example.description ?? "Prebuilt Examples"}
icon={
<DocumentDuplicateIcon
strokeWidth={1.5}
className="h-6 w-6 flex-shrink-0"
/>
}
onClick={() => {
addFlow(example, false);
setModalOpen(false);
}}
textColor="text-medium-emerald "
title={example.name}
></ButtonBox>
</div>
);
})}
</div>
<DialogFooter>
<div className="dialog-modal-footer">
<a
href="https://github.com/logspace-ai/langflow_examples"
target="_blank"
className="dialog-modal-footer-link "
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>
);
}

View file

@ -0,0 +1,298 @@
import { Loader2 } from "lucide-react";
import { ReactNode, useContext, useEffect, useMemo, useState } from "react";
import EditFlowSettings from "../../components/EditFlowSettingsComponent";
import IconComponent from "../../components/genericIconComponent";
import { TagsSelector } from "../../components/tagsSelectorComponent";
import { Button } from "../../components/ui/button";
import { Checkbox } from "../../components/ui/checkbox";
import { alertContext } from "../../contexts/alertContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext";
import { typesContext } from "../../contexts/typesContext";
import {
getStoreComponents,
getStoreTags,
saveFlowStore,
updateFlowStore,
} from "../../controllers/API";
import { FlowType } from "../../types/flow";
import {
downloadNode,
removeApiKeys,
removeFileNameFromComponents,
} from "../../utils/reactflowUtils";
import { getTagsIds } from "../../utils/storeUtils";
import ConfirmationModal from "../ConfirmationModal";
import BaseModal from "../baseModal";
export default function ShareModal({
component,
is_component,
children,
open,
setOpen,
disabled,
}: {
children?: ReactNode;
is_component: boolean;
component: FlowType;
open?: boolean;
setOpen?: (open: boolean) => void;
disabled?: boolean;
}): JSX.Element {
const { version, addFlow, downloadFlow } = useContext(FlowsContext);
const { hasApiKey, hasStore } = useContext(StoreContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { reactFlowInstance } = useContext(typesContext);
const [internalOpen, internalSetOpen] = useState(children ? false : true);
const [openConfirmationModal, setOpenConfirmationModal] = useState(false);
const nameComponent = is_component ? "component" : "flow";
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
const [loadingTags, setLoadingTags] = useState<boolean>(false);
const [sharePublic, setSharePublic] = useState(true);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [unavaliableNames, setUnavaliableNames] = useState<
{ id: string; name: string }[]
>([]);
const { saveFlow, flows, tabId } = useContext(FlowsContext);
const [loadingNames, setLoadingNames] = useState(false);
const name = component?.name ?? "";
const description = component?.description ?? "";
useEffect(() => {
if (open || internalOpen) {
if (hasApiKey && hasStore) {
handleGetTags();
handleGetNames();
}
}
}, [open, internalOpen, hasApiKey, hasStore]);
function handleGetTags() {
setLoadingTags(true);
getStoreTags().then((res) => {
setTags(res);
setLoadingTags(false);
});
}
async function handleGetNames() {
setLoadingNames(true);
const unavaliableNames: Array<{ id: string; name: string }> = [];
await getStoreComponents({
fields: ["name", "id", "is_component"],
filterByUser: true,
}).then((res) => {
res?.results?.forEach((element: any) => {
if ((element.is_component ?? false) === is_component)
unavaliableNames.push({ name: element.name, id: element.id });
});
setUnavaliableNames(unavaliableNames);
setLoadingNames(false);
});
}
const handleShareComponent = async (update = false) => {
//remove file names from flows before sharing
removeFileNameFromComponents(component);
const flow: FlowType = removeApiKeys({
id: component!.id,
data: component!.data,
description,
name,
last_tested_version: version,
is_component: is_component,
});
function successShare() {
if (!is_component) {
saveFlow(flow!, true);
}
setSuccessData({
title: `${is_component ? "Component" : "Flow"} shared successfully!`,
});
}
if (!update)
saveFlowStore(flow!, getTagsIds(selectedTags, tags), sharePublic).then(
successShare,
(err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
}
);
else
updateFlowStore(
flow!,
getTagsIds(selectedTags, tags),
sharePublic,
unavaliableNames.find((e) => e.name === name)!.id
).then(successShare, (err) => {
setErrorData({
title: "Error sharing " + is_component ? "component" : "flow",
list: [err["response"]["data"]["detail"]],
});
});
};
const handleUpdateComponent = () => {
handleShareComponent(true);
if (setOpen) setOpen(false);
else internalSetOpen(false);
};
const handleExportComponent = () => {
component = removeApiKeys(component);
downloadNode(component);
};
let modalConfirmation = useMemo(() => {
return (
<>
<ConfirmationModal
open={openConfirmationModal}
title={`Replace`}
cancelText="Cancel"
confirmationText="Replace"
size={"x-small"}
icon={"SaveAll"}
index={6}
onConfirm={() => {
handleUpdateComponent();
setOpenConfirmationModal(false);
}}
onCancel={() => {
setOpenConfirmationModal(false);
}}
>
<ConfirmationModal.Content>
<span>
It seems {name} already exists. Do you want to replace it with the
current?
</span>
<br></br>
<span className=" text-xs text-destructive ">
Warning: This action cannot be undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<></>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</>
);
}, [
unavaliableNames,
name,
loadingNames,
handleShareComponent,
openConfirmationModal,
]);
return (
<>
<BaseModal
size="smaller-h-full"
open={(!disabled && open) ?? internalOpen}
setOpen={setOpen ?? internalSetOpen}
>
<BaseModal.Trigger asChild>
{children ? children : <></>}
</BaseModal.Trigger>
<BaseModal.Header
description={`Share your ${nameComponent} to the Langflow Store.`}
>
<span className="pr-2">Share</span>
<IconComponent
name="Share3"
className="-m-0.5 h-6 w-6 text-foreground"
aria-hidden="true"
/>
</BaseModal.Header>
<BaseModal.Content>
<div className="w-full rounded-lg border border-border p-4">
<EditFlowSettings name={name} description={description} />
</div>
<div className="mt-3 flex h-8 w-full">
<TagsSelector
tags={tags}
loadingTags={loadingTags}
disabled={false}
selectedTags={selectedTags}
setSelectedTags={setSelectedTags}
/>
</div>
<div className="mb-2 mt-5 flex items-center space-x-2">
<Checkbox
id="public"
checked={sharePublic}
onCheckedChange={(event: boolean) => {
setSharePublic(event);
}}
/>
<label htmlFor="public" className="export-modal-save-api text-sm ">
Make {nameComponent} public
</label>
</div>
<span className=" text-xs text-destructive ">
<b>Warning:</b> API keys in designated fields are removed when
sharing.
</span>
</BaseModal.Content>
<BaseModal.Footer>
<div className="flex w-full justify-between gap-2">
<Button
type="button"
variant="outline"
className="gap-2"
onClick={() => {
handleExportComponent();
(setOpen || internalSetOpen)(false);
}}
>
<IconComponent name="Download" className="h-4 w-4" />
Export
</Button>
<Button
disabled={loadingNames}
type="button"
className={is_component ? "w-40" : "w-28"}
onClick={() => {
const isNameAvailable = !unavaliableNames.some(
(element) => element.name === name
);
if (isNameAvailable) {
handleShareComponent();
(setOpen || internalSetOpen)(false);
} else {
setOpenConfirmationModal(true);
}
}}
>
{loadingNames ? (
<>
<div className="center">
<Loader2 className="m-auto h-4 w-4 animate-spin"></Loader2>
</div>
</>
) : (
<>
Share{" "}
{!loadingNames && (!is_component ? "Flow" : "Component")}
</>
)}
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
<>{modalConfirmation}</>
</>
);
}

View file

@ -22,7 +22,7 @@ import {
} from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import {
addUser,
deleteUser,
@ -44,7 +44,7 @@ export default function AdminPage() {
const { userData } = useContext(AuthContext);
const [totalRowsCount, setTotalRowsCount] = useState(0);
const { setTabId } = useContext(TabsContext);
const { setTabId } = useContext(FlowsContext);
// set null id
useEffect(() => {
@ -312,7 +312,6 @@ export default function AdminPage() {
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
@ -326,12 +325,20 @@ export default function AdminPage() {
);
}}
>
<div className="flex w-fit">
<Checkbox
id="is_active"
checked={user.is_active}
/>
</div>
<ConfirmationModal.Content>
<span>
Are you completely confident about the changes
you are making to this user?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<div className="flex w-fit">
<Checkbox
id="is_active"
checked={user.is_active}
/>
</div>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</TableCell>
<TableCell className="relative left-1 truncate py-2 text-align-last-left">
@ -340,7 +347,6 @@ export default function AdminPage() {
title="Edit"
titleHeader={`${user.username}`}
modalContentTitle="Attention!"
modalContent="Are you completely confident about the changes you are making to this user?"
cancelText="Cancel"
confirmationText="Confirm"
icon={"UserCog2"}
@ -354,12 +360,20 @@ export default function AdminPage() {
);
}}
>
<div className="flex w-fit">
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</div>
<ConfirmationModal.Content>
<span>
Are you completely confident about the changes
you are making to this user?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<div className="flex w-fit">
<Checkbox
id="is_superuser"
checked={user.is_superuser}
/>
</div>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</TableCell>
<TableCell className="truncate py-2 ">
@ -402,7 +416,6 @@ export default function AdminPage() {
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this user? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
@ -412,12 +425,20 @@ export default function AdminPage() {
handleDeleteUser(user);
}}
>
<ShadTooltip content="Delete" side="top">
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
<ConfirmationModal.Content>
<span>
Are you sure you want to delete this user?
This action cannot be undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<ShadTooltip content="Delete" side="top">
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</div>
</TableCell>

View file

@ -224,7 +224,6 @@ export default function ApiKeysPage() {
title="Delete"
titleHeader="Delete User"
modalContentTitle="Attention!"
modalContent="Are you sure you want to delete this key? This action cannot be undone."
cancelText="Cancel"
confirmationText="Delete"
icon={"UserMinus2"}
@ -234,15 +233,19 @@ export default function ApiKeysPage() {
handleDeleteKey(keys);
}}
>
<ShadTooltip
content="Delete"
side="top"
>
<ConfirmationModal.Content>
<span>
Are you sure you want to delete
this key? This action cannot be
undone.
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<IconComponent
name="Trash2"
className="ml-2 h-4 w-4 cursor-pointer"
/>
</ShadTooltip>
</ConfirmationModal.Trigger>
</ConfirmationModal>
</div>
</TableCell>

View file

@ -1,116 +0,0 @@
import { useContext, useEffect, useState } from "react";
import { Button } from "../../components/ui/button";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { useNavigate } from "react-router-dom";
import { CardComponent } from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { getExamples } from "../../controllers/API";
import { FlowType } from "../../types/flow";
export default function CommunityPage(): JSX.Element {
const { flows, setTabId, downloadFlows, uploadFlows, addFlow } =
useContext(TabsContext);
// set null id
useEffect(() => {
setTabId("");
}, []);
const { setErrorData } = useContext(alertContext);
const [loadingExamples, setLoadingExamples] = useState(false);
const [examples, setExamples] = useState<FlowType[]>([]);
// Show community examples on screen
function handleExamples(): void {
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();
// Show community examples on page start
useEffect(() => {
handleExamples();
}, []);
return (
<>
<Header />
<div className="community-page-arrangement">
<div className="community-page-nav-arrangement">
<span className="community-page-nav-title">
<IconComponent name="Users2" className="w-6" />
Community Examples
</span>
<div className="community-page-nav-button">
<a
href="https://github.com/logspace-ai/langflow_examples"
target="_blank"
rel="noreferrer"
>
<Button variant="primary">
<IconComponent
name="GithubIcon"
className="main-page-nav-button"
/>
Add Your Example
</Button>
</a>
</div>
</div>
<span className="community-page-description-text">
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="community-pages-flows-panel">
{loadingExamples ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
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);
});
}}
>
<IconComponent
name="GitFork"
className="main-page-nav-button"
/>
Fork Example
</Button>
}
/>
))
)}
</div>
</div>
</>
);
}

View file

@ -8,12 +8,13 @@ export default function DisclosureComponent({
openDisc,
}: DisclosureComponentType): JSX.Element {
return (
<Disclosure as="div" key={title}>
<Disclosure as="div" defaultOpen={openDisc} key={title}>
{({ open }) => (
<>
<div>
<Disclosure.Button className="components-disclosure-arrangement">
<div className="flex gap-4">
{/* BUG ON THIS ICON */}
<Icon strokeWidth={1.5} size={22} className="text-primary" />
<span className="components-disclosure-title">{title}</span>
</div>
@ -34,9 +35,7 @@ export default function DisclosureComponent({
</div>
</Disclosure.Button>
</div>
<Disclosure.Panel as="div" static={openDisc}>
{children}
</Disclosure.Panel>
<Disclosure.Panel as="div">{children}</Disclosure.Panel>
</>
)}
</Disclosure>

View file

@ -13,10 +13,8 @@ import ReactFlow, {
Controls,
Edge,
EdgeChange,
Node,
NodeChange,
NodeDragHandler,
OnEdgesDelete,
OnSelectionChangeParams,
SelectionDragHandler,
addEdge,
@ -27,17 +25,25 @@ import ReactFlow, {
} from "reactflow";
import GenericNode from "../../../../CustomNodes/GenericNode";
import Chat from "../../../../components/chatComponent";
import Loading from "../../../../components/ui/loading";
import { alertContext } from "../../../../contexts/alertContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { locationContext } from "../../../../contexts/locationContext";
import { TabsContext } from "../../../../contexts/tabsContext";
import { typesContext } from "../../../../contexts/typesContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import { APIClassType } from "../../../../types/api";
import { FlowType, NodeType } from "../../../../types/flow";
import { TabsState } from "../../../../types/tabs";
import { isValidConnection } from "../../../../utils/reactflowUtils";
import { isWrappedWithClass } from "../../../../utils/utils";
import { FlowType, NodeType, targetHandleType } from "../../../../types/flow";
import { FlowsState } from "../../../../types/tabs";
import {
generateFlow,
generateNodeFromFlow,
isValidConnection,
scapeJSONParse,
validateSelection,
} from "../../../../utils/reactflowUtils";
import { cn, getRandomName, isWrappedWithClass } from "../../../../utils/utils";
import ConnectionLineComponent from "../ConnectionLineComponent";
import SelectionMenu from "../SelectionMenuComponent";
import ExtraSidebar from "../extraSidebarComponent";
const nodeTypes = {
@ -54,7 +60,6 @@ export default function Page({
let {
updateFlow,
uploadFlow,
addFlow,
getNodeId,
paste,
lastCopiedSelection,
@ -63,7 +68,8 @@ export default function Page({
saveFlow,
setTabsState,
tabId,
} = useContext(TabsContext);
saveCurrentFlow,
} = useContext(FlowsContext);
const {
types,
reactFlowInstance,
@ -76,16 +82,24 @@ export default function Page({
const reactFlowWrapper = useRef<HTMLDivElement>(null);
const { takeSnapshot } = useContext(undoRedoContext);
const { nodesOnFlow, setNodesOnFlow } = useContext(FlowsContext);
const [position, setPosition] = useState({ x: 0, y: 0 });
const position = useRef({ x: 0, y: 0 });
const [lastSelection, setLastSelection] =
useState<OnSelectionChangeParams | null>(null);
useEffect(() => {
// this effect is used to attach the global event handlers
const saveCurrentFlowTimeout = () => {
setTimeout(() => {
saveCurrentFlow();
}, 500); // need to do this because ReactFlow is not asynchronous.
};
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (!isWrappedWithClass(event, "nocopy")) {
if (
!isWrappedWithClass(event, "nocopy") &&
window.getSelection()?.toString().length === 0
) {
if (
(event.ctrlKey || event.metaKey) &&
event.key === "c" &&
@ -100,10 +114,10 @@ export default function Page({
lastCopiedSelection
) {
event.preventDefault();
let bounds = reactFlowWrapper.current?.getBoundingClientRect();
takeSnapshot();
paste(lastCopiedSelection, {
x: position.x - bounds!.left,
y: position.y - bounds!.top,
x: position.current.x,
y: position.current.y,
});
}
if (
@ -120,13 +134,16 @@ export default function Page({
lastSelection
) {
event.preventDefault();
takeSnapshot();
deleteNode(lastSelection.nodes.map((node) => node.id));
deleteEdge(lastSelection.edges.map((edge) => edge.id));
saveCurrentFlowTimeout();
}
}
};
const handleMouseMove = (event) => {
setPosition({ x: event.clientX, y: event.clientY });
position.current = { x: event.clientX, y: event.clientY };
};
document.addEventListener("keydown", onKeyDown);
@ -136,7 +153,12 @@ export default function Page({
document.removeEventListener("keydown", onKeyDown);
document.removeEventListener("mousemove", handleMouseMove);
};
}, [position, lastCopiedSelection, lastSelection]);
}, [
lastCopiedSelection,
lastSelection,
takeSnapshot,
saveCurrentFlowTimeout,
]);
const [selectionMenuVisible, setSelectionMenuVisible] = useState(false);
@ -145,63 +167,54 @@ export default function Page({
const [nodes, setNodes, onNodesChange] = useNodesState(
flow.data?.nodes ?? []
);
const [edges, setEdges, onEdgesChange] = useEdgesState(
flow.data?.edges ?? []
);
const { setViewport } = useReactFlow();
const edgeUpdateSuccessful = useRef(true);
const [loading, setLoading] = useState(true);
const timeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (reactFlowInstance && flow) {
flow.data = reactFlowInstance.toObject();
updateFlow(flow);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [edges]);
//update flow when tabs change
useEffect(() => {
setLoading(true);
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]);
setViewport(flow?.data?.viewport ?? { zoom: 1, x: 0, y: 0 });
const [seconds, setSeconds] = useState(0);
// Clear the previous timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
// Create a new timeout
timeoutRef.current = setTimeout(() => {
setLoading(false);
}, 300);
// Clear the timeout when the component is unmounted
return () => {
clearTimeout(timeoutRef.current);
};
}, [flow, reactFlowInstance]);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((prevSeconds) => {
let updatedSeconds = prevSeconds + 1;
if (updatedSeconds % 30 === 0) {
saveFlow(flow, true);
updatedSeconds = 0;
}
return updatedSeconds;
});
}, 1000);
saveFlow(flow, true);
}, 30000);
return () => {
clearInterval(interval);
};
}, []);
}, [flow, flow.data]);
const onEdgesChangeMod = useCallback(
(change: EdgeChange[]) => {
onEdgesChange(change);
setNodes((node) => {
let newX = _.cloneDeep(node);
return newX;
});
//@ts-ignore
setTabsState((prev: TabsState) => {
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
@ -210,27 +223,37 @@ export default function Page({
},
};
});
saveCurrentFlowTimeout();
},
[onEdgesChange, setNodes, setTabsState, tabId]
[onEdgesChange, setNodes, setTabsState, saveCurrentFlowTimeout, tabId]
);
const onNodesChangeMod = useCallback(
(change: NodeChange[]) => {
onNodesChange(change);
//@ts-ignore
setTabsState((prev: TabsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
const changeString = JSON.stringify(change);
if (changeString !== nodesOnFlow) {
onNodesChange(change);
updateNodeFlow(changeString);
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
saveCurrentFlowTimeout();
}
},
[onNodesChange, setTabsState, tabId]
[onNodesChange, setTabsState, tabId, updateNodeFlow, saveCurrentFlowTimeout]
);
function updateNodeFlow(changeString: string) {
setNodesOnFlow(changeString);
}
const onConnect = useCallback(
(params: Connection) => {
takeSnapshot();
@ -238,12 +261,19 @@ export default function Page({
addEdge(
{
...params,
data: {
targetHandle: scapeJSONParse(params.targetHandle!),
sourceHandle: scapeJSONParse(params.sourceHandle!),
},
style: { stroke: "#555" },
className:
(params.targetHandle?.split("|")[0] === "Text"
((scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text"
? "stroke-foreground "
: "stroke-foreground ") + " stroke-connection",
animated: params.targetHandle?.split("|")[0] === "Text",
animated:
(scapeJSONParse(params.targetHandle!) as targetHandleType)
.type === "Text",
},
eds
)
@ -252,8 +282,19 @@ export default function Page({
let newX = _.cloneDeep(node);
return newX;
});
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
saveCurrentFlowTimeout();
},
[setEdges, setNodes, takeSnapshot]
[setEdges, setNodes, takeSnapshot, addEdge]
);
const onNodeDragStart: NodeDragHandler = useCallback(() => {
@ -267,11 +308,6 @@ export default function Page({
takeSnapshot();
}, [takeSnapshot]);
const onEdgesDelete: OnEdgesDelete = useCallback(() => {
// 👇 make deleting edges undoable
takeSnapshot();
}, [takeSnapshot]);
const onDragOver = useCallback((event: React.DragEvent) => {
event.preventDefault();
if (event.dataTransfer.types.some((types) => types === "nodedata")) {
@ -296,11 +332,10 @@ export default function Page({
event.dataTransfer.getData("nodedata")
);
// 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,
const position = reactFlowInstance!.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
// Generate a unique node ID
@ -337,7 +372,21 @@ export default function Page({
} else if (event.dataTransfer.types.some((types) => types === "Files")) {
takeSnapshot();
if (event.dataTransfer.files.item(0)!.type === "application/json") {
uploadFlow(false, event.dataTransfer.files.item(0)!);
const position = {
x: event.clientX,
y: event.clientY,
};
uploadFlow({
newProject: false,
isComponent: false,
file: event.dataTransfer.files.item(0)!,
position: position,
}).catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
} else {
setErrorData({
title: "Invalid file type",
@ -351,6 +400,9 @@ export default function Page({
);
useEffect(() => {
setExtraComponent(<ExtraSidebar />);
setExtraNavigation({ title: "Components" });
return () => {
if (tabsState && tabsState[flow.id]?.isPending) {
saveFlow(flow);
@ -358,21 +410,6 @@ export default function Page({
};
}, []);
const onDelete = useCallback(
(mynodes: Node[]) => {
takeSnapshot();
setEdges(
edges.filter(
(edge) =>
!mynodes.some(
(node) => edge.source === node.id || edge.target === node.id
)
)
);
},
[takeSnapshot, edges, setEdges]
);
const onEdgeUpdateStart = useCallback(() => {
edgeUpdateSuccessful.current = false;
}, []);
@ -394,7 +431,7 @@ export default function Page({
edgeUpdateSuccessful.current = true;
}, []);
const [selectionEnded, setSelectionEnded] = useState(false);
const [selectionEnded, setSelectionEnded] = useState(true);
const onSelectionEnd = useCallback(() => {
setSelectionEnded(true);
@ -424,6 +461,20 @@ export default function Page({
setFilterEdge([]);
}, []);
const onMove = useCallback(() => {
saveCurrentFlowTimeout();
//@ts-ignore
setTabsState((prev: FlowsState) => {
return {
...prev,
[tabId]: {
...prev[tabId],
isPending: true,
},
};
});
}, [setTabsState, saveCurrentFlowTimeout]);
return (
<div className="flex h-full overflow-hidden">
{!view && <ExtraSidebar />}
@ -434,16 +485,18 @@ export default function Page({
<div className="h-full w-full" ref={reactFlowWrapper}>
{Object.keys(templates).length > 0 &&
Object.keys(types).length > 0 ? (
<div className="h-full w-full">
<div id="react-flow-id" className="h-full w-full">
<div
className={cn(
"relative flex h-full w-full items-center justify-center bg-background",
!loading ? "hidden" : ""
)}
>
<Loading />
</div>
<ReactFlow
nodes={nodes}
onMove={() => {
if (reactFlowInstance)
updateFlow({
...flow,
data: reactFlowInstance.toObject(),
});
}}
onMove={onMove}
edges={edges}
onNodesChange={onNodesChangeMod}
onEdgesChange={onEdgesChangeMod}
@ -458,12 +511,10 @@ export default function Page({
onSelectionDragStart={onSelectionDragStart}
onSelectionEnd={onSelectionEnd}
onSelectionStart={onSelectionStart}
onEdgesDelete={onEdgesDelete}
connectionLineComponent={ConnectionLineComponent}
onDragOver={onDragOver}
onDrop={onDrop}
onSelectionChange={onSelectionChange}
onNodesDelete={onDelete}
deleteKeyCode={[]}
className="theme-attribution"
minZoom={0.01}
@ -481,6 +532,51 @@ export default function Page({
[&>button]:border-b-border hover:[&>button]:bg-border"
></Controls>
)}
<SelectionMenu
isVisible={selectionMenuVisible}
nodes={lastSelection?.nodes}
onClick={() => {
takeSnapshot();
if (
validateSelection(lastSelection!, edges).length === 0
) {
const { newFlow } = generateFlow(
lastSelection!,
reactFlowInstance!,
getRandomName()
);
const newGroupNode = generateNodeFromFlow(
newFlow,
getNodeId
);
setNodes((oldNodes) => [
...oldNodes.filter(
(oldNodes) =>
!lastSelection?.nodes.some(
(selectionNode) =>
selectionNode.id === oldNodes.id
)
),
newGroupNode,
]);
setEdges((oldEdges) =>
oldEdges.filter(
(oldEdge) =>
!lastSelection!.nodes.some(
(selectionNode) =>
selectionNode.id === oldEdge.target ||
selectionNode.id === oldEdge.source
)
)
);
} else {
setErrorData({
title: "Invalid selection",
list: validateSelection(lastSelection!, edges),
});
}
}}
/>
</ReactFlow>
{!view && (
<Chat flow={flow} reactFlowInstance={reactFlowInstance!} />

View file

@ -0,0 +1,59 @@
import { useEffect, useState } from "react";
import { NodeToolbar } from "reactflow";
import { GradientGroup } from "../../../../icons/GradientSparkles";
export default function SelectionMenu({ onClick, nodes, isVisible }) {
const [isOpen, setIsOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
const [lastNodes, setLastNodes] = useState(nodes);
// nodes get saved to not be gone after the toolbar closes
useEffect(() => {
setLastNodes(nodes);
}, [isOpen]);
// transition starts after and ends before the toolbar closes
useEffect(() => {
if (isVisible) {
setIsOpen(true);
setTimeout(() => {
setIsTransitioning(true);
}, 50);
} else {
setIsTransitioning(false);
setTimeout(() => {
setIsOpen(false);
}, 500);
}
}, [isVisible]);
return (
<NodeToolbar
isVisible={isOpen}
offset={5}
nodeId={
lastNodes && lastNodes.length > 0 ? lastNodes.map((n) => n.id) : []
}
>
<div className="h-10 w-28 overflow-hidden">
<div
className={
"duration-400 h-10 w-24 rounded-md border border-indigo-300 bg-background px-2.5 text-primary shadow-inner transition-all ease-in-out" +
(isTransitioning ? " opacity-100" : " opacity-0 ")
}
>
<button
className="flex h-full w-full items-center justify-between text-sm hover:text-indigo-500"
onClick={onClick}
>
<GradientGroup
strokeWidth={1.5}
size={22}
className="text-primary"
/>
Group
</button>
</div>
</div>
</NodeToolbar>
);
}

View file

@ -1,29 +1,37 @@
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect, useMemo, useState } from "react";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent";
import { Input } from "../../../../components/ui/input";
import { Separator } from "../../../../components/ui/separator";
import { alertContext } from "../../../../contexts/alertContext";
import { TabsContext } from "../../../../contexts/tabsContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { StoreContext } from "../../../../contexts/storeContext";
import { typesContext } from "../../../../contexts/typesContext";
import ApiModal from "../../../../modals/ApiModal";
import ExportModal from "../../../../modals/exportModal";
import ShareModal from "../../../../modals/shareModal";
import { APIClassType, APIObjectType } from "../../../../types/api";
import {
nodeColors,
nodeIconsLucide,
nodeNames,
} from "../../../../utils/styleUtils";
import { classNames } from "../../../../utils/utils";
import {
classNames,
removeCountFromString,
sensitiveSort,
} from "../../../../utils/utils";
import DisclosureComponent from "../DisclosureComponent";
import SidebarDraggableComponent from "./sideBarDraggableComponent";
export default function ExtraSidebar(): JSX.Element {
const { data, templates, getFilterEdge, setFilterEdge } =
const { data, templates, getFilterEdge, setFilterEdge, reactFlowInstance } =
useContext(typesContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt } =
useContext(TabsContext);
const { setSuccessData, setErrorData } = useContext(alertContext);
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } =
useContext(FlowsContext);
const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext);
const { setErrorData } = useContext(alertContext);
const [dataFilter, setFilterData] = useState(data);
const [search, setSearch] = useState("");
const isPending = tabsState[tabId]?.isPending;
@ -52,8 +60,10 @@ export default function ExtraSidebar(): JSX.Element {
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())
let keys = Object.keys(data[d]).filter(
(nd) =>
nd.toLowerCase().includes(e.toLowerCase()) ||
data[d][nd].display_name?.toLowerCase().includes(e.toLowerCase())
);
keys.forEach((element) => {
ret[d][element] = data[d][element];
@ -76,7 +86,8 @@ export default function ExtraSidebar(): JSX.Element {
}, []);
function handleBlur() {
if (!search && search === "") {
// check if search is search to reset fitler on click input
if ((!search && search === "") || search === "search") {
setFilterData(data);
setFilterEdge([]);
setSearch("");
@ -84,11 +95,55 @@ export default function ExtraSidebar(): JSX.Element {
}
useEffect(() => {
if (getFilterEdge.length === 0 && search === "") {
setFilterData(data);
setFilterEdge([]);
if (getFilterEdge.length !== 0) {
setSearch("");
}
if (getFilterEdge.length === 0 && search === "") {
setSearch("");
setFilterData(data);
}
}, [getFilterEdge, data]);
useEffect(() => {
handleSearchInput(search);
}, [data]);
useEffect(() => {
if (getFilterEdge?.length > 0) {
setFilterData((_) => {
let dataClone = cloneDeep(data);
let ret = {};
Object.keys(dataClone).forEach((d: keyof APIObjectType, i) => {
ret[d] = {};
if (getFilterEdge.some((x) => x.family === d)) {
ret[d] = dataClone[d];
const filtered = getFilterEdge
.filter((x) => x.family === d)
.pop()
.type.split(",");
for (let i = 0; i < filtered.length; i++) {
filtered[i] = filtered[i].trimStart();
}
if (filtered.some((x) => x !== "")) {
let keys = Object.keys(dataClone[d]).filter((nd) =>
filtered.includes(nd)
);
Object.keys(dataClone[d]).forEach((element) => {
if (!keys.includes(element)) {
delete ret[d][element];
}
});
}
}
});
setSearch("");
return ret;
});
}
}, [getFilterEdge]);
useEffect(() => {
@ -122,39 +177,93 @@ export default function ExtraSidebar(): JSX.Element {
}
}
});
setSearch("search");
setSearch("");
return ret;
});
}
}, [getFilterEdge]);
}, [getFilterEdge, data]);
const ModalMemo = useMemo(
() => (
<ShareModal
is_component={false}
component={flow!}
disabled={!hasApiKey || !validApiKey || !hasStore}
>
<button
disabled={!hasApiKey || !validApiKey || !hasStore}
className={classNames(
"extra-side-bar-buttons gap-[4px] text-sm font-semibold",
!hasApiKey || !validApiKey || !hasStore
? "button-disable cursor-default text-muted-foreground"
: ""
)}
>
<IconComponent
name="Share3"
className={classNames(
"-m-0.5 -ml-1 h-6 w-6",
!hasApiKey || !validApiKey || !hasStore
? "extra-side-bar-save-disable"
: ""
)}
/>
Share
</button>
</ShareModal>
),
[hasApiKey, validApiKey, flow, hasStore]
);
const ExportMemo = useMemo(
() => (
<ExportModal>
<ShadTooltip content="Export" side="top">
<button className={classNames("extra-side-bar-buttons")}>
<IconComponent name="FileDown" className="side-bar-button-size" />
</button>
</ShadTooltip>
</ExportModal>
),
[]
);
return (
<div className="side-bar-arrangement">
<div className="side-bar-buttons-arrangement">
{hasStore && (
<ShadTooltip
content={
!hasApiKey || !validApiKey
? "Please review your API key."
: "Share"
}
side="top"
styleClasses="cursor-default"
>
<div className="side-bar-button">{ModalMemo}</div>
</ShadTooltip>
)}
<div className="side-bar-button">
<ShadTooltip content="Import" side="top">
<button
className="extra-side-bar-buttons"
onClick={() => {
uploadFlow();
uploadFlow({ newProject: false, isComponent: false }).catch(
(error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
}
);
}}
>
<IconComponent name="FileUp" className="side-bar-button-size " />
</button>
</ShadTooltip>
</div>
<div className="side-bar-button">
<ExportModal>
<ShadTooltip content="Export" side="top">
<div className={classNames("extra-side-bar-buttons")}>
<IconComponent
name="FileDown"
className="side-bar-button-size"
/>
</div>
</ShadTooltip>
</ExportModal>
</div>
{!hasStore && ExportMemo}
<ShadTooltip content={"Code"} side="top">
<div className="side-bar-button">
{flow && flow.data && (
@ -177,30 +286,39 @@ export default function ExtraSidebar(): JSX.Element {
</div>
</ShadTooltip>
<div className="side-bar-button">
<ShadTooltip content="Save" side="top">
<button
className={
"extra-side-bar-buttons " + (isPending ? "" : "button-disable")
}
onClick={(event) => {
saveFlow(flow!);
}}
>
<IconComponent
name="Save"
{flow && flow.data && (
<ShadTooltip content="Save" side="top">
<button
disabled={flow?.data?.nodes.length === 0}
className={
"side-bar-button-size" +
(isPending ? " " : " extra-side-bar-save-disable")
"extra-side-bar-buttons " +
(isPending && flow!.data!.nodes?.length > 0
? ""
: "button-disable")
}
/>
</button>
</ShadTooltip>
onClick={(event) => {
saveFlow(flow!);
}}
>
<IconComponent
name="Save"
className={
"side-bar-button-size" +
(isPending && flow!.data!.nodes?.length > 0
? " "
: " extra-side-bar-save-disable")
}
/>
</button>
</ShadTooltip>
)}
</div>
</div>
<Separator />
<div className="side-bar-search-div-placement">
<Input
onFocusCapture={() => handleBlur()}
value={search}
type="text"
name="search"
id="search"
@ -223,12 +341,28 @@ export default function ExtraSidebar(): JSX.Element {
<div className="side-bar-components-div-arrangement">
{Object.keys(dataFilter)
.sort()
.sort((a, b) => {
if (a.toLowerCase() === "saved_components") {
return -1;
} else if (b.toLowerCase() === "saved_components") {
return 1;
} else if (a.toLowerCase() === "custom_components") {
return -2;
} else if (b.toLowerCase() === "custom_components") {
return 2;
} else {
return a.localeCompare(b);
}
})
.map((SBSectionName: keyof APIObjectType, index) =>
Object.keys(dataFilter[SBSectionName]).length > 0 ? (
<DisclosureComponent
openDisc={search.length == 0 ? false : true}
key={index}
openDisc={
getFilterEdge.length !== 0 || search.length !== 0
? true
: false
}
key={index + search + JSON.stringify(getFilterEdge)}
button={{
title: nodeNames[SBSectionName] ?? nodeNames.unknown,
Icon:
@ -237,51 +371,45 @@ export default function ExtraSidebar(): JSX.Element {
>
<div className="side-bar-components-gap">
{Object.keys(dataFilter[SBSectionName])
.sort()
.sort((a, b) =>
sensitiveSort(
dataFilter[SBSectionName][a].display_name,
dataFilter[SBSectionName][b].display_name
)
)
.map((SBItemName: string, index) => (
<ShadTooltip
content={data[SBSectionName][SBItemName].display_name}
content={
dataFilter[SBSectionName][SBItemName].display_name
}
side="right"
key={index}
>
<div key={index} data-tooltip-id={SBItemName}>
<div
draggable={!data[SBSectionName][SBItemName].error}
className={
"side-bar-components-border bg-background" +
(data[SBSectionName][SBItemName].error
? " cursor-not-allowed select-none"
: "")
}
style={{
borderLeftColor:
nodeColors[SBSectionName] ?? nodeColors.unknown,
}}
onDragStart={(event) =>
onDragStart(event, {
type: SBItemName,
node: data[SBSectionName][SBItemName],
})
}
onDragEnd={() => {
document.body.removeChild(
document.getElementsByClassName(
"cursor-grabbing"
)[0]
);
}}
>
<div className="side-bar-components-div-form">
<span className="side-bar-components-text">
{data[SBSectionName][SBItemName].display_name}
</span>
<IconComponent
name="Menu"
className="side-bar-components-icon "
/>
</div>
</div>
</div>
<SidebarDraggableComponent
sectionName={SBSectionName as string}
apiClass={dataFilter[SBSectionName][SBItemName]}
key={index}
onDragStart={(event) =>
onDragStart(event, {
//split type to remove type in nodes saved with same name removing it's
type: removeCountFromString(SBItemName),
node: dataFilter[SBSectionName][SBItemName],
})
}
color={nodeColors[SBSectionName]}
itemName={SBItemName}
//convert error to boolean
error={!!dataFilter[SBSectionName][SBItemName].error}
display_name={
dataFilter[SBSectionName][SBItemName].display_name
}
official={
dataFilter[SBSectionName][SBItemName].official ===
false
? false
: true
}
/>
</ShadTooltip>
))}
</div>

View file

@ -0,0 +1,152 @@
import { DragEventHandler, useContext, useRef, useState } from "react";
import IconComponent from "../../../../../components/genericIconComponent";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "../../../../../components/ui/select-custom";
import { AuthContext } from "../../../../../contexts/authContext";
import { FlowsContext } from "../../../../../contexts/flowsContext";
import { APIClassType } from "../../../../../types/api";
import {
createFlowComponent,
downloadNode,
} from "../../../../../utils/reactflowUtils";
import { removeCountFromString } from "../../../../../utils/utils";
export default function SidebarDraggableComponent({
sectionName,
display_name,
itemName,
error,
color,
onDragStart,
apiClass,
official,
}: {
sectionName: string;
apiClass: APIClassType;
display_name: string;
itemName: string;
error: boolean;
color: string;
onDragStart: DragEventHandler<HTMLDivElement>;
official: boolean;
}) {
const [open, setOpen] = useState(false);
const { getNodeId, deleteComponent, version } = useContext(FlowsContext);
const { autoLogin, userData } = useContext(AuthContext);
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const popoverRef = useRef<HTMLDivElement>(null);
const handlePointerDown = (e) => {
if (!open) {
const rect = popoverRef.current?.getBoundingClientRect() ?? {
left: 0,
top: 0,
};
setCursorPos({ x: e.clientX - rect.left, y: e.clientY - rect.top });
}
};
function handleSelectChange(value: string) {
switch (value) {
case "share":
break;
case "download":
const type = removeCountFromString(itemName);
downloadNode(
createFlowComponent(
{ id: getNodeId(type), type, node: apiClass },
version
)
);
break;
case "delete":
deleteComponent(display_name);
break;
}
}
return (
<Select
onValueChange={handleSelectChange}
onOpenChange={(change) => setOpen(change)}
open={open}
key={itemName}
>
<div
onPointerDown={handlePointerDown}
onContextMenuCapture={(e) => {
e.preventDefault();
setOpen(true);
}}
key={itemName}
data-tooltip-id={itemName}
>
<div
draggable={!error}
className={
"side-bar-components-border bg-background" +
(error ? " cursor-not-allowed select-none" : "")
}
style={{
borderLeftColor: color,
}}
onDragStart={onDragStart}
onDragEnd={() => {
document.body.removeChild(
document.getElementsByClassName("cursor-grabbing")[0]
);
}}
>
<div
data-testid={sectionName + display_name}
id={sectionName + display_name}
className="side-bar-components-div-form"
>
<span className="side-bar-components-text">{display_name}</span>
<div ref={popoverRef}>
<IconComponent
name="Menu"
className="side-bar-components-icon "
/>
<SelectTrigger></SelectTrigger>
<SelectContent
position="popper"
side="bottom"
sideOffset={-25}
style={{
position: "absolute",
left: cursorPos.x,
top: cursorPos.y,
}}
>
<SelectItem value={"download"}>
<div className="flex">
<IconComponent
name="Download"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Download{" "}
</div>{" "}
</SelectItem>
{!official && (
<SelectItem value={"delete"}>
<div className="flex">
<IconComponent
name="Trash2"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Delete{" "}
</div>{" "}
</SelectItem>
)}
</SelectContent>
</div>
</div>
</div>
</div>
</Select>
);
}

View file

@ -1,4 +1,5 @@
import { useContext, useState } from "react";
import { cloneDeep } from "lodash";
import { useContext, useEffect, useState } from "react";
import { useReactFlow, useUpdateNodeInternals } from "reactflow";
import ShadTooltip from "../../../../components/ShadTooltipComponent";
import IconComponent from "../../../../components/genericIconComponent";
@ -8,15 +9,26 @@ import {
SelectItem,
SelectTrigger,
} from "../../../../components/ui/select-custom";
import { TabsContext } from "../../../../contexts/tabsContext";
import { FlowsContext } from "../../../../contexts/flowsContext";
import { StoreContext } from "../../../../contexts/storeContext";
import { undoRedoContext } from "../../../../contexts/undoRedoContext";
import ConfirmationModal from "../../../../modals/ConfirmationModal";
import EditNodeModal from "../../../../modals/EditNodeModal";
import ShareModal from "../../../../modals/shareModal";
import { nodeToolbarPropsType } from "../../../../types/components";
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
import { FlowType } from "../../../../types/flow";
import {
createFlowComponent,
downloadNode,
expandGroupNode,
updateFlowPosition,
} from "../../../../utils/reactflowUtils";
import { classNames } from "../../../../utils/utils";
export default function NodeToolbarComponent({
data,
setData,
deleteNode,
position,
setShowNode,
numberOfHandles,
showNode,
@ -33,10 +45,14 @@ export default function NodeToolbarComponent({
data.node.template[templateField].type === "prompt" ||
data.node.template[templateField].type === "file" ||
data.node.template[templateField].type === "Any" ||
data.node.template[templateField].type === "int")
data.node.template[templateField].type === "int" ||
data.node.template[templateField].type === "dict" ||
data.node.template[templateField].type === "NestedDict")
).length
);
const updateNodeInternals = useUpdateNodeInternals();
const { getNodeId } = useContext(FlowsContext);
const { hasApiKey, validApiKey, hasStore } = useContext(StoreContext);
function canMinimize() {
let countHandles: number = 0;
@ -47,26 +63,70 @@ export default function NodeToolbarComponent({
return true;
}
const isMinimal = canMinimize();
const { paste } = useContext(TabsContext);
const isGroup = data.node?.flow ? true : false;
const { paste, saveComponent, version, flows } = useContext(FlowsContext);
const { takeSnapshot } = useContext(undoRedoContext);
const reactFlowInstance = useReactFlow();
const [showModalAdvanced, setShowModalAdvanced] = useState(false);
const [showconfirmShare, setShowconfirmShare] = useState(false);
const [selectedValue, setSelectedValue] = useState("");
const [showOverrideModal, setShowOverrideModal] = useState(false);
const [flowComponent, setFlowComponent] = useState<FlowType>();
const openInNewTab = (url) => {
window.open(url, "_blank", "noreferrer");
};
useEffect(() => {
setFlowComponent(createFlowComponent(cloneDeep(data), version));
}, [
data,
data.node,
data.node?.display_name,
data.node?.description,
data.node?.template,
showModalAdvanced,
showconfirmShare,
]);
const handleSelectChange = (event) => {
setSelectedValue(event);
if (event.includes("advanced")) {
return setShowModalAdvanced(true);
}
setShowModalAdvanced(false);
if (event.includes("show")) {
setShowNode((prev) => !prev);
updateNodeInternals(data.id);
}
if (event.includes("disabled")) {
return;
switch (event) {
case "advanced":
setShowModalAdvanced(true);
break;
case "show":
takeSnapshot();
setShowNode(data.showNode ?? true ? false : true);
updateNodeInternals(data.id);
break;
case "Download":
downloadNode(createFlowComponent(cloneDeep(data), version));
break;
case "SaveAll":
saveComponent(cloneDeep(data), false);
break;
case "documentation":
if (data.node?.documentation) openInNewTab(data.node?.documentation);
break;
case "disabled":
break;
case "ungroup":
takeSnapshot();
updateFlowPosition(position, data.node?.flow!);
expandGroupNode(data, reactFlowInstance, getNodeId);
break;
case "override":
setShowOverrideModal(true);
break;
}
};
const isSaved = flows.some((flow) =>
Object.values(flow).includes(data.node?.display_name!)
);
return (
<>
<div className="w-26 h-10">
@ -106,70 +166,45 @@ export default function NodeToolbarComponent({
<IconComponent name="Copy" className="h-4 w-4" />
</button>
</ShadTooltip>
<ShadTooltip
content={
data.node?.documentation === "" ? "Coming Soon" : "Documentation"
}
side="top"
>
<a
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(data.node?.documentation === ""
? " text-muted-foreground"
: " text-foreground")
)}
target="_blank"
rel="noopener noreferrer"
href={data.node?.documentation}
// deactivate link if no documentation is provided
onClick={(event) => {
if (data.node?.documentation === "") {
{hasStore && (
<ShadTooltip content="Share" side="top">
<button
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10",
!hasApiKey || !validApiKey ? " text-muted-foreground" : ""
)}
onClick={(event) => {
event.preventDefault();
}
}}
>
<IconComponent name="FileText" className="h-4 w-4 " />
</a>
</ShadTooltip>
if (hasApiKey || hasStore) setShowconfirmShare(true);
}}
>
<IconComponent name="Share3" className="-m-1 h-6 w-6" />
</button>
</ShadTooltip>
)}
{isMinimal ? (
<Select onValueChange={handleSelectChange} value={selectedValue}>
<ShadTooltip content="More" side="top">
<SelectTrigger>
<div>
<div
className={classNames(
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent
name="MoreHorizontal"
className="relative left-2 h-4 w-4"
/>
</div>
</div>
</SelectTrigger>
</ShadTooltip>
<SelectContent>
<SelectItem
value={
getRandomKeyByssmm() +
(nodeLength == 0 ? "disabled" : "advanced")
}
>
<Select onValueChange={handleSelectChange} value={selectedValue}>
<ShadTooltip content="More" side="top">
<SelectTrigger>
<div>
<div
className={
"flex " +
(nodeLength == 0
? "text-muted-foreground"
: "text-primary")
}
data-testid="more-options-modal"
className={classNames(
"relative -ml-px inline-flex h-8 w-[31px] items-center rounded-r-md bg-background text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10"
)}
>
<IconComponent
name="MoreHorizontal"
className="relative left-2 h-4 w-4"
/>
</div>
</div>
</SelectTrigger>
</ShadTooltip>
<SelectContent>
{nodeLength > 0 && (
<SelectItem value={nodeLength === 0 ? "disabled" : "advanced"}>
<div className="flex" data-testid="edit-button-modal">
<IconComponent
name="Settings2"
className="relative top-0.5 mr-2 h-4 w-4"
@ -177,74 +212,116 @@ export default function NodeToolbarComponent({
Edit{" "}
</div>{" "}
</SelectItem>
{isMinimal && (
<SelectItem value={getRandomKeyByssmm() + "show"}>
<div className="flex">
<IconComponent
name={showNode ? "Minimize2" : "Maximize2"}
className="relative top-0.5 mr-2 h-4 w-4"
/>
{showNode ? "Minimize" : "Expand"}
</div>
</SelectItem>
)}
</SelectContent>
</Select>
) : (
<ShadTooltip content="Edit" side="top">
<div>
<button
disabled={nodeLength === 0}
onClick={() => setShowModalAdvanced(true)}
className={classNames(
"relative -ml-px inline-flex items-center rounded-r-md bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent name="Settings2" className="h-4 w-4 " />
</button>
</div>
</ShadTooltip>
)}
)}
{showModalAdvanced && (
<EditNodeModal
data={data}
setData={setData}
nodeLength={nodeLength}
open={showModalAdvanced}
onClose={(modal) => {
setShowModalAdvanced(modal);
}}
>
<></>
</EditNodeModal>
)}
{/*
<ShadTooltip content="Edit" side="top">
<div>
<EditNodeModal
data={data}
setData={setData}
nodeLength={nodeLength}
{isSaved ? (
<SelectItem value={"override"}>
<div className="flex">
<IconComponent
name="SaveAll"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Save{" "}
</div>{" "}
</SelectItem>
) : (
<SelectItem value={"SaveAll"}>
<div className="flex">
<IconComponent
name="SaveAll"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Save{" "}
</div>{" "}
</SelectItem>
)}
{!hasStore && (
<SelectItem value={"Download"}>
<div className="flex">
<IconComponent
name="Download"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Download{" "}
</div>{" "}
</SelectItem>
)}
<SelectItem
value={"documentation"}
disabled={data.node?.documentation === ""}
>
<div
className={classNames(
"relative -ml-px inline-flex items-center bg-background px-2 py-2 text-foreground shadow-md ring-1 ring-inset ring-ring transition-all duration-500 ease-in-out hover:bg-muted focus:z-10" +
(!canMinimize() && " rounded-r-md ") +
(nodeLength == 0
? " text-muted-foreground"
: " text-foreground")
)}
>
<IconComponent name="Settings2" className="h-4 w-4 " />
</div>
</EditNodeModal>
</div>
</ShadTooltip> */}
<div className="flex">
<IconComponent
name="FileText"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
{data.node?.documentation === ""
? "Coming Soon"
: "Documentation"}
</div>{" "}
</SelectItem>
{isMinimal && (
<SelectItem value={"show"}>
<div className="flex">
<IconComponent
name={showNode ? "Minimize2" : "Maximize2"}
className="relative top-0.5 mr-2 h-4 w-4"
/>
{showNode ? "Minimize" : "Expand"}
</div>
</SelectItem>
)}
{isGroup && (
<SelectItem value="ungroup">
<div className="flex">
<IconComponent
name="Combine"
className="relative top-0.5 mr-2 h-4 w-4"
/>{" "}
Ungroup{" "}
</div>
</SelectItem>
)}
</SelectContent>
</Select>
<ConfirmationModal
asChild
open={showOverrideModal}
title={`Replace`}
cancelText="Create New"
confirmationText="Replace"
size={"x-small"}
icon={"SaveAll"}
index={6}
onConfirm={(index, user) => {
saveComponent(cloneDeep(data), true);
}}
onClose={setShowOverrideModal}
onCancel={() => saveComponent(cloneDeep(data), false)}
>
<ConfirmationModal.Content>
<span>
It seems {data.node?.display_name} already exists. Do you want
to replace it with the current or create a new one?
</span>
</ConfirmationModal.Content>
<ConfirmationModal.Trigger>
<></>
</ConfirmationModal.Trigger>
</ConfirmationModal>
<EditNodeModal
data={data}
nodeLength={nodeLength}
open={showModalAdvanced}
setOpen={setShowModalAdvanced}
/>
<ShareModal
open={showconfirmShare}
setOpen={setShowconfirmShare}
is_component={true}
component={flowComponent!}
/>
</span>
</div>
</>

View file

@ -1,12 +1,11 @@
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect } from "react";
import { useParams } from "react-router-dom";
import Header from "../../components/headerComponent";
import { TabsContext } from "../../contexts/tabsContext";
import { getVersion } from "../../controllers/API";
import { FlowsContext } from "../../contexts/flowsContext";
import Page from "./components/PageComponent";
export default function FlowPage(): JSX.Element {
const { flows, tabId, setTabId } = useContext(TabsContext);
const { flows, tabId, setTabId, version } = useContext(FlowsContext);
const { id } = useParams();
// Set flow tab id
@ -14,14 +13,6 @@ export default function FlowPage(): JSX.Element {
setTabId(id!);
}, [id]);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return (
<>
<Header />

View file

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

View file

@ -1,180 +1,111 @@
import { useContext, useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Group, ToyBrick } from "lucide-react";
import { useContext, useEffect } from "react";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import DropdownButton from "../../components/DropdownButtonComponent";
import { CardComponent } from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import Header from "../../components/headerComponent";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import PageLayout from "../../components/pageLayout";
import SidebarNav from "../../components/sidebarComponent";
import { Button } from "../../components/ui/button";
import { USER_PROJECTS_HEADER } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
export default function HomePage(): JSX.Element {
const {
flows,
setTabId,
downloadFlows,
uploadFlows,
addFlow,
removeFlow,
uploadFlow,
isLoading,
} = useContext(TabsContext);
const { setErrorData } = useContext(alertContext);
const { setTabId, downloadFlows, uploadFlows, addFlow, uploadFlow } =
useContext(FlowsContext);
const { setErrorData, setSuccessData } = useContext(alertContext);
const location = useLocation();
const pathname = location.pathname;
const is_component = pathname === "/components";
const dropdownOptions = [
{
name: "Import from JSON",
onBtnClick: () =>
uploadFlow(true).then((id) => {
navigate("/flow/" + id);
}),
onBtnClick: () => {
uploadFlow({
newProject: true,
isComponent: is_component,
})
.then((id) => {
setSuccessData({
title: `${
is_component ? "Component" : "Flow"
} uploaded successfully`,
});
if (!is_component) navigate("/flow/" + id);
})
.catch((error) => {
setErrorData({
title: "Error uploading file",
list: [error],
});
});
},
},
];
const sidebarNavItems = [
{
title: "Flows",
href: "/flows",
icon: <Group className="w-5 stroke-[1.5]" />,
},
{
title: "Components",
href: "/components",
icon: <ToyBrick className="mx-[0.08rem] w-[1.1rem] stroke-[1.5]" />,
},
];
// Set a null id
useEffect(() => {
setTabId("");
}, []);
}, [pathname]);
const navigate = useNavigate();
const [isDragging, setIsDragging] = useState(false);
const dragOver = (e) => {
e.preventDefault();
if (e.dataTransfer.types.some((types) => types === "Files")) {
setIsDragging(true);
}
};
const dragEnter = (e) => {
if (e.dataTransfer.types.some((types) => types === "Files")) {
setIsDragging(true);
}
e.preventDefault();
};
const dragLeave = () => {
setIsDragging(false);
};
const fileDrop = (e) => {
e.preventDefault();
setIsDragging(false);
if (e.dataTransfer.types.some((types) => types === "Files")) {
if (e.dataTransfer.files.item(0).type === "application/json") {
uploadFlow(true, e.dataTransfer.files.item(0)!);
} else {
setErrorData({
title: "Invalid file type",
list: ["Please upload a JSON file"],
});
}
}
};
// Personal flows display
return (
<>
<Header />
<div className="main-page-panel">
<div className="main-page-nav-arrangement">
<span className="main-page-nav-title">
<IconComponent name="Home" className="w-6" />
{USER_PROJECTS_HEADER}
</span>
<div className="button-div-style">
<Button
variant="primary"
onClick={() => {
downloadFlows();
}}
>
<IconComponent name="Download" className="main-page-nav-button" />
Download Collection
</Button>
<Button
variant="primary"
onClick={() => {
uploadFlows();
}}
>
<IconComponent name="Upload" className="main-page-nav-button" />
Upload Collection
</Button>
<DropdownButton
firstButtonName="New Project"
onFirstBtnClick={() => {
addFlow(null!, true).then((id) => {
navigate("/flow/" + id);
});
}}
options={dropdownOptions}
/>
</div>
<PageLayout
title={USER_PROJECTS_HEADER}
description="Manage your personal projects. Download or upload your collection."
button={
<div className="flex gap-2">
<Button
variant="primary"
onClick={() => {
downloadFlows();
}}
>
<IconComponent name="Download" className="main-page-nav-button" />
Download Collection
</Button>
<Button
variant="primary"
onClick={() => {
uploadFlows();
}}
>
<IconComponent name="Upload" className="main-page-nav-button" />
Upload Collection
</Button>
<DropdownButton
firstButtonName="New Project"
onFirstBtnClick={() => {
addFlow(true).then((id) => {
navigate("/flow/" + id);
});
}}
options={dropdownOptions}
/>
</div>
<span className="main-page-description-text">
Manage your personal projects. Download or upload your collection.
</span>
<div
onDragOver={dragOver}
onDragEnter={dragEnter}
onDragLeave={dragLeave}
onDrop={fileDrop}
className={
"h-full w-full " +
(isDragging
? "mb-24 flex flex-col items-center justify-center gap-4 text-2xl font-light"
: "")
}
>
{isDragging ? (
<>
<IconComponent
name="ArrowUpToLine"
className="h-12 w-12 stroke-1"
/>
Drop your flow here
</>
) : (
<div className="main-page-flows-display">
{isLoading && flows.length == 0 ? (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
) : (
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 "
>
<IconComponent
name="ExternalLink"
className="main-page-nav-button"
/>
Edit Flow
</Button>
</Link>
}
onDelete={() => {
removeFlow(flow.id);
}}
/>
))
)}
</div>
)}
}
>
<div className="flex h-full w-full space-y-8 lg:flex-row lg:space-x-8 lg:space-y-0">
<aside className="flex h-full flex-col space-y-6 lg:w-1/5">
<SidebarNav items={sidebarNavItems} />
</aside>
<div className="h-full w-full flex-1">
<Outlet />
</div>
</div>
</>
</PageLayout>
);
}

View file

@ -9,7 +9,7 @@ import { Button } from "../../components/ui/button";
import { CONTROL_PATCH_USER_STATE } from "../../constants/constants";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { TabsContext } from "../../contexts/tabsContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { resetPassword, updateUser } from "../../controllers/API";
import {
inputHandlerEventType,
@ -17,7 +17,7 @@ import {
} from "../../types/components";
import { gradients } from "../../utils/styleUtils";
export default function ProfileSettingsPage(): JSX.Element {
const { setTabId } = useContext(TabsContext);
const { setTabId } = useContext(FlowsContext);
const [inputState, setInputState] = useState<patchUserInputStateType>(
CONTROL_PATCH_USER_STATE

View file

@ -0,0 +1,412 @@
import { uniqueId } from "lodash";
import { useContext, useEffect, useState } from "react";
import PaginatorComponent from "../../components/PaginatorComponent";
import ShadTooltip from "../../components/ShadTooltipComponent";
import CollectionCardComponent from "../../components/cardComponent";
import IconComponent from "../../components/genericIconComponent";
import PageLayout from "../../components/pageLayout";
import { SkeletonCardComponent } from "../../components/skeletonCardComponent";
import { Button } from "../../components/ui/button";
import { Input } from "../../components/ui/input";
import { Link, useParams } from "react-router-dom";
import { TagsSelector } from "../../components/tagsSelectorComponent";
import { Badge } from "../../components/ui/badge";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../components/ui/select";
import { alertContext } from "../../contexts/alertContext";
import { AuthContext } from "../../contexts/authContext";
import { FlowsContext } from "../../contexts/flowsContext";
import { StoreContext } from "../../contexts/storeContext";
import { getStoreComponents, getStoreTags } from "../../controllers/API";
import StoreApiKeyModal from "../../modals/StoreApiKeyModal";
import { storeComponent } from "../../types/store";
import { cn } from "../../utils/utils";
export default function StorePage(): JSX.Element {
const { validApiKey, setValidApiKey, hasApiKey, loadingApiKey } =
useContext(StoreContext);
const { apiKey } = useContext(AuthContext);
const { setErrorData } = useContext(alertContext);
const { setTabId } = useContext(FlowsContext);
const [loading, setLoading] = useState(true);
const [loadingTags, setLoadingTags] = useState(true);
const { id } = useParams();
const [filteredCategories, setFilterCategories] = useState<any[]>([]);
const [inputText, setInputText] = useState<string>("");
const [searchData, setSearchData] = useState<storeComponent[]>([]);
const [totalRowsCount, setTotalRowsCount] = useState(0);
const [pageSize, setPageSize] = useState(12);
const [pageIndex, setPageIndex] = useState(1);
const [pageOrder, setPageOrder] = useState("Popular");
const [tags, setTags] = useState<{ id: string; name: string }[]>([]);
const [tabActive, setTabActive] = useState("All");
const [searchNow, setSearchNow] = useState("");
const [selectFilter, setSelectFilter] = useState("all");
useEffect(() => {
handleGetTags();
}, []);
useEffect(() => {
if (!loadingApiKey) {
if (!hasApiKey) {
setErrorData({
title: "API Key Error",
list: [
"You don't have an API Key. Please add one to use the Langflow Store.",
],
});
setLoading(false);
} else if (!validApiKey) {
setErrorData({
title: "API Key Error",
list: [
"Your API Key is not valid. Please add a valid API Key to use the Langflow Store.",
],
});
}
}
}, [loadingApiKey, validApiKey, hasApiKey]);
useEffect(() => {
handleGetComponents();
}, [
tabActive,
pageOrder,
pageIndex,
pageSize,
filteredCategories,
selectFilter,
validApiKey,
hasApiKey,
apiKey,
searchNow,
loadingApiKey,
id,
]);
function handleGetTags() {
setLoadingTags(true);
getStoreTags()
.then((res) => {
setTags(res);
setLoadingTags(false);
})
.catch((err) => {
console.log(err);
setLoadingTags(false);
});
}
function handleGetComponents() {
if (!hasApiKey || loadingApiKey) return;
setLoading(true);
getStoreComponents({
component_id: id,
page: pageIndex,
limit: pageSize,
is_component:
tabActive === "All" ? null : tabActive === "Flows" ? false : true,
sort: pageOrder === "Popular" ? "-count(downloads)" : "name",
tags: filteredCategories,
liked: selectFilter === "likedbyme" && validApiKey ? true : null,
isPrivate: null,
search: inputText === "" ? null : inputText,
filterByUser: selectFilter === "createdbyme" && validApiKey ? true : null,
})
.then((res) => {
if (!res?.authorized && validApiKey === true) {
setValidApiKey(false);
setSelectFilter("all");
} else {
if (res?.authorized) {
setValidApiKey(true);
}
setLoading(false);
setSearchData(res?.results ?? []);
setTotalRowsCount(
filteredCategories?.length === 0
? Number(res?.count ?? 0)
: res?.results?.length ?? 0
);
}
})
.catch((err) => {
if (err.response.status === 403 || err.response.status === 401) {
setValidApiKey(false);
} else {
setSearchData([]);
setTotalRowsCount(0);
setLoading(false);
setErrorData({
title: "Error getting components.",
list: [err["response"]["data"]["detail"]],
});
}
});
}
// Set a null id
useEffect(() => {
setTabId("");
}, []);
function resetPagination() {
setPageIndex(1);
setPageSize(12);
}
return (
<PageLayout
title="Langflow Store"
description="Search flows and components from the community."
button={
<>
{StoreApiKeyModal && (
<StoreApiKeyModal disabled={loading}>
<Button
disabled={loading}
className={cn(
`${!validApiKey ? "animate-pulse border-error" : ""}`,
loading ? "cursor-not-allowed" : ""
)}
variant="primary"
>
<IconComponent name="Key" className="mr-2 w-4" />
API Key
</Button>
</StoreApiKeyModal>
)}
</>
}
>
<div className="flex h-full w-full flex-col justify-between">
<div className="flex w-full flex-col gap-4 p-0">
<div className="flex items-end gap-4">
<div className="relative h-12 w-[40%]">
<Input
disabled={loading}
placeholder="Search Flows and Components"
className="absolute h-12 pl-5 pr-12"
onChange={(e) => {
setInputText(e.target.value);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
setSearchNow(uniqueId());
}
}}
value={inputText}
/>
<button
disabled={loading}
className="absolute bottom-0 right-4 top-0 my-auto h-6 cursor-pointer stroke-1 text-muted-foreground"
onClick={() => {
setSearchNow(uniqueId());
}}
>
<IconComponent
name={loading ? "Loader2" : "Search"}
className={loading ? " animate-spin cursor-not-allowed" : ""}
/>
</button>
</div>
<div className="ml-4 flex w-full gap-2 border-b border-border">
<button
disabled={loading}
onClick={() => {
setTabActive("All");
}}
className={
(tabActive === "All"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
All
</button>
<button
disabled={loading}
onClick={() => {
resetPagination();
setTabActive("Flows");
}}
className={
(tabActive === "Flows"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Flows
</button>
<button
disabled={loading}
onClick={() => {
resetPagination();
setTabActive("Components");
}}
className={
(tabActive === "Components"
? "border-b-2 border-primary p-3"
: " border-b-2 border-transparent p-3 text-muted-foreground hover:text-primary") +
(loading ? " cursor-not-allowed " : "")
}
>
Components
</button>
<ShadTooltip content="Coming Soon">
<button className="cursor-not-allowed p-3 text-muted-foreground">
Bundles
</button>
</ShadTooltip>
</div>
</div>
<div className="flex items-center gap-2">
<Select
disabled={loading}
onValueChange={setSelectFilter}
value={selectFilter}
>
<SelectTrigger className="mr-4 w-[160px] flex-shrink-0">
<SelectValue placeholder="Filter Values" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectItem value="all">All</SelectItem>
<SelectItem
disabled={!hasApiKey || !validApiKey}
value="createdbyme"
>
Created By Me
</SelectItem>
<SelectItem
disabled={!hasApiKey || !validApiKey}
value="likedbyme"
>
Liked By Me
</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
{id === undefined ? (
<TagsSelector
tags={tags}
loadingTags={loadingTags}
disabled={loading}
selectedTags={filteredCategories}
setSelectedTags={setFilterCategories}
/>
) : (
<Badge
key="id"
variant="outline"
size="sq"
className="gap-2 bg-beta-foreground text-background hover:bg-beta-foreground"
>
<Link to={"/store"} className="cursor-pointer">
<IconComponent name="X" className="h-4 w-4" />
</Link>
{id}
</Badge>
)}
</div>
<div className="flex items-end justify-between">
<span className="px-0.5 text-sm text-muted-foreground">
{(!loading || searchData.length !== 0) && (
<>
{totalRowsCount} {totalRowsCount !== 1 ? "results" : "result"}
</>
)}
</span>
<Select
disabled={loading}
onValueChange={(e) => {
setPageOrder(e);
}}
>
<SelectTrigger>
<SelectValue placeholder="Popular" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Popular">Popular</SelectItem>
{/* <SelectItem value="Recent">Most Recent</SelectItem> */}
<SelectItem value="Alphabetical">Alphabetical</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid w-full gap-4 md:grid-cols-2 lg:grid-cols-3">
{!loading || searchData.length !== 0 ? (
searchData.map((item) => {
return (
<>
<CollectionCardComponent
key={item.id}
data={item}
authorized={validApiKey}
disabled={loading}
/>
</>
);
})
) : (
<>
<SkeletonCardComponent />
<SkeletonCardComponent />
<SkeletonCardComponent />
</>
)}
</div>
{!loading && searchData?.length === 0 && (
<div className="mt-6 flex w-full items-center justify-center text-center">
<div className="flex h-full w-full flex-col">
<div className="flex w-full flex-col gap-4">
<div className="grid w-full gap-4">
{selectFilter != "all" ? (
<>
You haven't{" "}
{selectFilter === "createdbyme" ? "created" : "liked"}{" "}
anything with the selected filters yet.
</>
) : (
<>
There are no{" "}
{tabActive == "Flows" ? "Flows" : "Components"} with the
selected filters.
</>
)}
</div>
</div>
</div>
</div>
)}
</div>
{!loading && searchData.length > 0 && (
<div className="relative py-6">
<PaginatorComponent
storeComponent={true}
pageIndex={pageIndex}
pageSize={pageSize}
totalRowsCount={totalRowsCount}
paginate={(pageSize, pageIndex) => {
setPageIndex(pageIndex);
setPageSize(pageSize);
}}
></PaginatorComponent>
</div>
)}
</div>
</PageLayout>
);
}

View file

@ -1,12 +1,8 @@
import { useContext, useEffect, useState } from "react";
import { useContext, useEffect } from "react";
import { useParams } from "react-router-dom";
import { darkContext } from "../../contexts/darkContext";
import { TabsContext } from "../../contexts/tabsContext";
import { getVersion } from "../../controllers/API";
import Page from "../FlowPage/components/PageComponent";
export default function ViewPage() {
const { flows, tabId, setTabId } = useContext(TabsContext);
const { setDark } = useContext(darkContext);
const { id, theme } = useParams();
@ -15,22 +11,6 @@ export default function ViewPage() {
setTabId(id!);
}, [id]);
useEffect(() => {
if (theme) {
setDark(theme === "dark");
} else {
setDark(false);
}
}, [theme]);
// Initialize state variable for the version
const [version, setVersion] = useState("");
useEffect(() => {
getVersion().then((data) => {
setVersion(data.version);
});
}, []);
return (
<div className="flow-page-positioning">
{flows.length > 0 &&

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