Merge remote-tracking branch 'origin/dev' into aws-cdk
This commit is contained in:
commit
01c23fdb77
316 changed files with 13915 additions and 7688 deletions
1632
src/frontend/package-lock.json
generated
1632
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -111,7 +111,7 @@
|
|||
"@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",
|
||||
|
|
|
|||
|
|
@ -42,9 +42,7 @@ export default function App() {
|
|||
successData,
|
||||
successOpen,
|
||||
setSuccessOpen,
|
||||
setErrorData,
|
||||
loading,
|
||||
setLoading,
|
||||
} = useContext(alertContext);
|
||||
const navigate = useNavigate();
|
||||
const { fetchError } = useContext(typesContext);
|
||||
|
|
|
|||
|
|
@ -22,11 +22,20 @@ 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 {
|
||||
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,
|
||||
|
|
@ -44,7 +53,6 @@ export default function ParameterComponent({
|
|||
left,
|
||||
id,
|
||||
data,
|
||||
setData,
|
||||
tooltipTitle,
|
||||
title,
|
||||
color,
|
||||
|
|
@ -60,9 +68,13 @@ export default function ParameterComponent({
|
|||
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(FlowsContext);
|
||||
const { setTabsState, tabId, flows, tabsState, updateFlow } =
|
||||
useContext(FlowsContext);
|
||||
|
||||
const [dataRef, setDataRef] = useState(data);
|
||||
|
||||
const flow = flows.find((flow) => flow.id === tabId)?.data?.nodes ?? null;
|
||||
|
||||
|
|
@ -80,38 +92,103 @@ 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 === scapedJSONStringfy(id)) ?? false;
|
||||
.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!.template[name].value !== newNodeClass.template[name].value) {
|
||||
takeSnapshot();
|
||||
}
|
||||
data.node = newNodeClass;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
const [errorDuplicateKey, setErrorDuplicateKey] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -168,21 +245,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>
|
||||
|
|
@ -199,17 +295,7 @@ 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 ? (
|
||||
<></>
|
||||
) : (
|
||||
<Button className="h-7 truncate bg-muted p-0 text-sm font-normal text-black hover:bg-muted">
|
||||
|
|
@ -282,17 +368,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">
|
||||
|
|
@ -352,6 +428,7 @@ export default function ParameterComponent({
|
|||
value={data.node.template[name].value ?? ""}
|
||||
onChange={handleOnNewValue}
|
||||
id={"textarea-" + index}
|
||||
data-testid={"textarea-" + index}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
|
|
@ -386,12 +463,26 @@ export default function ParameterComponent({
|
|||
) : 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">
|
||||
|
|
@ -402,9 +493,7 @@ export default function ParameterComponent({
|
|||
: 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 ?? ""}
|
||||
|
|
@ -419,7 +508,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;
|
||||
}}
|
||||
|
|
@ -439,19 +527,13 @@ export default function ParameterComponent({
|
|||
<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" ? (
|
||||
|
|
@ -471,6 +553,7 @@ export default function ParameterComponent({
|
|||
data.node!.template[name].value = newValue;
|
||||
handleOnNewValue(newValue);
|
||||
}}
|
||||
id="div-dict-input"
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "dict" ? (
|
||||
|
|
@ -479,10 +562,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) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { NodeToolbar, useUpdateNodeInternals } from "reactflow";
|
||||
import ShadTooltip from "../../components/ShadTooltipComponent";
|
||||
|
|
@ -6,23 +5,22 @@ 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 { 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,
|
||||
handleKeyDown,
|
||||
scapedJSONStringfy,
|
||||
} from "../../utils/reactflowUtils";
|
||||
import { handleKeyDown, scapedJSONStringfy } from "../../utils/reactflowUtils";
|
||||
import { nodeColors, nodeIconsLucide } from "../../utils/styleUtils";
|
||||
import { classNames, getFieldTitle } from "../../utils/utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
|
||||
export default function GenericNode({
|
||||
data: olddata,
|
||||
data,
|
||||
xPos,
|
||||
yPos,
|
||||
selected,
|
||||
|
|
@ -32,13 +30,13 @@ export default function GenericNode({
|
|||
xPos: number;
|
||||
yPos: number;
|
||||
}): JSX.Element {
|
||||
const [data, setData] = useState(olddata);
|
||||
const { updateFlow, flows, tabId } = useContext(FlowsContext);
|
||||
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(true);
|
||||
const [inputName, setInputName] = useState(false);
|
||||
const [nodeName, setNodeName] = useState(data.node!.display_name);
|
||||
const [inputDescription, setInputDescription] = useState(false);
|
||||
const [nodeDescription, setNodeDescription] = useState(
|
||||
|
|
@ -46,9 +44,11 @@ export default function GenericNode({
|
|||
);
|
||||
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)
|
||||
|
|
@ -81,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(() => {
|
||||
|
|
@ -123,15 +104,23 @@ export default function GenericNode({
|
|||
setValidationStatus(null);
|
||||
}
|
||||
}, [sseData, data.id]);
|
||||
|
||||
const showNode = data.showNode ?? true;
|
||||
|
||||
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>
|
||||
|
|
@ -140,10 +129,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"
|
||||
)}
|
||||
>
|
||||
|
|
@ -154,6 +140,7 @@ export default function GenericNode({
|
|||
)}
|
||||
<div>
|
||||
<div
|
||||
data-testid={"div-generic-node"}
|
||||
className={
|
||||
"generic-node-div-title " +
|
||||
(!showNode
|
||||
|
|
@ -168,10 +155,10 @@ export default function GenericNode({
|
|||
}
|
||||
>
|
||||
<IconComponent
|
||||
name={data.node?.flow ? "Ungroup" : 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]]}`}
|
||||
/>
|
||||
|
|
@ -180,7 +167,6 @@ export default function GenericNode({
|
|||
{data.node?.flow && inputName ? (
|
||||
<div>
|
||||
<InputComponent
|
||||
autoFocus
|
||||
onBlur={() => {
|
||||
setInputName(false);
|
||||
if (nodeName.trim() !== "") {
|
||||
|
|
@ -199,10 +185,21 @@ export default function GenericNode({
|
|||
) : (
|
||||
<ShadTooltip content={data.node?.display_name}>
|
||||
<div
|
||||
className="generic-node-tooltip-div text-primary"
|
||||
onDoubleClick={() => setInputName(true)}
|
||||
className="flex"
|
||||
onDoubleClick={() => {
|
||||
setInputName(true);
|
||||
takeSnapshot();
|
||||
}}
|
||||
>
|
||||
{data.node?.display_name}
|
||||
<div className="generic-node-tooltip-div pr-2 text-primary">
|
||||
{data.node?.display_name}
|
||||
</div>
|
||||
{data.node?.flow && (
|
||||
<IconComponent
|
||||
name="Pencil"
|
||||
className="h-4 w-4 text-ring"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
|
|
@ -229,7 +226,6 @@ export default function GenericNode({
|
|||
proxy: data.node!.template[templateField].proxy,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={
|
||||
nodeColors[
|
||||
types[data.node?.template[templateField].type!]
|
||||
|
|
@ -278,7 +274,6 @@ export default function GenericNode({
|
|||
dataType: data.type,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={nodeColors[types[data.type]] ?? nodeColors.unknown}
|
||||
title={
|
||||
data.node?.output_types &&
|
||||
|
|
@ -366,7 +361,7 @@ export default function GenericNode({
|
|||
<div
|
||||
className={
|
||||
showNode
|
||||
? "generic-node-desc " +
|
||||
? "generic-node-desc overflow-hidden " +
|
||||
(data.node?.description !== "" ? "py-5" : "pb-5")
|
||||
: ""
|
||||
}
|
||||
|
|
@ -408,8 +403,11 @@ export default function GenericNode({
|
|||
/>
|
||||
) : (
|
||||
<div
|
||||
className="generic-node-desc-text"
|
||||
onDoubleClick={() => setInputDescription(true)}
|
||||
className="generic-node-desc-text truncate-multiline word-break-break-word"
|
||||
onDoubleClick={() => {
|
||||
setInputDescription(true);
|
||||
takeSnapshot();
|
||||
}}
|
||||
>
|
||||
{data.node?.description}
|
||||
</div>
|
||||
|
|
@ -417,7 +415,15 @@ export default function GenericNode({
|
|||
<>
|
||||
{Object.keys(data.node!.template)
|
||||
.filter((templateField) => templateField.charAt(0) !== "_")
|
||||
.sort()
|
||||
.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 &&
|
||||
|
|
@ -433,13 +439,12 @@ export default function GenericNode({
|
|||
proxy: data.node!.template[templateField].proxy,
|
||||
})}
|
||||
data={data}
|
||||
setData={setData}
|
||||
color={
|
||||
nodeColors[
|
||||
types[data.node?.template[templateField].type!]
|
||||
data.node?.template[templateField].type!
|
||||
] ??
|
||||
nodeColors[
|
||||
data.node?.template[templateField].type!
|
||||
types[data.node?.template[templateField].type!]
|
||||
] ??
|
||||
nodeColors.unknown
|
||||
}
|
||||
|
|
@ -483,30 +488,37 @@ export default function GenericNode({
|
|||
>
|
||||
{" "}
|
||||
</div>
|
||||
<ParameterComponent
|
||||
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 && 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}
|
||||
/>
|
||||
{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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 ">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -1,31 +1,18 @@
|
|||
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";
|
||||
|
||||
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,19 +21,6 @@ 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);
|
||||
};
|
||||
|
||||
|
|
@ -62,9 +36,6 @@ export const EditFlowSettings: React.FC<InputProps> = ({
|
|||
{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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const SanitizedHTMLWrapper = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
data-testid="edit-prompt-sanitized"
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHTML }}
|
||||
suppressContentEditableWarning={suppressWarning}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import { useContext } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { cardComponentPropsType } from "../../types/components";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
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(FlowsContext);
|
||||
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>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
63
src/frontend/src/components/cardsWrapComponent/index.tsx
Normal file
63
src/frontend/src/components/cardsWrapComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -26,7 +26,7 @@ export default function BuildTrigger({
|
|||
}): JSX.Element {
|
||||
const { updateSSEData, isBuilding, setIsBuilding, sseData } = useSSE();
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setTabsState } = useContext(FlowsContext);
|
||||
const { setTabsState, saveFlow } = useContext(FlowsContext);
|
||||
const { setErrorData, setSuccessData } = useContext(alertContext);
|
||||
const [isIconTouched, setIsIconTouched] = useState(false);
|
||||
const eventClick = isBuilding ? "pointer-events-none" : "";
|
||||
|
|
@ -76,6 +76,7 @@ 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export default function CodeAreaComponent({
|
|||
<div className="flex w-full items-center">
|
||||
<span
|
||||
id={id}
|
||||
data-testid={id}
|
||||
className={
|
||||
editNode
|
||||
? "input-edit-node input-dialog"
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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
|
||||
) => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export default function DictComponent({
|
|||
onChange,
|
||||
disabled,
|
||||
editNode = false,
|
||||
id = "",
|
||||
}: DictComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -30,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) => {
|
||||
|
|
@ -45,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -3,14 +3,24 @@ import { IconComponentProps } from "../../types/components";
|
|||
import { nodeIconsLucide } from "../../utils/styleUtils";
|
||||
|
||||
const ForwardedIconComponent = forwardRef(
|
||||
({ name, className, iconColor }: IconComponentProps, ref) => {
|
||||
(
|
||||
{ name, className, iconColor, stroke, id = "" }: IconComponentProps,
|
||||
ref
|
||||
) => {
|
||||
const TargetIcon = nodeIconsLucide[name] ?? nodeIconsLucide["unknown"];
|
||||
|
||||
const style = {
|
||||
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}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { USER_PROJECTS_HEADER } from "../../constants/constants";
|
|||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { AuthContext } from "../../contexts/authContext";
|
||||
import { darkContext } from "../../contexts/darkContext";
|
||||
import { StoreContext } from "../../contexts/storeContext";
|
||||
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { gradients } from "../../utils/styleUtils";
|
||||
import IconComponent from "../genericIconComponent";
|
||||
|
|
@ -27,12 +29,13 @@ export default function Header(): JSX.Element {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -55,7 +55,13 @@ 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();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ export default function InputFileComponent({
|
|||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
suffixes,
|
||||
fileTypes,
|
||||
onFileChange,
|
||||
editNode = false,
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -36,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();
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ export default function KeypairListComponent({
|
|||
disabled,
|
||||
editNode = false,
|
||||
duplicateKey,
|
||||
advanced = false,
|
||||
dataValue,
|
||||
}: KeyPairListComponentType): JSX.Element {
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
|
|
@ -55,7 +53,10 @@ export default function KeypairListComponent({
|
|||
return (
|
||||
<div key={idx} className="flex w-full gap-2">
|
||||
<Input
|
||||
id={"keypair" + index}
|
||||
data-testid={
|
||||
editNode ? "editNodekeypair" + index : "keypair" + index
|
||||
}
|
||||
id={editNode ? "editNodekeypair" + index : "keypair" + index}
|
||||
type="text"
|
||||
value={key.trim()}
|
||||
className={classNames(
|
||||
|
|
@ -73,7 +74,16 @@ export default function KeypairListComponent({
|
|||
/>
|
||||
|
||||
<Input
|
||||
id={"keypair" + (index + 100).toString()}
|
||||
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" : ""}
|
||||
|
|
@ -90,7 +100,16 @@ export default function KeypairListComponent({
|
|||
newInputList.push({ "": "" });
|
||||
onChange(newInputList);
|
||||
}}
|
||||
id={"plusbtn" + index.toString()}
|
||||
id={
|
||||
editNode
|
||||
? "editNodeplusbtn" + index.toString()
|
||||
: "plusbtn" + index.toString()
|
||||
}
|
||||
data-testid={
|
||||
editNode
|
||||
? "editNodeplusbtn" + index.toString()
|
||||
: "plusbtn" + index.toString()
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name="Plus"
|
||||
|
|
@ -104,7 +123,16 @@ export default function KeypairListComponent({
|
|||
newInputList.splice(index, 1);
|
||||
onChange(newInputList);
|
||||
}}
|
||||
id={"minusbtn" + index.toString()}
|
||||
data-testid={
|
||||
editNode
|
||||
? "editNodeminusbtn" + index.toString()
|
||||
: "minusbtn" + index.toString()
|
||||
}
|
||||
id={
|
||||
editNode
|
||||
? "editNodeminusbtn" + index.toString()
|
||||
: "minusbtn" + index.toString()
|
||||
}
|
||||
>
|
||||
<IconComponent
|
||||
name="X"
|
||||
|
|
|
|||
31
src/frontend/src/components/pageLayout/index.tsx
Normal file
31
src/frontend/src/components/pageLayout/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -23,18 +22,6 @@ export default function PromptAreaComponent({
|
|||
}
|
||||
}, [disabled]);
|
||||
|
||||
useEffect(() => {
|
||||
//prevent update from prompt template after group node if prompt is wrongly marked as not dynamic
|
||||
if (value !== "" && !editNode && !readonly && !nodeClass?.flow) {
|
||||
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
|
||||
|
|
@ -44,15 +31,14 @@ export default function PromptAreaComponent({
|
|||
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"
|
||||
|
|
@ -64,6 +50,7 @@ export default function PromptAreaComponent({
|
|||
</span>
|
||||
{!editNode && (
|
||||
<IconComponent
|
||||
id={id}
|
||||
name="ExternalLink"
|
||||
className={
|
||||
"icons-parameters-comp" +
|
||||
|
|
|
|||
47
src/frontend/src/components/sidebarComponent/index.tsx
Normal file
47
src/frontend/src/components/sidebarComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
30
src/frontend/src/components/stackedComponents/index.tsx
Normal file
30
src/frontend/src/components/stackedComponents/index.tsx
Normal 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;
|
||||
12
src/frontend/src/components/storeGuard/index.tsx
Normal file
12
src/frontend/src/components/storeGuard/index.tsx
Normal 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;
|
||||
};
|
||||
111
src/frontend/src/components/tagsSelectorComponent/index.tsx
Normal file
111
src/frontend/src/components/tagsSelectorComponent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,29 +20,39 @@ export default function TextAreaComponent({
|
|||
}, [disabled]);
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center">
|
||||
<Input
|
||||
id={id}
|
||||
<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" +
|
||||
|
|
@ -50,8 +60,8 @@ export default function TextAreaComponent({
|
|||
}
|
||||
/>
|
||||
)}
|
||||
</GenericModal>
|
||||
</div>
|
||||
</div>
|
||||
</GenericModal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
79
src/frontend/src/components/ui/combobox.tsx
Normal file
79
src/frontend/src/components/ui/combobox.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
155
src/frontend/src/components/ui/command.tsx
Normal file
155
src/frontend/src/components/ui/command.tsx
Normal 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,
|
||||
};
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
|||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 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}
|
||||
|
|
|
|||
|
|
@ -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"]);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -28,17 +34,23 @@ import {
|
|||
sourceHandleType,
|
||||
targetHandleType,
|
||||
} from "../types/flow";
|
||||
import { FlowsContextType, TabsState } from "../types/tabs";
|
||||
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";
|
||||
|
|
@ -51,7 +63,11 @@ const FlowsContextInitialValue: FlowsContextType = {
|
|||
isLoading: true,
|
||||
flows: [],
|
||||
removeFlow: (id: string) => {},
|
||||
addFlow: async (newProject: boolean, flowData?: FlowType) => "",
|
||||
addFlow: async (
|
||||
newProject: boolean,
|
||||
flowData?: FlowType,
|
||||
override?: boolean
|
||||
) => "",
|
||||
updateFlow: (newFlow: FlowType) => {},
|
||||
incrementNodeId: () => uid(),
|
||||
downloadFlow: (flow: FlowType) => {},
|
||||
|
|
@ -65,7 +81,8 @@ const FlowsContextInitialValue: FlowsContextType = {
|
|||
lastCopiedSelection: null,
|
||||
setLastCopiedSelection: (selection: any) => {},
|
||||
tabsState: {},
|
||||
setTabsState: (state: TabsState) => {},
|
||||
setTabsState: (state: FlowsState) => {},
|
||||
saveCurrentFlow: () => {},
|
||||
getNodeId: (nodeType: string) => "",
|
||||
setTweak: (tweak: any) => {},
|
||||
getTweak: [],
|
||||
|
|
@ -73,29 +90,35 @@ const FlowsContextInitialValue: FlowsContextType = {
|
|||
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 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(() => {
|
||||
|
|
@ -113,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) {}
|
||||
|
|
@ -127,25 +150,42 @@ 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;
|
||||
}
|
||||
processDataFromFlow(flow, false);
|
||||
} 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;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -170,28 +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 (node.data.node?.flow) return;
|
||||
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);
|
||||
//update baseclasses in edges
|
||||
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"];
|
||||
}
|
||||
|
|
@ -244,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
|
||||
|
|
@ -287,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(newProject, flow);
|
||||
});
|
||||
}
|
||||
// parse the text into a JSON object
|
||||
let flow: FlowType = JSON.parse(text);
|
||||
|
||||
id = await addFlow(newProject, flow);
|
||||
} 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(newProject, flow);
|
||||
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() {
|
||||
|
|
@ -357,12 +405,13 @@ 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));
|
||||
}
|
||||
}
|
||||
/**
|
||||
|
|
@ -389,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
|
||||
|
|
@ -470,7 +522,9 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
const addFlow = async (
|
||||
newProject: Boolean,
|
||||
flow?: FlowType
|
||||
flow?: FlowType,
|
||||
override?: boolean,
|
||||
position?: XYPosition
|
||||
): Promise<String | undefined> => {
|
||||
if (newProject) {
|
||||
let flowData = flow
|
||||
|
|
@ -478,12 +532,21 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
: { 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;
|
||||
addFlowToLocalState(newFlow);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFlow = createNewFlow(flowData, flow!);
|
||||
|
||||
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.
|
||||
|
|
@ -501,7 +564,7 @@ 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 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -522,46 +585,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
};
|
||||
|
||||
const updateEdges = (edges: Edge[]) => {
|
||||
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 updateNodes = (nodes: Node[], edges: Edge[]) => {
|
||||
nodes.forEach((node) => {
|
||||
if (node.data.node?.flow) return;
|
||||
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) => {
|
||||
let sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
if (edge.source === node.id) {
|
||||
let newSourceHandle = sourceHandleObject;
|
||||
newSourceHandle.baseClasses.concat(template["base_classes"]);
|
||||
edge.sourceHandle = scapedJSONStringfy(newSourceHandle);
|
||||
}
|
||||
});
|
||||
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 = (
|
||||
|
|
@ -572,12 +606,17 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
name: flow?.name ?? getRandomName(),
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -593,10 +632,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
|
||||
|
|
@ -635,16 +684,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 (
|
||||
<FlowsContext.Provider
|
||||
value={{
|
||||
version,
|
||||
saveFlow,
|
||||
isBuilt,
|
||||
setIsBuilt,
|
||||
lastCopiedSelection,
|
||||
setLastCopiedSelection,
|
||||
saveCurrentFlow,
|
||||
hardReset,
|
||||
tabId,
|
||||
setTabId,
|
||||
|
|
@ -664,6 +738,10 @@ export function TabsProvider({ children }: { children: ReactNode }) {
|
|||
getTweak,
|
||||
setTweak,
|
||||
isLoading,
|
||||
saveComponent,
|
||||
deleteComponent,
|
||||
nodesOnFlow,
|
||||
setNodesOnFlow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ import { SSEProvider } from "./SSEContext";
|
|||
import { AlertProvider } from "./alertContext";
|
||||
import { AuthProvider } from "./authContext";
|
||||
import { DarkProvider } from "./darkContext";
|
||||
import { TabsProvider } from "./flowsContext";
|
||||
import { FlowsProvider } from "./flowsContext";
|
||||
import { LocationProvider } from "./locationContext";
|
||||
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>
|
||||
|
|
|
|||
74
src/frontend/src/contexts/storeContext.tsx
Normal file
74
src/frontend/src/contexts/storeContext.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
template,
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ 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 { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { TemplateVariableType } from "../../types/api";
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
62
src/frontend/src/modals/DeleteConfirmationModal/index.tsx
Normal file
62
src/frontend/src/modals/DeleteConfirmationModal/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,12 +1,5 @@
|
|||
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";
|
||||
|
|
@ -32,7 +25,11 @@ import {
|
|||
TableHeader,
|
||||
TableRow,
|
||||
} from "../../components/ui/table";
|
||||
import { limitScrollFieldsModal } from "../../constants/constants";
|
||||
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";
|
||||
|
|
@ -41,6 +38,7 @@ import {
|
|||
convertObjToArray,
|
||||
convertValuesToNumbers,
|
||||
hasDuplicateKeys,
|
||||
scapedJSONStringfy,
|
||||
} from "../../utils/reactflowUtils";
|
||||
import { classNames } from "../../utils/utils";
|
||||
import BaseModal from "../baseModal";
|
||||
|
|
@ -49,70 +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(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(() => {
|
||||
if (modalOpen) {
|
||||
myData.current = data; // reset data to what it is on node when opening modal
|
||||
onClose!(modalOpen);
|
||||
if (open) {
|
||||
setMyData(data); // reset data to what it is on node when opening modal
|
||||
}
|
||||
}, [modalOpen]);
|
||||
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">
|
||||
|
|
@ -129,7 +125,7 @@ const EditNodeModal = forwardRef(
|
|||
"edit-node-modal-box",
|
||||
nodeLength > limitScrollFieldsModal
|
||||
? "overflow-scroll overflow-x-hidden custom-scroll"
|
||||
: "overflow-hidden"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
{nodeLength > 0 && (
|
||||
|
|
@ -145,387 +141,383 @@ 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">
|
||||
<ShadTooltip
|
||||
content={
|
||||
myData.current.node?.template[templateParam]
|
||||
.proxy
|
||||
? myData.current.node?.template[
|
||||
templateParam
|
||||
].proxy?.id
|
||||
: null
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{myData.current.node?.template[templateParam]
|
||||
.display_name
|
||||
? myData.current.node.template[
|
||||
templateParam
|
||||
].display_name
|
||||
: myData.current.node?.template[
|
||||
templateParam
|
||||
].name}
|
||||
</span>
|
||||
</ShadTooltip>
|
||||
</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
|
||||
id={"textarea-edit-" + index}
|
||||
</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
|
||||
id={"input-" + index}
|
||||
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}
|
||||
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={"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
|
||||
id={"toggle-edit-" + index}
|
||||
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
|
||||
id={"int-input-" + index}
|
||||
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
|
||||
readonly={
|
||||
myData.current.node?.flow ? true : false
|
||||
}
|
||||
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);
|
||||
}}
|
||||
id={"prompt-area-edit" + index}
|
||||
/>
|
||||
</div>
|
||||
) : myData.current.node?.template[templateParam]
|
||||
.type === "code" ? (
|
||||
<div className="mx-auto">
|
||||
<CodeAreaComponent
|
||||
readonly={
|
||||
myData.current.node?.flow &&
|
||||
myData.current.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.current.node.template[
|
||||
templateParam
|
||||
].value ?? ""
|
||||
}
|
||||
onChange={(value: string | string[]) => {
|
||||
handleOnNewValue(value, templateParam);
|
||||
}}
|
||||
id={"code-area-edit" + index}
|
||||
/>
|
||||
</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
|
||||
id={
|
||||
"show" +
|
||||
myData.current.node?.template[templateParam]
|
||||
.name
|
||||
}
|
||||
enabled={
|
||||
!myData.current.node?.template[
|
||||
templateParam
|
||||
].advanced
|
||||
}
|
||||
setEnabled={(e) => {
|
||||
changeAdvanced(templateParam);
|
||||
}}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
|
@ -539,8 +531,7 @@ const EditNodeModal = forwardRef(
|
|||
id={"saveChangesBtn"}
|
||||
className="mt-3"
|
||||
onClick={() => {
|
||||
const newData = cloneDeep(myData.current);
|
||||
myData.current = newData;
|
||||
data.node = myData.node;
|
||||
//@ts-ignore
|
||||
setTabsState((prev: FlowsState) => {
|
||||
return {
|
||||
|
|
@ -551,7 +542,7 @@ const EditNodeModal = forwardRef(
|
|||
},
|
||||
};
|
||||
});
|
||||
setModalOpen(false);
|
||||
setOpen(false);
|
||||
}}
|
||||
type="submit"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
124
src/frontend/src/modals/StoreApiKeyModal/index.tsx
Normal file
124
src/frontend/src/modals/StoreApiKeyModal/index.tsx
Normal 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">
|
||||
Don’t 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -88,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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={() => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import BaseModal from "../baseModal";
|
|||
|
||||
const ExportModal = forwardRef(
|
||||
(props: { children: ReactNode }, ref): JSX.Element => {
|
||||
const { flows, tabId, downloadFlow } = useContext(FlowsContext);
|
||||
const { flows, tabId, downloadFlow, version } = useContext(FlowsContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const { setNoticeData } = useContext(alertContext);
|
||||
const [checked, setChecked] = useState(true);
|
||||
|
|
@ -26,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>
|
||||
|
|
@ -40,8 +40,6 @@ const ExportModal = forwardRef(
|
|||
<EditFlowSettings
|
||||
name={name}
|
||||
description={description}
|
||||
flows={flows}
|
||||
tabId={tabId}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
/>
|
||||
|
|
@ -57,7 +55,7 @@ const ExportModal = forwardRef(
|
|||
Save with my API keys
|
||||
</label>
|
||||
</div>
|
||||
<span className="text-xs text-destructive">
|
||||
<span className=" text-xs text-destructive ">
|
||||
Caution: Uncheck this box only removes API keys from fields
|
||||
specifically designated for API keys.
|
||||
</span>
|
||||
|
|
@ -70,9 +68,11 @@ const ExportModal = forwardRef(
|
|||
downloadFlow(
|
||||
{
|
||||
id: tabId,
|
||||
data: reactFlowInstance?.toObject()!,
|
||||
data: flow!.data!,
|
||||
description,
|
||||
name,
|
||||
last_tested_version: version,
|
||||
is_component: false,
|
||||
},
|
||||
name!,
|
||||
description
|
||||
|
|
@ -85,9 +85,11 @@ const ExportModal = forwardRef(
|
|||
downloadFlow(
|
||||
removeApiKeys({
|
||||
id: tabId,
|
||||
data: reactFlowInstance?.toObject()!,
|
||||
data: flow!.data!,
|
||||
description,
|
||||
name,
|
||||
last_tested_version: version,
|
||||
is_component: false,
|
||||
}),
|
||||
name!,
|
||||
description
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { Button } from "../../components/ui/button";
|
|||
import { SETTINGS_DIALOG_SUBTITLE } from "../../constants/constants";
|
||||
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(FlowsContext);
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -394,7 +394,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,
|
||||
|
|
|
|||
|
|
@ -40,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);
|
||||
|
|
@ -106,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?.[""]
|
||||
)
|
||||
|
|
@ -123,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 {
|
||||
|
|
@ -174,6 +158,10 @@ export default function GenericModal({
|
|||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setModalContextOpen(modalOpen);
|
||||
}, [modalOpen]);
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
onChangeOpenModal={(open) => {}}
|
||||
|
|
@ -195,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 "
|
||||
|
|
@ -213,6 +203,7 @@ export default function GenericModal({
|
|||
{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}
|
||||
|
|
@ -252,6 +243,8 @@ export default function GenericModal({
|
|||
handleKeyDown(e, value, "");
|
||||
}}
|
||||
readOnly={readonly}
|
||||
id={"text-area-modal"}
|
||||
data-testid={"text-area-modal"}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
|
|
@ -308,6 +301,7 @@ export default function GenericModal({
|
|||
)}
|
||||
</div>
|
||||
<Button
|
||||
data-testid="genericModalBtnSave"
|
||||
id="genericModalBtnSave"
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
|
|
|
|||
311
src/frontend/src/modals/shareModal/index.tsx
Normal file
311
src/frontend/src/modals/shareModal/index.tsx
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
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 {
|
||||
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 } = useContext(FlowsContext);
|
||||
const { hasApiKey, hasStore } = useContext(StoreContext);
|
||||
const { setSuccessData, setErrorData } = useContext(alertContext);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [name, setName] = useState(component?.name ?? "");
|
||||
const [description, setDescription] = useState(component?.description ?? "");
|
||||
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);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setName(component?.name ?? "");
|
||||
setDescription(component?.description ?? "");
|
||||
}, [component, open, internalOpen]);
|
||||
|
||||
const handleShareComponent = async (update = false) => {
|
||||
//remove file names from flows before sharing
|
||||
removeFileNameFromComponents(component);
|
||||
const flow: FlowType = checked
|
||||
? {
|
||||
id: component!.id,
|
||||
data: component!.data,
|
||||
description,
|
||||
name,
|
||||
last_tested_version: version,
|
||||
is_component: is_component,
|
||||
}
|
||||
: removeApiKeys({
|
||||
id: component!.id,
|
||||
data: component!.data,
|
||||
description,
|
||||
name,
|
||||
last_tested_version: version,
|
||||
is_component: is_component,
|
||||
});
|
||||
|
||||
await saveFlow(flow!, true);
|
||||
|
||||
function successShare() {
|
||||
if (is_component) {
|
||||
addFlow(true, flow);
|
||||
}
|
||||
setSuccessData({
|
||||
title: `${nameComponent} 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);
|
||||
};
|
||||
|
||||
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>{children ? children : <></>}</BaseModal.Trigger>
|
||||
<BaseModal.Header
|
||||
description={`Share your ${nameComponent} to the Langflow Store.`}
|
||||
>
|
||||
<span className="pr-2">Share</span>
|
||||
<IconComponent
|
||||
name="Share2"
|
||||
className="h-6 w-6 pl-1 text-foreground"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</BaseModal.Header>
|
||||
<BaseModal.Content>
|
||||
<EditFlowSettings
|
||||
name={name}
|
||||
invalidNameList={unavaliableNames.map((element) => element.name)}
|
||||
description={description}
|
||||
setName={setName}
|
||||
setDescription={setDescription}
|
||||
/>
|
||||
<div className="mt-3 flex h-8 w-full">
|
||||
<TagsSelector
|
||||
tags={tags}
|
||||
loadingTags={loadingTags}
|
||||
disabled={false}
|
||||
selectedTags={selectedTags}
|
||||
setSelectedTags={setSelectedTags}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="public"
|
||||
checked={sharePublic}
|
||||
onCheckedChange={(event: boolean) => {
|
||||
setSharePublic(event);
|
||||
setChecked(false);
|
||||
}}
|
||||
/>
|
||||
<label htmlFor="public" className="export-modal-save-api text-sm ">
|
||||
Make {nameComponent} public
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="terms"
|
||||
checked={checked}
|
||||
onCheckedChange={(event: boolean) => {
|
||||
setChecked(event);
|
||||
}}
|
||||
disabled={sharePublic}
|
||||
/>
|
||||
<label htmlFor="terms" className="export-modal-save-api text-sm ">
|
||||
Save with my API keys
|
||||
</label>
|
||||
</div>
|
||||
<span className=" text-xs text-destructive ">
|
||||
Warning: Deselecting this box will exclusively eliminate API keys
|
||||
from fields explicitly designated for API keys.
|
||||
</span>
|
||||
</BaseModal.Content>
|
||||
|
||||
<BaseModal.Footer>
|
||||
<div className="text-right">
|
||||
<Button
|
||||
disabled={loadingNames}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const isNameAvailable = !unavaliableNames.some(
|
||||
(element) => element.name === name
|
||||
);
|
||||
|
||||
if (isNameAvailable) {
|
||||
handleShareComponent();
|
||||
(setOpen || internalSetOpen)(false);
|
||||
} else {
|
||||
setOpenConfirmationModal(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{loadingNames ? (
|
||||
<>
|
||||
<div className="center w-16">
|
||||
<Loader2 className="m-auto h-4 w-4 animate-spin"></Loader2>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{is_component && !loadingNames ? "Save and " : ""}Share{" "}
|
||||
{!is_component && !loadingNames ? "Flow" : ""}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</BaseModal.Footer>
|
||||
</BaseModal>
|
||||
<>{modalConfirmation}</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,116 +0,0 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
|
||||
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(FlowsContext);
|
||||
|
||||
// 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(true, flow).then((id) => {
|
||||
navigate("/flow/" + id);
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="GitFork"
|
||||
className="main-page-nav-button"
|
||||
/>
|
||||
Fork Example
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,10 +13,8 @@ import ReactFlow, {
|
|||
Controls,
|
||||
Edge,
|
||||
EdgeChange,
|
||||
Node,
|
||||
NodeChange,
|
||||
NodeDragHandler,
|
||||
OnEdgesDelete,
|
||||
OnSelectionChangeParams,
|
||||
SelectionDragHandler,
|
||||
addEdge,
|
||||
|
|
@ -27,6 +25,7 @@ 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";
|
||||
|
|
@ -42,7 +41,7 @@ import {
|
|||
scapeJSONParse,
|
||||
validateSelection,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||
import { cn, getRandomName, isWrappedWithClass } from "../../../../utils/utils";
|
||||
import ConnectionLineComponent from "../ConnectionLineComponent";
|
||||
import SelectionMenu from "../SelectionMenuComponent";
|
||||
import ExtraSidebar from "../extraSidebarComponent";
|
||||
|
|
@ -61,7 +60,6 @@ export default function Page({
|
|||
let {
|
||||
updateFlow,
|
||||
uploadFlow,
|
||||
addFlow,
|
||||
getNodeId,
|
||||
paste,
|
||||
lastCopiedSelection,
|
||||
|
|
@ -70,6 +68,7 @@ export default function Page({
|
|||
saveFlow,
|
||||
setTabsState,
|
||||
tabId,
|
||||
saveCurrentFlow,
|
||||
} = useContext(FlowsContext);
|
||||
const {
|
||||
types,
|
||||
|
|
@ -83,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" &&
|
||||
|
|
@ -107,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 (
|
||||
|
|
@ -127,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);
|
||||
|
|
@ -143,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);
|
||||
|
||||
|
|
@ -152,48 +167,43 @@ 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);
|
||||
|
|
@ -203,10 +213,6 @@ export default function Page({
|
|||
const onEdgesChangeMod = useCallback(
|
||||
(change: EdgeChange[]) => {
|
||||
onEdgesChange(change);
|
||||
setNodes((node) => {
|
||||
let newX = _.cloneDeep(node);
|
||||
return newX;
|
||||
});
|
||||
//@ts-ignore
|
||||
setTabsState((prev: FlowsState) => {
|
||||
return {
|
||||
|
|
@ -217,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: FlowsState) => {
|
||||
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();
|
||||
|
|
@ -267,7 +283,7 @@ export default function Page({
|
|||
return newX;
|
||||
});
|
||||
},
|
||||
[setEdges, setNodes, takeSnapshot]
|
||||
[setEdges, setNodes, takeSnapshot, addEdge]
|
||||
);
|
||||
|
||||
const onNodeDragStart: NodeDragHandler = useCallback(() => {
|
||||
|
|
@ -281,11 +297,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")) {
|
||||
|
|
@ -311,9 +322,9 @@ export default function Page({
|
|||
);
|
||||
|
||||
// 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
|
||||
|
|
@ -350,7 +361,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",
|
||||
|
|
@ -364,6 +389,9 @@ export default function Page({
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
setExtraComponent(<ExtraSidebar />);
|
||||
setExtraNavigation({ title: "Components" });
|
||||
|
||||
return () => {
|
||||
if (tabsState && tabsState[flow.id]?.isPending) {
|
||||
saveFlow(flow);
|
||||
|
|
@ -371,21 +399,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;
|
||||
}, []);
|
||||
|
|
@ -437,6 +450,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 />}
|
||||
|
|
@ -448,15 +475,17 @@ export default function Page({
|
|||
{Object.keys(templates).length > 0 &&
|
||||
Object.keys(types).length > 0 ? (
|
||||
<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}
|
||||
|
|
@ -471,12 +500,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}
|
||||
|
|
@ -498,6 +525,7 @@ export default function Page({
|
|||
isVisible={selectionMenuVisible}
|
||||
nodes={lastSelection?.nodes}
|
||||
onClick={() => {
|
||||
takeSnapshot();
|
||||
if (
|
||||
validateSelection(lastSelection!, edges).length === 0
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { NodeToolbar } from "reactflow";
|
||||
import IconComponent from "../../../../components/genericIconComponent";
|
||||
import { GradientGroup } from "../../../../icons/GradientSparkles";
|
||||
export default function SelectionMenu({ onClick, nodes, isVisible }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
|
@ -37,15 +37,19 @@ export default function SelectionMenu({ onClick, nodes, isVisible }) {
|
|||
<div className="h-10 w-28 overflow-hidden">
|
||||
<div
|
||||
className={
|
||||
"h-10 w-24 rounded-md border border-indigo-300 bg-white px-2.5 text-gray-700 shadow-inner transition-all duration-500 ease-in-out dark:bg-gray-800 dark:text-gray-300" +
|
||||
(isTransitioning ? " translate-y-0" : " translate-y-10")
|
||||
"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}
|
||||
>
|
||||
<IconComponent name="Group" className="w-6" />
|
||||
<GradientGroup
|
||||
strokeWidth={1.5}
|
||||
size={22}
|
||||
className="text-primary"
|
||||
/>
|
||||
Group
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 { 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 } =
|
||||
const { flows, tabId, uploadFlow, tabsState, saveFlow, isBuilt, version } =
|
||||
useContext(FlowsContext);
|
||||
const { setSuccessData, setErrorData } = useContext(alertContext);
|
||||
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,12 +95,19 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (getFilterEdge.length === 0 && search === "") {
|
||||
setFilterData(data);
|
||||
setFilterEdge([]);
|
||||
if (getFilterEdge.length !== 0) {
|
||||
setSearch("");
|
||||
}
|
||||
}, [getFilterEdge]);
|
||||
|
||||
if (getFilterEdge.length === 0 && search === "") {
|
||||
setSearch("");
|
||||
setFilterData(data);
|
||||
}
|
||||
}, [getFilterEdge, data]);
|
||||
|
||||
useEffect(() => {
|
||||
handleSearchInput(search);
|
||||
}, [data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (getFilterEdge?.length > 0) {
|
||||
|
|
@ -128,6 +146,108 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
}
|
||||
}, [getFilterEdge]);
|
||||
|
||||
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, data]);
|
||||
|
||||
const ModalMemo = useMemo(
|
||||
() =>
|
||||
!hasApiKey || !validApiKey ? (
|
||||
<button
|
||||
disabled={!hasApiKey || !validApiKey || !hasStore}
|
||||
className={classNames(
|
||||
"extra-side-bar-buttons",
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? "button-disable cursor-default"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
name="Share2"
|
||||
className={classNames(
|
||||
"side-bar-button-size",
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? "extra-side-bar-save-disable"
|
||||
: ""
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
) : (
|
||||
<ShareModal
|
||||
is_component={false}
|
||||
component={flow!}
|
||||
disabled={!hasApiKey || !validApiKey || !hasStore}
|
||||
>
|
||||
<button
|
||||
disabled={!hasApiKey || !validApiKey || !hasStore}
|
||||
className={classNames(
|
||||
"extra-side-bar-buttons",
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? "button-disable cursor-default"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<IconComponent
|
||||
name="Share2"
|
||||
className={classNames(
|
||||
"side-bar-button-size",
|
||||
!hasApiKey || !validApiKey || !hasStore
|
||||
? "extra-side-bar-save-disable"
|
||||
: ""
|
||||
)}
|
||||
/>
|
||||
</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">
|
||||
|
|
@ -136,25 +256,21 @@ export default function ExtraSidebar(): JSX.Element {
|
|||
<button
|
||||
className="extra-side-bar-buttons"
|
||||
onClick={() => {
|
||||
uploadFlow(false);
|
||||
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>
|
||||
<div className="side-bar-button">{ExportMemo}</div>
|
||||
<ShadTooltip content={"Code"} side="top">
|
||||
<div className="side-bar-button">
|
||||
{flow && flow.data && (
|
||||
|
|
@ -177,30 +293,44 @@ 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"
|
||||
className={
|
||||
"side-bar-button-size" +
|
||||
(isPending ? " " : " extra-side-bar-save-disable")
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</ShadTooltip>
|
||||
{flow && flow.data && (
|
||||
<ShadTooltip content="Save" side="top">
|
||||
<div>
|
||||
<button
|
||||
disabled={flow?.data?.nodes.length === 0}
|
||||
className={
|
||||
"extra-side-bar-buttons " +
|
||||
(isPending && flow!.data!.nodes?.length > 0
|
||||
? ""
|
||||
: "button-disable")
|
||||
}
|
||||
onClick={(event) => {
|
||||
saveFlow(flow!);
|
||||
}}
|
||||
>
|
||||
<IconComponent
|
||||
name="Save"
|
||||
className={
|
||||
"side-bar-button-size" +
|
||||
(isPending && flow!.data!.nodes?.length > 0
|
||||
? " "
|
||||
: " extra-side-bar-save-disable")
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</ShadTooltip>
|
||||
)}
|
||||
</div>
|
||||
<ShadTooltip content="Share" side="top" styleClasses="cursor-default">
|
||||
<div className="side-bar-button">{ModalMemo}</div>
|
||||
</ShadTooltip>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="side-bar-search-div-placement">
|
||||
<Input
|
||||
onFocusCapture={() => handleBlur()}
|
||||
value={search}
|
||||
type="text"
|
||||
name="search"
|
||||
id="search"
|
||||
|
|
@ -223,7 +353,19 @@ 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
|
||||
|
|
@ -241,57 +383,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"
|
||||
id={
|
||||
"side" +
|
||||
data[SBSectionName][SBItemName].display_name
|
||||
}
|
||||
>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
@ -9,17 +10,23 @@ import {
|
|||
SelectTrigger,
|
||||
} from "../../../../components/ui/select-custom";
|
||||
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 { FlowType } from "../../../../types/flow";
|
||||
import {
|
||||
createFlowComponent,
|
||||
downloadNode,
|
||||
expandGroupNode,
|
||||
updateFlowPosition,
|
||||
} from "../../../../utils/reactflowUtils";
|
||||
import { classNames, getRandomKeyByssmm } from "../../../../utils/utils";
|
||||
import { classNames } from "../../../../utils/utils";
|
||||
|
||||
export default function NodeToolbarComponent({
|
||||
data,
|
||||
setData,
|
||||
deleteNode,
|
||||
position,
|
||||
setShowNode,
|
||||
|
|
@ -38,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;
|
||||
|
|
@ -54,30 +65,56 @@ export default function NodeToolbarComponent({
|
|||
const isMinimal = canMinimize();
|
||||
const isGroup = data.node?.flow ? true : false;
|
||||
|
||||
const { paste } = useContext(FlowsContext);
|
||||
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>();
|
||||
|
||||
useEffect(() => {
|
||||
setFlowComponent(createFlowComponent(cloneDeep(data), version));
|
||||
}, [data, data.node, showModalAdvanced]);
|
||||
|
||||
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;
|
||||
}
|
||||
if (event.includes("ungroup")) {
|
||||
updateFlowPosition(position, data.node?.flow!);
|
||||
expandGroupNode(data, reactFlowInstance);
|
||||
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 "Share":
|
||||
if (hasApiKey || hasStore) setShowconfirmShare(true);
|
||||
break;
|
||||
case "SaveAll":
|
||||
saveComponent(cloneDeep(data), false);
|
||||
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">
|
||||
|
|
@ -145,43 +182,28 @@ export default function NodeToolbarComponent({
|
|||
</a>
|
||||
</ShadTooltip>
|
||||
|
||||
{isMinimal || isGroup ? (
|
||||
<Select onValueChange={handleSelectChange} value={selectedValue}>
|
||||
<ShadTooltip content="More" side="top">
|
||||
<SelectTrigger>
|
||||
<div id="advancedIcon">
|
||||
<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
|
||||
id="editAdvancedBtn"
|
||||
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"
|
||||
|
|
@ -189,62 +211,115 @@ export default function NodeToolbarComponent({
|
|||
Edit{" "}
|
||||
</div>{" "}
|
||||
</SelectItem>
|
||||
{isMinimal && (
|
||||
<SelectItem value={getRandomKeyByssmm() + "show"}>
|
||||
<div className="flex" id="editAdvanced">
|
||||
<IconComponent
|
||||
name={showNode ? "Minimize2" : "Maximize2"}
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
/>
|
||||
{showNode ? "Minimize" : "Expand"}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
{isGroup && (
|
||||
<SelectItem value={getRandomKeyByssmm() + "ungroup"}>
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name="Ungroup"
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
/>{" "}
|
||||
Ungroup{" "}
|
||||
</div>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<ShadTooltip content="Edit" side="top">
|
||||
<div id="editAdvancedIcon">
|
||||
<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);
|
||||
}}
|
||||
>
|
||||
{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
|
||||
disabled={!hasApiKey || !validApiKey}
|
||||
value={"Share"}
|
||||
>
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name="Share2"
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
/>{" "}
|
||||
Share{" "}
|
||||
</div>{" "}
|
||||
</SelectItem>
|
||||
)}
|
||||
<SelectItem value={"Download"}>
|
||||
<div className="flex">
|
||||
<IconComponent
|
||||
name="Download"
|
||||
className="relative top-0.5 mr-2 h-4 w-4"
|
||||
/>{" "}
|
||||
Download{" "}
|
||||
</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>
|
||||
<></>
|
||||
</EditNodeModal>
|
||||
)}
|
||||
</ConfirmationModal.Trigger>
|
||||
</ConfirmationModal>
|
||||
<EditNodeModal
|
||||
data={data}
|
||||
nodeLength={nodeLength}
|
||||
open={showModalAdvanced}
|
||||
setOpen={setShowModalAdvanced}
|
||||
/>
|
||||
<ShareModal
|
||||
open={showconfirmShare}
|
||||
setOpen={setShowconfirmShare}
|
||||
is_component={true}
|
||||
component={flowComponent!}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { getVersion } from "../../controllers/API";
|
||||
import Page from "./components/PageComponent";
|
||||
|
||||
export default function FlowPage(): JSX.Element {
|
||||
const { flows, tabId, setTabId } = useContext(FlowsContext);
|
||||
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 />
|
||||
|
|
|
|||
202
src/frontend/src/pages/MainPage/components/components/index.tsx
Normal file
202
src/frontend/src/pages/MainPage/components/components/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 { FlowsContext } from "../../contexts/flowsContext";
|
||||
export default function HomePage(): JSX.Element {
|
||||
const {
|
||||
flows,
|
||||
setTabId,
|
||||
downloadFlows,
|
||||
uploadFlows,
|
||||
addFlow,
|
||||
removeFlow,
|
||||
uploadFlow,
|
||||
isLoading,
|
||||
} = useContext(FlowsContext);
|
||||
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(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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
412
src/frontend/src/pages/StorePage/index.tsx
Normal file
412
src/frontend/src/pages/StorePage/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { useContext, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FlowsContext } from "../../contexts/flowsContext";
|
||||
import { getVersion } from "../../controllers/API";
|
||||
import Page from "../FlowPage/components/PageComponent";
|
||||
|
||||
export default function ViewPage() {
|
||||
|
|
@ -13,14 +12,6 @@ export default function ViewPage() {
|
|||
setTabId(id!);
|
||||
}, [id]);
|
||||
|
||||
// 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 &&
|
||||
|
|
|
|||
|
|
@ -1,21 +1,31 @@
|
|||
import { Route, Routes } from "react-router-dom";
|
||||
import { useEffect } from "react";
|
||||
import { Route, Routes, useNavigate } from "react-router-dom";
|
||||
import { ProtectedAdminRoute } from "./components/authAdminGuard";
|
||||
import { ProtectedRoute } from "./components/authGuard";
|
||||
import { ProtectedLoginRoute } from "./components/authLoginGuard";
|
||||
import { CatchAllRoute } from "./components/catchAllRoutes";
|
||||
import { StoreGuard } from "./components/storeGuard";
|
||||
import AdminPage from "./pages/AdminPage";
|
||||
import LoginAdminPage from "./pages/AdminPage/LoginPage";
|
||||
import ApiKeysPage from "./pages/ApiKeysPage";
|
||||
import CommunityPage from "./pages/CommunityPage";
|
||||
import FlowPage from "./pages/FlowPage";
|
||||
import HomePage from "./pages/MainPage";
|
||||
import ComponentsComponent from "./pages/MainPage/components/components";
|
||||
import ProfileSettingsPage from "./pages/ProfileSettingsPage";
|
||||
import StorePage from "./pages/StorePage";
|
||||
import ViewPage from "./pages/ViewPage";
|
||||
import DeleteAccountPage from "./pages/deleteAccountPage";
|
||||
import LoginPage from "./pages/loginPage";
|
||||
import SignUp from "./pages/signUpPage";
|
||||
|
||||
const Router = () => {
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
// Redirect from root to /flows
|
||||
if (window.location.pathname === "/") {
|
||||
navigate("/flows");
|
||||
}
|
||||
}, [navigate]);
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
|
|
@ -25,15 +35,37 @@ const Router = () => {
|
|||
<HomePage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
>
|
||||
<Route
|
||||
path="flows"
|
||||
element={<ComponentsComponent key="flows" is_component={false} />}
|
||||
/>
|
||||
<Route
|
||||
path="components"
|
||||
element={<ComponentsComponent key="components" />}
|
||||
/>
|
||||
</Route>
|
||||
<Route
|
||||
path="/community"
|
||||
path="/store"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<CommunityPage />
|
||||
<StoreGuard>
|
||||
<StorePage />
|
||||
</StoreGuard>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/store/:id/"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<StoreGuard>
|
||||
<StorePage />
|
||||
</StoreGuard>
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route path="/flow/:id/">
|
||||
<Route
|
||||
path=""
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
@apply fixed right-4;
|
||||
}
|
||||
.side-bar-arrangement {
|
||||
@apply flex h-full w-52 flex-col overflow-hidden border-r scrollbar-hide;
|
||||
@apply flex h-full w-[14.5rem] flex-col overflow-hidden border-r scrollbar-hide;
|
||||
}
|
||||
.side-bar-search-div-placement {
|
||||
@apply relative mx-auto mb-2 mt-2 flex items-center;
|
||||
|
|
@ -114,7 +114,7 @@
|
|||
@apply pointer-events-none;
|
||||
}
|
||||
.extra-side-bar-buttons {
|
||||
@apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground shadow-sm ring-1 ring-inset ring-input transition-all duration-500 ease-in-out;
|
||||
@apply relative inline-flex w-full items-center justify-center rounded-md bg-background px-2 py-2 text-foreground shadow-sm ring-1 ring-inset ring-input transition-all duration-500 ease-in-out;
|
||||
}
|
||||
.extra-side-bar-buttons:hover {
|
||||
@apply hover:bg-muted;
|
||||
|
|
@ -123,7 +123,7 @@
|
|||
@apply flex gap-2;
|
||||
}
|
||||
.primary-input {
|
||||
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 sm:text-sm;
|
||||
@apply form-input block w-full truncate rounded-md border-border bg-background px-3 text-left text-sm shadow-sm placeholder:text-muted-foreground focus:border-ring focus:placeholder-transparent focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
|
|
@ -205,19 +205,19 @@
|
|||
}
|
||||
|
||||
.loading-page-panel {
|
||||
@apply flex h-full w-full items-center justify-center bg-muted;
|
||||
@apply flex h-full w-full items-center justify-center bg-background;
|
||||
}
|
||||
|
||||
.main-page-panel {
|
||||
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
|
||||
@apply flex-max-width h-full flex-col overflow-auto bg-background px-16;
|
||||
}
|
||||
|
||||
.admin-page-panel {
|
||||
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
|
||||
@apply flex-max-width h-full flex-col overflow-auto bg-background px-16;
|
||||
}
|
||||
|
||||
.main-page-nav-arrangement {
|
||||
@apply flex-max-width justify-between px-6 py-12 pb-2;
|
||||
@apply flex-max-width justify-between py-8 pb-2;
|
||||
}
|
||||
|
||||
.main-page-nav-title {
|
||||
|
|
@ -229,23 +229,23 @@
|
|||
}
|
||||
|
||||
.main-page-description-text {
|
||||
@apply flex w-[60%] px-6 pb-14 text-muted-foreground;
|
||||
@apply flex w-[60%] pb-4 text-muted-foreground;
|
||||
}
|
||||
|
||||
.admin-page-description-text {
|
||||
@apply flex w-[80%] px-6 pb-8 text-muted-foreground;
|
||||
@apply flex w-[80%] pb-8 text-muted-foreground;
|
||||
}
|
||||
|
||||
.main-page-flows-display {
|
||||
@apply grid w-full gap-4 p-4 md:grid-cols-2 lg:grid-cols-4;
|
||||
@apply grid w-full gap-4 pb-7 md:grid-cols-2 lg:grid-cols-3;
|
||||
}
|
||||
|
||||
.community-page-arrangement {
|
||||
@apply flex-max-width h-full flex-col overflow-auto bg-muted px-16;
|
||||
@apply flex-max-width h-full flex-col justify-between overflow-auto bg-background px-16;
|
||||
}
|
||||
|
||||
.community-page-nav-arrangement {
|
||||
@apply flex-max-width justify-between px-6 py-12 pb-2;
|
||||
@apply flex-max-width justify-between py-8 pb-2;
|
||||
}
|
||||
|
||||
.community-page-nav-title {
|
||||
|
|
@ -257,7 +257,7 @@
|
|||
}
|
||||
|
||||
.community-page-description-text {
|
||||
@apply flex w-[70%] px-6 pb-8 text-muted-foreground;
|
||||
@apply flex w-[70%] pb-8 text-muted-foreground;
|
||||
}
|
||||
|
||||
.community-pages-flows-panel {
|
||||
|
|
@ -311,10 +311,10 @@
|
|||
@apply hover:text-accent-foreground hover:transition-all;
|
||||
}
|
||||
.generic-node-desc {
|
||||
@apply h-full w-full text-foreground;
|
||||
@apply h-full w-full text-foreground;
|
||||
}
|
||||
.generic-node-desc-text {
|
||||
@apply w-full px-5 pb-3 text-sm text-muted-foreground;
|
||||
@apply w-full px-5 mb-4 text-sm text-muted-foreground;
|
||||
}
|
||||
|
||||
.alert-icon {
|
||||
|
|
@ -353,34 +353,6 @@
|
|||
@apply alert-font-size text-success-foreground word-break-break-word;
|
||||
}
|
||||
|
||||
.card-component-title-display {
|
||||
@apply round-button-div flex-max-width;
|
||||
}
|
||||
.card-component-image {
|
||||
@apply flex h-7 w-7 items-center justify-center rounded-full text-2xl;
|
||||
}
|
||||
.card-component-title-size {
|
||||
@apply w-full flex-1 truncate-doubleline word-break-break-word;
|
||||
}
|
||||
.card-component-delete-button {
|
||||
@apply flex self-start;
|
||||
}
|
||||
.card-component-delete-icon {
|
||||
@apply h-4 w-4 text-primary opacity-0 transition-all group-hover:opacity-100;
|
||||
}
|
||||
.card-component-desc {
|
||||
@apply pb-2 pt-2;
|
||||
}
|
||||
.card-component-desc-text {
|
||||
@apply truncate-doubleline;
|
||||
}
|
||||
.card-component-footer-arrangement {
|
||||
@apply flex-max-width items-end justify-between gap-2;
|
||||
}
|
||||
.card-component-footer {
|
||||
@apply flex flex-wrap gap-2;
|
||||
}
|
||||
|
||||
.unused-side-bar-aside {
|
||||
@apply flex flex-shrink-0 flex-col overflow-hidden border-r transition-all duration-500;
|
||||
}
|
||||
|
|
@ -495,7 +467,7 @@
|
|||
@apply flex items-center gap-0.5 rounded-md px-1.5 py-1 text-sm font-medium;
|
||||
}
|
||||
.header-menu-bar-display {
|
||||
@apply flex max-w-[200px] cursor-pointer items-center gap-2;
|
||||
@apply flex max-w-[120px] cursor-pointer items-center gap-2 lg:max-w-[200px];
|
||||
}
|
||||
.header-menu-flow-name {
|
||||
@apply flex-1 truncate;
|
||||
|
|
@ -505,13 +477,13 @@
|
|||
}
|
||||
|
||||
.header-arrangement {
|
||||
@apply flex-max-width h-12 items-center justify-between border-border bg-background;
|
||||
@apply flex-max-width h-12 items-center justify-between border-b border-border bg-muted;
|
||||
}
|
||||
.header-start-display {
|
||||
@apply flex w-[30%] items-center justify-start gap-2;
|
||||
@apply flex items-center justify-start gap-2;
|
||||
}
|
||||
.header-end-division {
|
||||
@apply flex w-[30%] justify-end px-2;
|
||||
@apply flex justify-end px-2;
|
||||
}
|
||||
.header-end-display {
|
||||
@apply ml-auto mr-2 flex items-center gap-5;
|
||||
|
|
@ -535,7 +507,7 @@
|
|||
@apply hover:bg-accent hover:text-accent-foreground;
|
||||
}
|
||||
.header-github-display {
|
||||
@apply -mr-px ml-2 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm;
|
||||
@apply -mr-px ml-1 flex h-9 items-center justify-center rounded-md rounded-l-none border bg-background px-2 text-sm;
|
||||
}
|
||||
.header-notifications-box {
|
||||
@apply fixed left-0 top-0 h-screen w-screen;
|
||||
|
|
@ -1061,4 +1033,55 @@
|
|||
.input-invalid {
|
||||
@apply border-destructive focus:border-destructive focus:ring-destructive;
|
||||
}
|
||||
|
||||
.fade-container {
|
||||
@apply relative overflow-hidden;
|
||||
}
|
||||
|
||||
.fade-container::before,
|
||||
.fade-container::after {
|
||||
@apply pointer-events-none absolute bottom-0 top-0 bg-gradient-to-r to-transparent from-background;
|
||||
content: "";
|
||||
width: 50px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-container::after {
|
||||
@apply right-0;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.fade-container.fade-left::before,
|
||||
.fade-container.fade-right::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-container-dark {
|
||||
@apply relative overflow-hidden;
|
||||
}
|
||||
|
||||
.fade-container-dark::before,
|
||||
.fade-container-dark::after {
|
||||
@apply pointer-events-none absolute bottom-0 top-0;
|
||||
content: "";
|
||||
width: 50px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
background: linear-gradient(to right, black, transparent);
|
||||
}
|
||||
|
||||
.fade-container-dark:after {
|
||||
@apply right-0;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.fade-container-dark.fade-left::before,
|
||||
.fade-container-dark.fade-right::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
@apply flex overflow-x-scroll scrollbar-hide;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,9 @@
|
|||
--chat-bot-icon: #afe6ef;
|
||||
--chat-user-icon: #aface9;
|
||||
|
||||
--component-icon: #d8598a;
|
||||
--flow-icon: #2f67d0;
|
||||
|
||||
/* Colors that are shared in dark and light mode */
|
||||
--blur-shared: #151923de;
|
||||
--build-trigger: #dc735b;
|
||||
|
|
@ -107,6 +110,9 @@
|
|||
--medium-indigo: #6366f1;
|
||||
--low-indigo: #e0e7ff;
|
||||
|
||||
--component-icon: #c35f85;
|
||||
--flow-icon: #2467e4;
|
||||
|
||||
/* Colors that are shared in dark and light mode */
|
||||
--blur-shared: #151923d2;
|
||||
--build-trigger: #dc735b;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export type APIClassType = {
|
|||
beta?: boolean;
|
||||
documentation: string;
|
||||
error?: string;
|
||||
official?: boolean;
|
||||
flow?: FlowType;
|
||||
[key: string]:
|
||||
| Array<string>
|
||||
|
|
@ -120,3 +121,10 @@ export type Users = {
|
|||
create_at: Date;
|
||||
updated_at: Date;
|
||||
};
|
||||
|
||||
export type Component = {
|
||||
name: string;
|
||||
description: string;
|
||||
data: Object;
|
||||
tags: [string];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -36,10 +36,10 @@ export type DropDownComponentType = {
|
|||
editNode?: boolean;
|
||||
apiModal?: boolean;
|
||||
numberOfOptions?: number;
|
||||
id?: string;
|
||||
};
|
||||
export type ParameterComponentType = {
|
||||
data: NodeDataType;
|
||||
setData: (value: NodeDataType) => void;
|
||||
title: string;
|
||||
id: sourceHandleType | targetHandleType;
|
||||
color: string;
|
||||
|
|
@ -54,6 +54,7 @@ export type ParameterComponentType = {
|
|||
proxy?: { field: string; id: string };
|
||||
showNode?: boolean;
|
||||
index?: string;
|
||||
onCloseModal?: (close: boolean) => void;
|
||||
};
|
||||
export type InputListComponentType = {
|
||||
value: string[];
|
||||
|
|
@ -68,8 +69,7 @@ export type KeyPairListComponentType = {
|
|||
disabled: boolean;
|
||||
editNode?: boolean;
|
||||
duplicateKey?: boolean;
|
||||
advanced?: boolean | null;
|
||||
dataValue?: any;
|
||||
editNodeModal?: boolean;
|
||||
};
|
||||
|
||||
export type DictComponentType = {
|
||||
|
|
@ -77,6 +77,7 @@ export type DictComponentType = {
|
|||
onChange: (value) => void;
|
||||
disabled: boolean;
|
||||
editNode?: boolean;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type TextAreaComponentType = {
|
||||
|
|
@ -94,7 +95,7 @@ export type TextAreaComponentType = {
|
|||
export type PromptAreaComponentType = {
|
||||
field_name?: string;
|
||||
nodeClass?: APIClassType;
|
||||
setNodeClass?: (value: APIClassType) => void;
|
||||
setNodeClass?: (value: APIClassType, code?: string) => void;
|
||||
disabled: boolean;
|
||||
onChange: (value: string[] | string) => void;
|
||||
value: string;
|
||||
|
|
@ -109,7 +110,7 @@ export type CodeAreaComponentType = {
|
|||
value: string;
|
||||
editNode?: boolean;
|
||||
nodeClass?: APIClassType;
|
||||
setNodeClass?: (value: APIClassType) => void;
|
||||
setNodeClass?: (value: APIClassType, code?: string) => void;
|
||||
dynamic?: boolean;
|
||||
id?: string;
|
||||
readonly?: boolean;
|
||||
|
|
@ -119,7 +120,6 @@ export type FileComponentType = {
|
|||
disabled: boolean;
|
||||
onChange: (value: string[] | string) => void;
|
||||
value: string;
|
||||
suffixes: Array<string>;
|
||||
fileTypes: Array<string>;
|
||||
onFileChange: (value: string) => void;
|
||||
editNode?: boolean;
|
||||
|
|
@ -216,18 +216,17 @@ export type IconComponentProps = {
|
|||
className?: string;
|
||||
iconColor?: string;
|
||||
onClick?: () => void;
|
||||
stroke?: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export type InputProps = {
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
maxLength?: number;
|
||||
flows: Array<{ id: string; name: string; description: string }>;
|
||||
tabId: string;
|
||||
invalidName?: boolean;
|
||||
setName: (name: string) => void;
|
||||
setDescription: (description: string) => void;
|
||||
setInvalidName?: (invalidName: boolean) => void;
|
||||
invalidNameList?: string[];
|
||||
};
|
||||
|
||||
export type TooltipProps = {
|
||||
|
|
@ -246,8 +245,15 @@ export type LoadingComponentProps = {
|
|||
remSize: number;
|
||||
};
|
||||
|
||||
export type ContentProps = { children: ReactNode };
|
||||
export type ContentProps = {
|
||||
children: ReactNode;
|
||||
tolltipContent?: ReactNode;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
};
|
||||
export type HeaderProps = { children: ReactNode; description: string };
|
||||
export type TriggerProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export interface languageMap {
|
||||
[key: string]: string | undefined;
|
||||
|
|
@ -261,7 +267,7 @@ export type signUpInputStateType = {
|
|||
|
||||
export type inputHandlerEventType = {
|
||||
target: {
|
||||
value: string | boolean;
|
||||
value: string;
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
|
|
@ -271,21 +277,37 @@ export type PaginatorComponentType = {
|
|||
rowsCount?: number[];
|
||||
totalRowsCount: number;
|
||||
paginate: (pageIndex: number, pageSize: number) => void;
|
||||
storeComponent?: boolean;
|
||||
};
|
||||
|
||||
export type ConfirmationModalType = {
|
||||
onCancel?: () => void;
|
||||
title: string;
|
||||
titleHeader: string;
|
||||
titleHeader?: string;
|
||||
asChild?: boolean;
|
||||
modalContent: string;
|
||||
modalContentTitle: string;
|
||||
destructive?: boolean;
|
||||
modalContentTitle?: string;
|
||||
cancelText: string;
|
||||
confirmationText: string;
|
||||
children: ReactElement;
|
||||
children: [
|
||||
React.ReactElement<ContentProps>,
|
||||
React.ReactElement<TriggerProps>
|
||||
];
|
||||
icon: string;
|
||||
data: any;
|
||||
index: number;
|
||||
data?: any;
|
||||
index?: number;
|
||||
onConfirm: (index, data) => void;
|
||||
open?: boolean;
|
||||
onClose?: (close: boolean) => void;
|
||||
size?:
|
||||
| "x-small"
|
||||
| "smaller"
|
||||
| "small"
|
||||
| "medium"
|
||||
| "large"
|
||||
| "large-h-full"
|
||||
| "small-h-full"
|
||||
| "medium-h-full";
|
||||
};
|
||||
|
||||
export type UserManagementType = {
|
||||
|
|
@ -332,14 +354,20 @@ export type ApiKeyType = {
|
|||
onCloseModal: () => void;
|
||||
};
|
||||
|
||||
export type ApiKeyInputType = {
|
||||
apikeyname: string;
|
||||
export type StoreApiKeyType = {
|
||||
children: ReactElement;
|
||||
disabled?: boolean;
|
||||
};
|
||||
export type groupedObjType = {
|
||||
family: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type nodeGroupedObjType = {
|
||||
displayName: string;
|
||||
node: string[] | string;
|
||||
};
|
||||
|
||||
type test = {
|
||||
[char: string]: string;
|
||||
};
|
||||
|
|
@ -439,7 +467,6 @@ export type fileCardPropsType = {
|
|||
export type nodeToolbarPropsType = {
|
||||
data: NodeDataType;
|
||||
deleteNode: (idx: string) => void;
|
||||
setData: (newState: NodeDataType) => void;
|
||||
position: XYPosition;
|
||||
setShowNode: (boolean: any) => void;
|
||||
numberOfHandles: boolean[] | [];
|
||||
|
|
@ -473,7 +500,7 @@ export type codeAreaModalPropsType = {
|
|||
setValue: (value: string) => void;
|
||||
value: string;
|
||||
nodeClass: APIClassType | undefined;
|
||||
setNodeClass: (Class: APIClassType) => void | undefined;
|
||||
setNodeClass: (Class: APIClassType, code?: string) => void | undefined;
|
||||
children: ReactNode;
|
||||
dynamic?: boolean;
|
||||
readonly?: boolean;
|
||||
|
|
@ -499,7 +526,7 @@ export type genericModalPropsType = {
|
|||
modalTitle: string;
|
||||
type: number;
|
||||
nodeClass?: APIClassType;
|
||||
setNodeClass?: (Class: APIClassType) => void;
|
||||
setNodeClass?: (Class: APIClassType, code?: string) => void;
|
||||
children: ReactNode;
|
||||
id?: string;
|
||||
readonly?: boolean;
|
||||
|
|
@ -526,8 +553,7 @@ export type groupDataType = {
|
|||
};
|
||||
|
||||
export type cardComponentPropsType = {
|
||||
flow: FlowType;
|
||||
id: string;
|
||||
data: FlowType;
|
||||
onDelete?: () => void;
|
||||
button?: JSX.Element;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,4 +14,7 @@ export type AuthContextType = {
|
|||
authenticationErrorCount: number;
|
||||
autoLogin: boolean;
|
||||
setAutoLogin: (autoLogin: boolean) => void;
|
||||
apiKey: string | null;
|
||||
setApiKey: (apiKey: string | null) => void;
|
||||
storeApiKey: (apiKey: string) => void;
|
||||
};
|
||||
|
|
|
|||
9
src/frontend/src/types/contexts/store.ts
Normal file
9
src/frontend/src/types/contexts/store.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export type storeContextType = {
|
||||
setHasStore: (store: boolean) => void;
|
||||
hasStore: boolean;
|
||||
setHasApiKey: (key: boolean) => void;
|
||||
hasApiKey: boolean;
|
||||
setValidApiKey: (key: boolean) => void;
|
||||
validApiKey: boolean;
|
||||
loadingApiKey: boolean;
|
||||
};
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
import { FlowType } from "../flow";
|
||||
|
||||
export type sidebarNavigationItemType = {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
|
||||
current: boolean;
|
||||
};
|
||||
|
||||
export type localStorageUserType = {
|
||||
components: { [key: string]: FlowType };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ export type FlowType = {
|
|||
data: ReactFlowJsonObject | null;
|
||||
description: string;
|
||||
style?: FlowStyleType;
|
||||
is_component: boolean;
|
||||
parent?: string;
|
||||
date_created?: string;
|
||||
updated_at?: string;
|
||||
last_tested_version?: string;
|
||||
};
|
||||
export type NodeType = {
|
||||
id: string;
|
||||
|
|
@ -17,6 +22,7 @@ export type NodeType = {
|
|||
};
|
||||
|
||||
export type NodeDataType = {
|
||||
showNode?: boolean;
|
||||
type: string;
|
||||
node?: APIClassType;
|
||||
id: string;
|
||||
|
|
|
|||
20
src/frontend/src/types/store/index.ts
Normal file
20
src/frontend/src/types/store/index.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
export type storeComponent = {
|
||||
id: string;
|
||||
is_component: boolean;
|
||||
tags?: { id: string; name: string }[];
|
||||
metadata?: any;
|
||||
downloads_count?: number;
|
||||
name: string;
|
||||
description: string;
|
||||
liked_by_count?: number;
|
||||
liked_by_user?: boolean;
|
||||
user_created?: { username: string };
|
||||
last_tested_version?: string;
|
||||
private?: boolean;
|
||||
};
|
||||
|
||||
export type StoreComponentResponse = {
|
||||
count: number;
|
||||
authorized: boolean;
|
||||
results: storeComponent[];
|
||||
};
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { XYPosition } from "reactflow";
|
||||
import { tweakType } from "../components";
|
||||
import { FlowType } from "../flow";
|
||||
import { FlowType, NodeDataType } from "../flow";
|
||||
|
||||
export type FlowsContextType = {
|
||||
saveFlow: (flow: FlowType, silent?: boolean) => Promise<void>;
|
||||
|
|
@ -10,7 +11,9 @@ export type FlowsContextType = {
|
|||
removeFlow: (id: string) => void;
|
||||
addFlow: (
|
||||
newProject: boolean,
|
||||
flow?: FlowType
|
||||
flow?: FlowType,
|
||||
override?: boolean,
|
||||
position?: XYPosition
|
||||
) => Promise<String | undefined>;
|
||||
updateFlow: (newFlow: FlowType) => void;
|
||||
incrementNodeId: () => string;
|
||||
|
|
@ -23,7 +26,18 @@ export type FlowsContextType = {
|
|||
uploadFlows: () => void;
|
||||
isBuilt: boolean;
|
||||
setIsBuilt: (state: boolean) => void;
|
||||
uploadFlow: (newFlow: boolean, file?: File) => Promise<String | undefined>;
|
||||
saveCurrentFlow: () => void;
|
||||
uploadFlow: ({
|
||||
newProject,
|
||||
file,
|
||||
isComponent,
|
||||
position,
|
||||
}: {
|
||||
newProject: boolean;
|
||||
file?: File;
|
||||
isComponent?: boolean;
|
||||
position?: XYPosition;
|
||||
}) => Promise<String | never>;
|
||||
hardReset: () => void;
|
||||
getNodeId: (nodeType: string) => string;
|
||||
tabsState: FlowsState;
|
||||
|
|
@ -36,6 +50,14 @@ export type FlowsContextType = {
|
|||
setLastCopiedSelection: (selection: { nodes: any; edges: any }) => void;
|
||||
setTweak: (tweak: tweakType) => tweakType | void;
|
||||
getTweak: tweakType;
|
||||
saveComponent: (
|
||||
component: NodeDataType,
|
||||
override: boolean
|
||||
) => Promise<String | undefined>;
|
||||
deleteComponent: (key: string) => void;
|
||||
version: string;
|
||||
nodesOnFlow: string;
|
||||
setNodesOnFlow: (nodes: string) => void;
|
||||
};
|
||||
|
||||
export type FlowsState = {
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ export type alertContextType = {
|
|||
removeFromNotificationList: (index: string) => void;
|
||||
loading: boolean;
|
||||
setLoading: (newState: boolean) => void;
|
||||
modalContextOpen: boolean | null;
|
||||
setModalContextOpen: (newState: boolean) => void;
|
||||
};
|
||||
|
||||
export type darkContextType = {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ import {
|
|||
ReactFlowJsonObject,
|
||||
XYPosition,
|
||||
} from "reactflow";
|
||||
import { specialCharsRegex } from "../constants/constants";
|
||||
import ShortUniqueId from "short-unique-id";
|
||||
import {
|
||||
LANGFLOW_SUPPORTED_TYPES,
|
||||
specialCharsRegex,
|
||||
} from "../constants/constants";
|
||||
import { APITemplateType, TemplateVariableType } from "../types/api";
|
||||
import {
|
||||
FlowType,
|
||||
|
|
@ -96,8 +100,7 @@ export function isValidConnection(
|
|||
(t) =>
|
||||
targetHandleObject.inputTypes?.some((n) => n === t) ||
|
||||
t === targetHandleObject.type
|
||||
) ||
|
||||
targetHandleObject.type === "str"
|
||||
)
|
||||
) {
|
||||
let targetNode = reactFlowInstance?.getNode(target!)?.data?.node;
|
||||
if (!targetNode) {
|
||||
|
|
@ -162,46 +165,51 @@ export function updateIds(
|
|||
) {
|
||||
let idsMap = {};
|
||||
|
||||
newFlow.nodes.forEach((node: NodeType) => {
|
||||
// Generate a unique node ID
|
||||
let newId = getNodeId(node.data.node?.flow ? "GroupNode" : node.data.type);
|
||||
idsMap[node.id] = newId;
|
||||
node.id = newId;
|
||||
node.data.id = newId;
|
||||
// Add the new node to the list of nodes in state
|
||||
});
|
||||
if (newFlow.nodes)
|
||||
newFlow.nodes.forEach((node: NodeType) => {
|
||||
// Generate a unique node ID
|
||||
let newId = getNodeId(
|
||||
node.data.node?.flow ? "GroupNode" : node.data.type
|
||||
);
|
||||
idsMap[node.id] = newId;
|
||||
node.id = newId;
|
||||
node.data.id = newId;
|
||||
// Add the new node to the list of nodes in state
|
||||
});
|
||||
|
||||
newFlow.edges.forEach((edge: Edge) => {
|
||||
edge.source = idsMap[edge.source];
|
||||
edge.target = idsMap[edge.target];
|
||||
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
edge.sourceHandle = scapedJSONStringfy({
|
||||
...sourceHandleObject,
|
||||
id: edge.source,
|
||||
if (newFlow.edges)
|
||||
newFlow.edges.forEach((edge: Edge) => {
|
||||
edge.source = idsMap[edge.source];
|
||||
edge.target = idsMap[edge.target];
|
||||
const sourceHandleObject: sourceHandleType = scapeJSONParse(
|
||||
edge.sourceHandle!
|
||||
);
|
||||
edge.sourceHandle = scapedJSONStringfy({
|
||||
...sourceHandleObject,
|
||||
id: edge.source,
|
||||
});
|
||||
if (edge.data?.sourceHandle?.id) {
|
||||
edge.data.sourceHandle.id = edge.source;
|
||||
}
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||
edge.targetHandle!
|
||||
);
|
||||
edge.targetHandle = scapedJSONStringfy({
|
||||
...targetHandleObject,
|
||||
id: edge.target,
|
||||
});
|
||||
if (edge.data?.targetHandle?.id) {
|
||||
edge.data.targetHandle.id = edge.target;
|
||||
}
|
||||
edge.id =
|
||||
"reactflow__edge-" +
|
||||
edge.source +
|
||||
edge.sourceHandle +
|
||||
"-" +
|
||||
edge.target +
|
||||
edge.targetHandle;
|
||||
});
|
||||
if (edge.data?.sourceHandle?.id) {
|
||||
edge.data.sourceHandle.id = edge.source;
|
||||
}
|
||||
const targetHandleObject: targetHandleType = scapeJSONParse(
|
||||
edge.targetHandle!
|
||||
);
|
||||
edge.targetHandle = scapedJSONStringfy({
|
||||
...targetHandleObject,
|
||||
id: edge.target,
|
||||
});
|
||||
if (edge.data?.targetHandle?.id) {
|
||||
edge.data.targetHandle.id = edge.target;
|
||||
}
|
||||
edge.id =
|
||||
"reactflow__edge-" +
|
||||
edge.source +
|
||||
edge.sourceHandle +
|
||||
"-" +
|
||||
edge.target +
|
||||
edge.targetHandle;
|
||||
});
|
||||
return idsMap;
|
||||
}
|
||||
|
||||
export function buildTweaks(flow: FlowType) {
|
||||
|
|
@ -518,6 +526,7 @@ export function generateFlow(
|
|||
|
||||
const newFlow: FlowType = {
|
||||
data: newFlowData,
|
||||
is_component: false,
|
||||
name: name,
|
||||
description: "",
|
||||
//generating local id instead of using the id from the server, can change in the future
|
||||
|
|
@ -627,15 +636,7 @@ function updateGroupNodeTemplate(template: APITemplateType) {
|
|||
let type = template[key].type;
|
||||
let input_types = template[key].input_types;
|
||||
if (
|
||||
(type === "str" ||
|
||||
type === "bool" ||
|
||||
type === "float" ||
|
||||
type === "code" ||
|
||||
type === "prompt" ||
|
||||
type === "file" ||
|
||||
type === "int" ||
|
||||
type === "dict" ||
|
||||
type === "NestedDict") &&
|
||||
LANGFLOW_SUPPORTED_TYPES.has(type) &&
|
||||
!template[key].required &&
|
||||
!input_types
|
||||
) {
|
||||
|
|
@ -695,7 +696,6 @@ function isHandleConnected(
|
|||
/*
|
||||
this function receives a flow and a handleId and check if there is a connection with this handle
|
||||
*/
|
||||
scapedJSONStringfy({ type: field.type, fieldName: key, id: nodeId });
|
||||
if (field.proxy) {
|
||||
if (
|
||||
edges.some(
|
||||
|
|
@ -899,17 +899,43 @@ export function ungroupNode(
|
|||
BaseFlow.edges = edges;
|
||||
}
|
||||
|
||||
function updateProxyIdsOnTemplate(
|
||||
template: APITemplateType,
|
||||
idsMap: { [key: string]: string }
|
||||
) {
|
||||
Object.keys(template).forEach((key) => {
|
||||
if (template[key].proxy && idsMap[template[key].proxy!.id]) {
|
||||
template[key].proxy!.id = idsMap[template[key].proxy!.id];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateEdgesIds(edges: Edge[], idsMap: { [key: string]: string }) {
|
||||
edges.forEach((edge) => {
|
||||
let targetHandle: targetHandleType = edge.data.targetHandle;
|
||||
if (targetHandle.proxy && idsMap[targetHandle.proxy!.id]) {
|
||||
targetHandle.proxy!.id = idsMap[targetHandle.proxy!.id];
|
||||
}
|
||||
edge.data.targetHandle = targetHandle;
|
||||
edge.targetHandle = scapedJSONStringfy(targetHandle);
|
||||
});
|
||||
}
|
||||
|
||||
export function expandGroupNode(
|
||||
groupNode: NodeDataType,
|
||||
ReactFlowInstance: ReactFlowInstance
|
||||
ReactFlowInstance: ReactFlowInstance,
|
||||
getNodeId: (type: string) => string
|
||||
) {
|
||||
const { template, flow } = _.cloneDeep(groupNode.node!);
|
||||
const idsMap = updateIds(flow!.data!, getNodeId);
|
||||
updateProxyIdsOnTemplate(template, idsMap);
|
||||
let flowEdges = ReactFlowInstance.getEdges();
|
||||
updateEdgesIds(flowEdges, idsMap);
|
||||
const gNodes: NodeType[] = flow?.data?.nodes!;
|
||||
const gEdges = flow!.data!.edges;
|
||||
console.log(gEdges);
|
||||
//redirect edges to correct proxy node
|
||||
let updatedEdges: Edge[] = [];
|
||||
ReactFlowInstance.getEdges().forEach((edge) => {
|
||||
flowEdges.forEach((edge) => {
|
||||
let newEdge = _.cloneDeep(edge);
|
||||
if (newEdge.target === groupNode.id) {
|
||||
const targetHandle: targetHandleType = newEdge.data.targetHandle;
|
||||
|
|
@ -995,7 +1021,6 @@ export function expandGroupNode(
|
|||
...gEdges,
|
||||
...updatedEdges,
|
||||
];
|
||||
console.log(edges);
|
||||
ReactFlowInstance.setNodes(nodes);
|
||||
ReactFlowInstance.setEdges(edges);
|
||||
}
|
||||
|
|
@ -1018,7 +1043,6 @@ export function getGroupStatus(
|
|||
let status = { valid: true, params: "Built sucessfully ✨" };
|
||||
const { nodes } = flow.data!;
|
||||
const ids = nodes.map((n: NodeType) => n.data.id);
|
||||
ids.forEach((id) => console.log(ssData[id]));
|
||||
ids.forEach((id) => {
|
||||
if (!ssData[id]) {
|
||||
status = ssData[id];
|
||||
|
|
@ -1030,3 +1054,58 @@ export function getGroupStatus(
|
|||
});
|
||||
return status;
|
||||
}
|
||||
|
||||
export function createFlowComponent(
|
||||
nodeData: NodeDataType,
|
||||
version: string
|
||||
): FlowType {
|
||||
nodeData.node!.official = false;
|
||||
const flowNode: FlowType = {
|
||||
data: {
|
||||
edges: [],
|
||||
nodes: [
|
||||
{
|
||||
data: nodeData,
|
||||
id: nodeData.id,
|
||||
position: { x: 0, y: 0 },
|
||||
type: "genericNode",
|
||||
},
|
||||
],
|
||||
viewport: { x: 1, y: 1, zoom: 1 },
|
||||
},
|
||||
description: nodeData.node?.description || "",
|
||||
name: nodeData.node?.display_name || nodeData.type || "",
|
||||
id: nodeData.id || "",
|
||||
is_component: true,
|
||||
last_tested_version: version,
|
||||
};
|
||||
return flowNode;
|
||||
}
|
||||
|
||||
export function downloadNode(NodeFLow: FlowType) {
|
||||
const element = document.createElement("a");
|
||||
const file = new Blob([JSON.stringify(NodeFLow)], {
|
||||
type: "application/json",
|
||||
});
|
||||
element.href = URL.createObjectURL(file);
|
||||
element.download = `${NodeFLow.name}.json`;
|
||||
element.click();
|
||||
}
|
||||
|
||||
export function updateComponentNameAndType(
|
||||
data: any,
|
||||
component: NodeDataType
|
||||
) {}
|
||||
|
||||
export function removeFileNameFromComponents(flow: FlowType) {
|
||||
flow.data!.nodes.forEach((node: NodeType) => {
|
||||
Object.keys(node.data.node!.template).forEach((field) => {
|
||||
if (node.data.node?.template[field].type === "file") {
|
||||
node.data.node!.template[field].value = "";
|
||||
}
|
||||
});
|
||||
if (node.data.node?.flow) {
|
||||
removeFileNameFromComponents(node.data.node.flow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
23
src/frontend/src/utils/storeUtils.ts
Normal file
23
src/frontend/src/utils/storeUtils.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { cloneDeep } from "lodash";
|
||||
import { FlowType } from "../types/flow";
|
||||
|
||||
export default function cloneFLowWithParent(
|
||||
flow: FlowType,
|
||||
parent: string,
|
||||
is_component: boolean
|
||||
) {
|
||||
let childFLow = cloneDeep(flow);
|
||||
childFLow.parent = parent;
|
||||
childFLow.id = "";
|
||||
childFLow.is_component = is_component;
|
||||
return childFLow;
|
||||
}
|
||||
|
||||
export function getTagsIds(
|
||||
tags: string[],
|
||||
tagListId: { name: string; id: string }[]
|
||||
) {
|
||||
return tags
|
||||
.map((tag) => tagListId.find((tagObj) => tagObj.name === tag))!
|
||||
.map((tag) => tag!.id);
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@ import {
|
|||
ArrowUpToLine,
|
||||
Bell,
|
||||
BookMarked,
|
||||
BookmarkPlus,
|
||||
Boxes,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
|
|
@ -14,6 +16,7 @@ import {
|
|||
Circle,
|
||||
Clipboard,
|
||||
Code2,
|
||||
Combine,
|
||||
Compass,
|
||||
Copy,
|
||||
Cpu,
|
||||
|
|
@ -31,11 +34,14 @@ import {
|
|||
FileText,
|
||||
FileUp,
|
||||
Fingerprint,
|
||||
FolderPlus,
|
||||
Gift,
|
||||
GitBranchPlus,
|
||||
GitFork,
|
||||
GithubIcon,
|
||||
Group,
|
||||
Hammer,
|
||||
Heart,
|
||||
HelpCircle,
|
||||
Home,
|
||||
Info,
|
||||
|
|
@ -44,7 +50,9 @@ import {
|
|||
Layers,
|
||||
Lightbulb,
|
||||
Link,
|
||||
Loader2,
|
||||
Lock,
|
||||
LogIn,
|
||||
LucideSend,
|
||||
Maximize2,
|
||||
Menu,
|
||||
|
|
@ -55,20 +63,27 @@ import {
|
|||
Minus,
|
||||
MoonIcon,
|
||||
MoreHorizontal,
|
||||
Network,
|
||||
Paperclip,
|
||||
Pencil,
|
||||
Plus,
|
||||
Redo,
|
||||
RefreshCcw,
|
||||
Rocket,
|
||||
Save,
|
||||
SaveAll,
|
||||
Scissors,
|
||||
Search,
|
||||
Settings2,
|
||||
Share2,
|
||||
Shield,
|
||||
Sparkles,
|
||||
Square,
|
||||
Store,
|
||||
SunIcon,
|
||||
TerminalIcon,
|
||||
TerminalSquare,
|
||||
ToyBrick,
|
||||
Trash2,
|
||||
Undo,
|
||||
Ungroup,
|
||||
|
|
@ -81,6 +96,7 @@ import {
|
|||
Users2,
|
||||
Variable,
|
||||
Wand2,
|
||||
Workflow,
|
||||
Wrench,
|
||||
X,
|
||||
XCircle,
|
||||
|
|
@ -97,7 +113,11 @@ import { EvernoteIcon } from "../icons/Evernote";
|
|||
import { FBIcon } from "../icons/FacebookMessenger";
|
||||
import { GitBookIcon } from "../icons/GitBook";
|
||||
import { GoogleIcon } from "../icons/Google";
|
||||
import { GradientSparkles } from "../icons/GradientSparkles";
|
||||
import {
|
||||
GradientInfinity,
|
||||
GradientSave,
|
||||
GradientUngroup,
|
||||
} from "../icons/GradientSparkles";
|
||||
import { HuggingFaceIcon } from "../icons/HuggingFace";
|
||||
import { IFixIcon } from "../icons/IFixIt";
|
||||
import { MetaIcon } from "../icons/Meta";
|
||||
|
|
@ -154,9 +174,12 @@ export const nodeColors: { [char: string]: string } = {
|
|||
prompts: "#4367BF",
|
||||
llms: "#6344BE",
|
||||
chains: "#FE7500",
|
||||
Document: "#7AAE42",
|
||||
list: "#9AAE42",
|
||||
agents: "#903BBE",
|
||||
tools: "#FF3434",
|
||||
memories: "#F5B85A",
|
||||
saved_components: "#a5B85A",
|
||||
advanced: "#000000",
|
||||
chat: "#198BF6",
|
||||
thought: "#272541",
|
||||
|
|
@ -181,6 +204,7 @@ export const nodeNames: { [char: string]: string } = {
|
|||
agents: "Agents",
|
||||
tools: "Tools",
|
||||
memories: "Memories",
|
||||
saved_components: "Saved",
|
||||
advanced: "Advanced",
|
||||
chat: "Chat",
|
||||
embeddings: "Embeddings",
|
||||
|
|
@ -233,6 +257,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
ChatVertexAI: VertexAIIcon,
|
||||
VertexAIEmbeddings: VertexAIIcon,
|
||||
agents: Rocket,
|
||||
Workflow,
|
||||
User,
|
||||
WikipediaAPIWrapper: SvgWikipedia,
|
||||
chains: Link,
|
||||
|
|
@ -243,6 +268,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
advanced: Laptop2,
|
||||
chat: MessageCircle,
|
||||
embeddings: Fingerprint,
|
||||
saved_components: GradientSave,
|
||||
documentloaders: Paperclip,
|
||||
vectorstores: Layers,
|
||||
toolkits: Hammer,
|
||||
|
|
@ -255,9 +281,12 @@ export const nodeIconsLucide: iconsType = {
|
|||
unknown: HelpCircle,
|
||||
WikipediaQueryRun: SvgWikipedia,
|
||||
WolframAlphaQueryRun: SvgWolfram,
|
||||
custom_components: GradientSparkles,
|
||||
custom_components: GradientInfinity,
|
||||
group_components: GradientUngroup,
|
||||
custom: Edit,
|
||||
Trash2,
|
||||
Boxes,
|
||||
Network,
|
||||
X,
|
||||
XCircle,
|
||||
Info,
|
||||
|
|
@ -285,6 +314,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
Clipboard,
|
||||
Code2,
|
||||
Variable,
|
||||
Store,
|
||||
Download,
|
||||
Eraser,
|
||||
Lock,
|
||||
|
|
@ -293,6 +323,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
DownloadCloud,
|
||||
File,
|
||||
FileText,
|
||||
FolderPlus,
|
||||
GitFork,
|
||||
GithubIcon,
|
||||
FileDown,
|
||||
|
|
@ -317,6 +348,7 @@ export const nodeIconsLucide: iconsType = {
|
|||
Key,
|
||||
Unplug,
|
||||
Group,
|
||||
LogIn,
|
||||
ChevronUp,
|
||||
Ungroup,
|
||||
BookMarked,
|
||||
|
|
@ -324,4 +356,15 @@ export const nodeIconsLucide: iconsType = {
|
|||
Square,
|
||||
Minimize2,
|
||||
Maximize2,
|
||||
SaveAll,
|
||||
Share2,
|
||||
GitBranchPlus,
|
||||
Loader2,
|
||||
BookmarkPlus,
|
||||
Heart,
|
||||
Link,
|
||||
ToyBrick,
|
||||
RefreshCcw,
|
||||
Combine,
|
||||
TerminalIcon,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import {
|
||||
IVarHighlightType,
|
||||
groupedObjType,
|
||||
nodeGroupedObjType,
|
||||
tweakType,
|
||||
} from "../types/components";
|
||||
import { FlowType, NodeType } from "../types/flow";
|
||||
|
|
@ -107,13 +108,15 @@ export function groupByFamily(
|
|||
const baseClassesSet = new Set(baseClasses.split("\n"));
|
||||
let arrOfPossibleInputs: Array<{
|
||||
category: string;
|
||||
nodes: string[];
|
||||
nodes: nodeGroupedObjType[];
|
||||
full: boolean;
|
||||
display_name?: string;
|
||||
}> = [];
|
||||
let arrOfPossibleOutputs: Array<{
|
||||
category: string;
|
||||
nodes: string[];
|
||||
nodes: nodeGroupedObjType[];
|
||||
full: boolean;
|
||||
display_name?: string;
|
||||
}> = [];
|
||||
let checkedNodes = new Map();
|
||||
const excludeTypes = new Set([
|
||||
|
|
@ -126,18 +129,23 @@ export function groupByFamily(
|
|||
"int",
|
||||
]);
|
||||
|
||||
const checkBaseClass = (template: TemplateVariableType) =>
|
||||
template.type &&
|
||||
template.show &&
|
||||
((!excludeTypes.has(template.type) && baseClassesSet.has(template.type)) ||
|
||||
(template.input_types &&
|
||||
template.input_types.some((inputType) =>
|
||||
baseClassesSet.has(inputType)
|
||||
)));
|
||||
const checkBaseClass = (template: TemplateVariableType) => {
|
||||
return (
|
||||
template.type &&
|
||||
template.show &&
|
||||
((!excludeTypes.has(template.type) &&
|
||||
baseClassesSet.has(template.type)) ||
|
||||
(template.input_types &&
|
||||
template.input_types.some((inputType) => {
|
||||
baseClassesSet.has(inputType);
|
||||
})))
|
||||
);
|
||||
};
|
||||
|
||||
if (flow) {
|
||||
for (const node of flow) {
|
||||
const nodeData = node.data;
|
||||
|
||||
const foundNode = checkedNodes.get(nodeData.type);
|
||||
checkedNodes.set(nodeData.type, {
|
||||
hasBaseClassInTemplate:
|
||||
|
|
@ -148,16 +156,18 @@ export function groupByFamily(
|
|||
nodeData.node!.base_classes.some((baseClass) =>
|
||||
baseClassesSet.has(baseClass)
|
||||
),
|
||||
displayName: nodeData.node?.display_name,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const [d, nodes] of Object.entries(data)) {
|
||||
let tempInputs: string[] = [],
|
||||
tempOutputs: string[] = [];
|
||||
let tempInputs: nodeGroupedObjType[] = [],
|
||||
tempOutputs: nodeGroupedObjType[] = [];
|
||||
|
||||
for (const [n, node] of Object.entries(nodes!)) {
|
||||
let foundNode = checkedNodes.get(n);
|
||||
|
||||
if (!foundNode) {
|
||||
foundNode = {
|
||||
hasBaseClassInTemplate: Object.values(node!.template).some(
|
||||
|
|
@ -166,15 +176,18 @@ export function groupByFamily(
|
|||
hasBaseClassInBaseClasses: node!.base_classes.some((baseClass) =>
|
||||
baseClassesSet.has(baseClass)
|
||||
),
|
||||
displayName: node?.display_name,
|
||||
};
|
||||
checkedNodes.set(n, foundNode);
|
||||
}
|
||||
|
||||
if (foundNode.hasBaseClassInTemplate) tempInputs.push(n);
|
||||
if (foundNode.hasBaseClassInBaseClasses) tempOutputs.push(n);
|
||||
if (foundNode.hasBaseClassInTemplate)
|
||||
tempInputs.push({ node: n, displayName: foundNode.displayName });
|
||||
if (foundNode.hasBaseClassInBaseClasses)
|
||||
tempOutputs.push({ node: n, displayName: foundNode.displayName });
|
||||
}
|
||||
|
||||
const totalNodes = Object.keys(nodes!).length;
|
||||
|
||||
if (tempInputs.length)
|
||||
arrOfPossibleInputs.push({
|
||||
category: d,
|
||||
|
|
@ -192,11 +205,15 @@ export function groupByFamily(
|
|||
return left
|
||||
? arrOfPossibleOutputs.map((output) => ({
|
||||
family: output.category,
|
||||
type: output.full ? "" : output.nodes.join(", "),
|
||||
type: output.full
|
||||
? ""
|
||||
: output.nodes.map((item) => item.node).join(", "),
|
||||
display_name: "",
|
||||
}))
|
||||
: arrOfPossibleInputs.map((input) => ({
|
||||
family: input.category,
|
||||
type: input.full ? "" : input.nodes.join(", "),
|
||||
type: input.full ? "" : input.nodes.map((item) => item.node).join(", "),
|
||||
display_name: input.nodes.map((item) => item.displayName).join(", "),
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -556,6 +573,77 @@ export function tabsArray(codes: string[], method: number) {
|
|||
];
|
||||
}
|
||||
|
||||
export function checkLocalStorageKey(key: string): boolean {
|
||||
return localStorage.getItem(key) !== null;
|
||||
}
|
||||
|
||||
export function IncrementObjectKey(
|
||||
object: object,
|
||||
key: string
|
||||
): { newKey: string; increment: number } {
|
||||
let count = 1;
|
||||
const type = removeCountFromString(key);
|
||||
let newKey = type + " " + `(${count})`;
|
||||
while (object[newKey]) {
|
||||
count++;
|
||||
newKey = type + " " + `(${count})`;
|
||||
}
|
||||
return { newKey, increment: count };
|
||||
}
|
||||
|
||||
export function removeCountFromString(input: string): string {
|
||||
// Define a regex pattern to match the count in parentheses
|
||||
const pattern = /\s*\(\w+\)\s*$/;
|
||||
|
||||
// Use the `replace` method to remove the matched pattern
|
||||
const result = input.replace(pattern, "");
|
||||
|
||||
return result.trim(); // Trim any leading/trailing spaces
|
||||
}
|
||||
|
||||
export function createRandomKey(key: string, uid: string): string {
|
||||
return removeCountFromString(key) + ` (${uid})`;
|
||||
}
|
||||
|
||||
export function sensitiveSort(a: string, b: string): number {
|
||||
// Extract the name and number from each string using regular expressions
|
||||
const regex = /(.+) \((\w+)\)/;
|
||||
const matchA = a.match(regex);
|
||||
const matchB = b.match(regex);
|
||||
|
||||
if (matchA && matchB) {
|
||||
// Compare the names alphabetically
|
||||
const nameA = matchA[1];
|
||||
const nameB = matchB[1];
|
||||
if (nameA !== nameB) {
|
||||
return nameA.localeCompare(nameB);
|
||||
}
|
||||
|
||||
// If the names are the same, compare the numbers numerically
|
||||
const numberA = parseInt(matchA[2]);
|
||||
const numberB = parseInt(matchB[2]);
|
||||
return numberA - numberB;
|
||||
} else {
|
||||
// Handle cases where one or both strings do not match the expected pattern
|
||||
// Simple strings are treated as pure alphabetical comparisons
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
}
|
||||
// this function is used to get the set of keys from an object
|
||||
export function getSetFromObject(obj: object, key?: string): Set<string> {
|
||||
const set = new Set<string>();
|
||||
if (key) {
|
||||
for (const objKey in obj) {
|
||||
set.add(obj[objKey][key]);
|
||||
}
|
||||
} else {
|
||||
for (const key in obj) {
|
||||
set.add(key);
|
||||
}
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
export function getFieldTitle(
|
||||
template: APITemplateType,
|
||||
templateField: string
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@ module.exports = {
|
|||
},
|
||||
extend: {
|
||||
colors: {
|
||||
"component-icon": "var(--component-icon)",
|
||||
"flow-icon": "var(--flow-icon)",
|
||||
"low-indigo": "var(--low-indigo)",
|
||||
"chat-send": "var(--chat-send)",
|
||||
connection: "var(--connection)",
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ test("CodeAreaModalComponent", async ({ page }) => {
|
|||
await page.waitForTimeout(2000);
|
||||
|
||||
await page
|
||||
.locator('//*[@id="sidePythonFunctionTool"]')
|
||||
.getByTestId("toolsPythonFunctionTool")
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
|
|
@ -31,11 +31,8 @@ test("CodeAreaModalComponent", async ({ page }) => {
|
|||
|
||||
await page.locator('//*[@id="checkAndSaveBtn"]').click();
|
||||
|
||||
await page
|
||||
.locator('//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div')
|
||||
.click();
|
||||
await page.locator('//*[@id="advancedIcon"]').click();
|
||||
await page.locator('//*[@id="editAdvancedBtn"]').click();
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByTestId("edit-button-modal").click();
|
||||
|
||||
await page.locator('//*[@id="showcode"]').click();
|
||||
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeFalsy();
|
||||
|
|
@ -111,11 +108,8 @@ test("CodeAreaModalComponent", async ({ page }) => {
|
|||
if (elementCount === 0) {
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
await page
|
||||
.locator('//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div')
|
||||
.click();
|
||||
await page.locator('//*[@id="advancedIcon"]').click();
|
||||
await page.locator('//*[@id="editAdvancedBtn"]').click();
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByTestId("edit-button-modal").click();
|
||||
|
||||
await page.locator('//*[@id="showcode"]').click();
|
||||
expect(await page.locator('//*[@id="showcode"]').isChecked()).toBeTruthy();
|
||||
|
|
|
|||
104
src/frontend/tests/end-to-end/dropdownComponent.spec.ts
Normal file
104
src/frontend/tests/end-to-end/dropdownComponent.spec.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("dropDownComponent", async ({ page }) => {
|
||||
await page.goto("http://localhost:3000/");
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.locator('//*[@id="new-project-btn"]').click();
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.getByPlaceholder("Search").click();
|
||||
await page.getByPlaceholder("Search").fill("amazon");
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page
|
||||
.getByTestId("llmsAmazon Bedrock")
|
||||
.first()
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
|
||||
await page.getByTestId("dropdown-2-display").click();
|
||||
await page.getByTestId("ai21.j2-grande-instruct-0-option").click();
|
||||
|
||||
let value = await page.getByTestId("dropdown-2-display").innerText();
|
||||
if (value !== "ai21.j2-grande-instruct") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page.getByTestId("dropdown-2-display").click();
|
||||
await page.getByTestId("ai21.j2-jumbo-instruct-1-option").click();
|
||||
|
||||
value = await page.getByTestId("dropdown-2-display").innerText();
|
||||
if (value !== "ai21.j2-jumbo-instruct") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByTestId("edit-button-modal").click();
|
||||
|
||||
value = await page.getByTestId("dropdown-edit-1-display").innerText();
|
||||
if (value !== "ai21.j2-jumbo-instruct") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
// showcredentials_profile_name
|
||||
await page.locator('//*[@id="showcredentials_profile_name"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showmodel_id
|
||||
await page.locator('//*[@id="showmodel_id"]').click();
|
||||
expect(await page.locator('//*[@id="showmodel_id"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showcredentials_profile_name
|
||||
await page.locator('//*[@id="showcredentials_profile_name"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showmodel_id
|
||||
await page.locator('//*[@id="showmodel_id"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmodel_id"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showcredentials_profile_name
|
||||
await page.locator('//*[@id="showcredentials_profile_name"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked()
|
||||
).toBeFalsy();
|
||||
|
||||
// showmodel_id
|
||||
await page.locator('//*[@id="showmodel_id"]').click();
|
||||
expect(await page.locator('//*[@id="showmodel_id"]').isChecked()).toBeFalsy();
|
||||
|
||||
// showcredentials_profile_name
|
||||
await page.locator('//*[@id="showcredentials_profile_name"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showcredentials_profile_name"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
// showmodel_id
|
||||
await page.locator('//*[@id="showmodel_id"]').click();
|
||||
expect(
|
||||
await page.locator('//*[@id="showmodel_id"]').isChecked()
|
||||
).toBeTruthy();
|
||||
|
||||
await page.getByTestId("dropdown-edit-1-display").click();
|
||||
await page.getByTestId("ai21.j2-ultra-v1-5-option").click();
|
||||
|
||||
value = await page.getByTestId("dropdown-edit-1-display").innerText();
|
||||
if (value !== "ai21.j2-ultra-v1") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page.locator('//*[@id="saveChangesBtn"]').click();
|
||||
|
||||
value = await page.getByTestId("dropdown-2-display").innerText();
|
||||
if (value !== "ai21.j2-ultra-v1") {
|
||||
expect(false).toBeTruthy();
|
||||
}
|
||||
});
|
||||
|
|
@ -13,7 +13,7 @@ test("FloatComponent", async ({ page }) => {
|
|||
await page.waitForTimeout(2000);
|
||||
|
||||
await page
|
||||
.locator('//*[@id="sideLlamaCpp"]')
|
||||
.locator('//*[@id="llmsLlamaCpp"]')
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
|
|
@ -36,12 +36,8 @@ test("FloatComponent", async ({ page }) => {
|
|||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page
|
||||
.locator(
|
||||
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
|
||||
)
|
||||
.click();
|
||||
await page.locator('//*[@id="editAdvancedIcon"]').click();
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByTestId("edit-button-modal").click();
|
||||
|
||||
await page.locator('//*[@id="showcache"]').click();
|
||||
expect(await page.locator('//*[@id="showcache"]').isChecked()).toBeTruthy();
|
||||
|
|
@ -380,12 +376,8 @@ test("FloatComponent", async ({ page }) => {
|
|||
if (elementCount === 0) {
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
await page
|
||||
.locator(
|
||||
'//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div/div/div[1]/div/div[1]/div'
|
||||
)
|
||||
.click();
|
||||
await page.locator('//*[@id="editAdvancedIcon"]').click();
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByTestId("edit-button-modal").click();
|
||||
|
||||
// showtemperature
|
||||
await page.locator('//*[@id="showtemperature"]').click();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ test.describe("Flow Page tests", () => {
|
|||
.filter({ hasText: /^Custom Component$/ })
|
||||
.nth(4)
|
||||
.dragTo(page.locator(".react-flow__pane"));
|
||||
await page.locator("div:nth-child(4) > .extra-side-bar-buttons").click();
|
||||
await page.locator(".success-alert").click();
|
||||
|
||||
// await page.getByTestId("icon-ExternalLink").click();
|
||||
// await page.locator('//*[@id="checkAndSaveBtn"]').click();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ test("IntComponent", async ({ page }) => {
|
|||
await page.waitForTimeout(2000);
|
||||
|
||||
await page
|
||||
.locator('//*[@id="sideGET Request"]')
|
||||
.getByTestId("utilitiesGET Request")
|
||||
.first()
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
|
|
@ -30,7 +31,7 @@ test("IntComponent", async ({ page }) => {
|
|||
}
|
||||
|
||||
await page.locator('//*[@id="int-input-2"]').click();
|
||||
await page.locator('//*[@id="int-input-2"]').fill("-3");
|
||||
await page.locator('//*[@id="int-input-2"]').fill("0");
|
||||
|
||||
value = await page.locator('//*[@id="int-input-2"]').inputValue();
|
||||
|
||||
|
|
@ -38,11 +39,8 @@ test("IntComponent", async ({ page }) => {
|
|||
expect(false).toBeTruthy();
|
||||
}
|
||||
|
||||
await page
|
||||
.locator('//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div')
|
||||
.click();
|
||||
await page.locator('//*[@id="advancedIcon"]').click();
|
||||
await page.locator('//*[@id="editAdvancedBtn"]').click();
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByTestId("edit-button-modal").click();
|
||||
|
||||
value = await page.locator('//*[@id="int-input-1"]').inputValue();
|
||||
|
||||
|
|
@ -77,11 +75,8 @@ test("IntComponent", async ({ page }) => {
|
|||
if (elementCount === 0) {
|
||||
expect(true).toBeTruthy();
|
||||
|
||||
await page
|
||||
.locator('//*[@id="react-flow-id"]/div[1]/div[1]/div[1]/div/div[2]/div')
|
||||
.click();
|
||||
await page.locator('//*[@id="advancedIcon"]').click();
|
||||
await page.locator('//*[@id="editAdvancedBtn"]').click();
|
||||
await page.getByTestId("more-options-modal").click();
|
||||
await page.getByTestId("edit-button-modal").click();
|
||||
|
||||
await page.locator('//*[@id="showtimeout"]').click();
|
||||
expect(
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue