diff --git a/dashboard/app/(copilot)/_parts/CopilotNotFound.tsx b/dashboard/app/(copilot)/_parts/CopilotNotFound.tsx
new file mode 100644
index 000000000..954f6e965
--- /dev/null
+++ b/dashboard/app/(copilot)/_parts/CopilotNotFound.tsx
@@ -0,0 +1,12 @@
+import { EmptyBlock } from "@/components/domain/EmptyBlock";
+import React from "react";
+
+export function CopilotNotFound() {
+ return (
+
+
+ Copilot not found
+
+
+ );
+}
diff --git a/dashboard/app/(copilot)/copilot/[copilot_id]/flows/_parts/WorkflowsList.tsx b/dashboard/app/(copilot)/copilot/[copilot_id]/flows/_parts/WorkflowsList.tsx
index 01a4e87e6..9f3887f71 100644
--- a/dashboard/app/(copilot)/copilot/[copilot_id]/flows/_parts/WorkflowsList.tsx
+++ b/dashboard/app/(copilot)/copilot/[copilot_id]/flows/_parts/WorkflowsList.tsx
@@ -25,6 +25,7 @@ export function WorkflowsList({ copilot_id }: { copilot_id: string }) {
diff --git a/dashboard/app/(copilot)/copilot/[copilot_id]/flows/_parts/useIsEditing.ts b/dashboard/app/(copilot)/copilot/[copilot_id]/flows/_parts/useIsEditing.ts
new file mode 100644
index 000000000..25319cd9e
--- /dev/null
+++ b/dashboard/app/(copilot)/copilot/[copilot_id]/flows/_parts/useIsEditing.ts
@@ -0,0 +1,8 @@
+import { useSearchParams } from "next/navigation";
+
+export function useIsEditing() {
+ const searchParams = useSearchParams();
+ const workflow_id = searchParams.get("workflow_id");
+ const isEditing = !!workflow_id;
+ return [isEditing, workflow_id] as const;
+}
diff --git a/dashboard/app/(copilot)/copilot/[copilot_id]/flows/page.tsx b/dashboard/app/(copilot)/copilot/[copilot_id]/flows/page.tsx
index d506796d5..d18518574 100644
--- a/dashboard/app/(copilot)/copilot/[copilot_id]/flows/page.tsx
+++ b/dashboard/app/(copilot)/copilot/[copilot_id]/flows/page.tsx
@@ -1,37 +1,49 @@
"use client";
import React, { useState } from "react";
-import {
- CodePreview,
- Controller,
- FlowArena,
- useController,
- transformPaths,
- trasnformEndpointNodesData,
-} from "@openchatai/copilot-flows-editor";
import { HeaderShell } from "@/components/domain/HeaderShell";
import { useCopilot } from "../../_context/CopilotProvider";
-import useSwr from "swr";
+import useSwr, { mutate } from "swr";
import { getSwaggerByBotId } from "@/data/swagger";
import { Button } from "@/components/ui/button";
import {
createWorkflowByBotId,
deleteWorkflowById,
- getWorkflowsByBotId,
+ getWorkflowById,
} from "@/data/flow";
-import { useRouter, useSearchParams } from "next/navigation";
+import { useRouter } from "next/navigation";
import { toast } from "@/components/ui/use-toast";
import _ from "lodash";
+import { useIsEditing } from "./_parts/useIsEditing";
+import {
+ Controller,
+ FlowArena,
+ transformPaths,
+ useController,
+} from "@/components/domain/flows-editor";
function Header() {
- const { id: copilotId, name: copilotName } = useCopilot();
- const { loadPaths, state } = useController();
- const saerchParams = useSearchParams();
- const workflow_id = saerchParams.get("workflow_id");
- const isEditing = !!workflow_id;
- const { replace } = useRouter();
- const [loading, setLoading] = useState(false);
// editing => workflow_id // creating => undefined
- const { isLoading: isSwaggerLoading } = useSwr(
+ const [isEditing, workflow_id] = useIsEditing();
+ const { replace } = useRouter();
+ const { id: copilotId, name: copilotName } = useCopilot();
+ const {
+ loadPaths,
+ reset: resetFlowEditor,
+ getData,
+ loadFlows,
+ state,
+ } = useController();
+ useSwr(workflow_id, getWorkflowById, {
+ onSuccess: (data) => {
+ const first = _.first(data.data.flows);
+ if (first) {
+ console.log(first);
+ // @ts-ignore
+ loadFlows([first]);
+ }
+ },
+ });
+ const { isLoading: isSwaggerLoading, mutate: mutateSwagger } = useSwr(
copilotId + "swagger_file",
async () => getSwaggerByBotId(copilotId),
{
@@ -41,14 +53,16 @@ function Header() {
},
},
);
+ console.log(state);
+ const [loading, setLoading] = useState(false);
const isLoading = isSwaggerLoading || loading;
+
async function handleSave() {}
async function handleDelete() {
if (!isEditing || !workflow_id) return;
setLoading(true);
const { data } = await deleteWorkflowById(workflow_id);
if (data) {
- console.log(data);
toast({
title: "Workflow deleted",
description: "Your workflow has been deleted.",
@@ -57,46 +71,46 @@ function Header() {
replace(`/copilot/${copilotId}/flows`, {
scroll: false,
});
+ mutate(copilotId + "/workflows");
setLoading(false);
}
}
async function handleCreate() {
if (isEditing) return;
- setLoading(true);
- const firstFlow = _.first(state.flows);
- const steps = trasnformEndpointNodesData(firstFlow?.steps || []);
-
- const { data } = await createWorkflowByBotId(copilotId, {
- opencopilot: "0.1",
- info: {
- title: "My OpenCopilot definition",
- version: "1.0.0",
- },
- flows: [
- {
- name: firstFlow?.name,
- description: firstFlow?.description,
- steps: steps.map((step) => ({
- operation: "call",
- stepId: step.id,
- open_api_operation_id: step.operationId,
- })),
- requires_confirmation: true,
- on_success: [{}],
- on_failure: [{}],
- },
- ],
- });
- if (data.workflow_id) {
+ const firstFlow = _.first(getData().flows);
+ if (firstFlow) {
+ setLoading(true);
+ try {
+ const { data } = await createWorkflowByBotId(copilotId, getData());
+ if (data.workflow_id) {
+ toast({
+ title: "Workflow created",
+ description: "Your workflow has been created.",
+ variant: "success",
+ });
+ replace(
+ `/copilot/${copilotId}/flows/?workflow_id=${data.workflow_id}`,
+ {
+ scroll: false,
+ },
+ );
+ mutate(copilotId + "/workflows");
+ }
+ } catch (error) {
+ toast({
+ title: "Workflow not created",
+ description: "Something went wrong.",
+ variant: "destructive",
+ });
+ } finally {
+ setLoading(false);
+ }
+ } else {
toast({
- title: "Workflow created",
- description: "Your workflow has been created.",
- variant: "success",
- });
- replace(`/copilot/${copilotId}/flows/?workflow_id=${data.workflow_id}`, {
- scroll: false,
+ title: "Workflow not created",
+ description: "You need at least one flow to create a workflow.",
+ variant: "destructive",
});
- setLoading(false);
}
}
return (
@@ -122,53 +136,27 @@ function Header() {
Create
+ {
+ confirm("Are you sure??") && resetFlowEditor();
+ mutateSwagger();
+ }}
+ >
+ Reset
+
)}
);
}
-
-export default function FlowsPage({
- params: { copilot_id },
-}: {
- params: { copilot_id: string };
-}) {
- const saerchParams = useSearchParams();
- const workflow_id = saerchParams.get("workflow_id");
- const isEditing = !!workflow_id;
-
- const { data: workflowData } = useSwr(copilot_id + "/swagger", () =>
- getWorkflowsByBotId(copilot_id),
- );
- console.log(workflowData);
+export default function FlowsPage() {
return (
- // @ts-ignore
-
+
diff --git a/dashboard/app/(copilot)/copilot/[copilot_id]/page.tsx b/dashboard/app/(copilot)/copilot/[copilot_id]/page.tsx
index afff4ba7d..ec36878ec 100644
--- a/dashboard/app/(copilot)/copilot/[copilot_id]/page.tsx
+++ b/dashboard/app/(copilot)/copilot/[copilot_id]/page.tsx
@@ -42,9 +42,8 @@ function InstallationSection() {
// be aware to call this function when the document/window is ready.
-
const options = {
- apiUrl: "http://localhost:8888/backend/api" // your base url where your are hosting OpenCopilot at (the API), usually it's http://localhost:5000/api
+ apiUrl: "http://localhost:8888/backend/api", // your base url where your are hosting OpenCopilot at (the API), usually it's http://localhost:5000/api
initialMessages: ["How are the things"], // optional: you can pass an array of messages that will be sent to the copilot when it's initialized
token: "${CopilotToken}", // you can get your token from the dashboard
triggerSelector: "#triggerSelector", // the selector of the element that will trigger the copilot when clicked
diff --git a/dashboard/app/(copilot)/copilot/_context/CopilotProvider.tsx b/dashboard/app/(copilot)/copilot/_context/CopilotProvider.tsx
index 1510093e0..62373df06 100644
--- a/dashboard/app/(copilot)/copilot/_context/CopilotProvider.tsx
+++ b/dashboard/app/(copilot)/copilot/_context/CopilotProvider.tsx
@@ -5,6 +5,7 @@ import { createSafeContext } from "@/lib/createSafeContext";
import { useParams } from "next/navigation";
import React from "react";
import useSwr from "swr";
+import { CopilotNotFound } from "../../_parts/CopilotNotFound";
const [SafeCopilotProvider, useCopilot] = createSafeContext(
"[useCopilot] should be used within a CopilotProvider",
);
@@ -26,7 +27,7 @@ function CopilotProvider({ children }: { children: React.ReactNode }) {
);
} else {
- return copilot not found
;
+ return ;
}
}
diff --git a/dashboard/app/globals.css b/dashboard/app/globals.css
index 18fdbf0e0..478f5c85c 100644
--- a/dashboard/app/globals.css
+++ b/dashboard/app/globals.css
@@ -1,4 +1,3 @@
-@import url("@openchatai/copilot-flows-editor/dist/style.css");
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -102,3 +101,27 @@
display: none; /* Safari and Chrome */
}
}
+.loading-el {
+ position: relative;
+ overflow: hidden;
+ pointer-events: none;
+ color: transparent;
+}
+.loading-el::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: 10;
+ @apply bg-white opacity-50;
+}
+
+.loading-el::before {
+ content: "";
+ position: absolute;
+ z-index: 11;
+ translate: -50% -50%;
+ top: 50%;
+ left: 50%;
+ border-width: 3px;
+ @apply h-5 w-5 animate-spin rounded-full border-primary border-t-transparent;
+}
diff --git a/dashboard/app/swr-provider.tsx b/dashboard/app/swr-provider.tsx
index 693e549dc..f17025782 100644
--- a/dashboard/app/swr-provider.tsx
+++ b/dashboard/app/swr-provider.tsx
@@ -2,5 +2,13 @@
import React from "react";
import { SWRConfig } from "swr";
export function SWRProvider({ children }: { children: React.ReactNode }) {
- return {children} ;
+ return (
+
+ {children}
+
+ );
}
diff --git a/dashboard/components/domain/CodeBlock.tsx b/dashboard/components/domain/CodeBlock.tsx
index b9947b4b3..a84672bd0 100644
--- a/dashboard/components/domain/CodeBlock.tsx
+++ b/dashboard/components/domain/CodeBlock.tsx
@@ -41,11 +41,13 @@ function CodeBlock({
padding: "20px 24px",
color: "#ddd",
fontFamily: '"Fira Code"',
- fontSize: "13px",
+ fontSize: "14px",
margin: "0px",
borderRadius: "0.5rem",
+ fontWeight: 500,
},
}}
+ wrapLongLines
>
{code}
diff --git a/dashboard/components/domain/flows-editor/editor/AsideMenu.tsx b/dashboard/components/domain/flows-editor/editor/AsideMenu.tsx
new file mode 100644
index 000000000..46fcc1c15
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/editor/AsideMenu.tsx
@@ -0,0 +1,104 @@
+import { useMode } from "../stores/ModeProvider";
+import { PathButton } from "./PathButton";
+import { useMemo, useState } from "react";
+import { MethodBtn } from "./MethodRenderer";
+import { FlowsList } from "./FlowsList";
+import { useController } from "../stores/Controller";
+import { isEmpty } from "lodash";
+import { cn } from "@/lib/utils";
+import { Input } from "@/components/ui/input";
+
+export function AsideMenu() {
+ const {
+ state: { paths },
+ } = useController();
+
+ const { mode, isEdit } = useMode();
+
+ const [search, setSearch] = useState("");
+
+ const renderedPaths = useMemo(
+ () =>
+ search.trim().length > 0
+ ? paths.filter((path) => path.path.includes(search.trim()))
+ : paths,
+ [search, paths],
+ );
+
+ return (
+
+
+
+
+
+ Select Step
+
+
+
+
+ setSearch(e.target.value)}
+ placeholder="Search..."
+ />
+
+
+
+
+ {!isEmpty(renderedPaths) && (
+ <>
+ {renderedPaths.map((path) => (
+
+
+
+ ))}
+ >
+ )}
+
+
+
+
+ {mode.type === "edit-node" && (
+
+
+
+ {mode.node.data.path}
+
+ {mode.node.data.method}
+
+
+
+
+ {/* {mode.node.data.tags?.map((t, i) => (
+
+ #{t}
+
+ ))} */}
+
+
+
{mode.node.data.description}
+
+
+ )}
+
+
+
+
+ );
+}
diff --git a/dashboard/components/domain/flows-editor/editor/EndpointEdge.tsx b/dashboard/components/domain/flows-editor/editor/EndpointEdge.tsx
new file mode 100644
index 000000000..998c5676c
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/editor/EndpointEdge.tsx
@@ -0,0 +1,61 @@
+import { Plus } from "lucide-react";
+import {
+ BaseEdge,
+ EdgeLabelRenderer,
+ EdgeProps,
+ getStraightPath,
+} from "reactflow";
+import { useMode } from "../stores/ModeProvider";
+import { useMemo } from "react";
+import { cn } from "@/lib/utils";
+
+export function NodeEdge({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ ...props
+}: EdgeProps) {
+ const [edgePath, labelX, labelY] = getStraightPath({
+ sourceX,
+ sourceY,
+ targetX,
+ targetY,
+ });
+ const { mode } = useMode();
+ const activeEdge = useMemo(() => {
+ if (mode.type === "add-node-between") return mode.edge;
+ }, [mode]);
+ return (
+ <>
+
+
+
+
+ >
+ );
+}
diff --git a/dashboard/components/domain/flows-editor/editor/EndpointNode.tsx b/dashboard/components/domain/flows-editor/editor/EndpointNode.tsx
new file mode 100644
index 000000000..2edee5ae9
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/editor/EndpointNode.tsx
@@ -0,0 +1,164 @@
+import {
+ Handle,
+ NodeProps,
+ Position,
+ useNodeId,
+ useNodes,
+ NodeToolbar,
+ useReactFlow,
+} from "reactflow";
+import { useMode } from "../stores/ModeProvider";
+import { memo, useCallback, useMemo } from "react";
+import { Y, nodedimensions } from "./consts";
+import { PlusIcon, TrashIcon } from "lucide-react";
+import { MethodBtn } from "./MethodRenderer";
+import { NodeData } from "../types/Swagger";
+import {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import { updateNodesPositions } from "../utils";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import _ from "lodash";
+
+const HideHandleStyles = {
+ background: "transparent",
+ fill: "transparent",
+ color: "transparent",
+ border: "none",
+};
+function EndpointNode({ data, zIndex }: NodeProps) {
+ const nodes = useNodes();
+ const { setNodes } = useReactFlow();
+ const nodeId = useNodeId();
+ const nodeObj = nodes.find((n) => n.id === nodeId);
+ const { mode, setMode, reset: resetMode } = useMode();
+ const isActive = useMemo(() => {
+ if (mode.type === "edit-node") {
+ return mode.node.id === nodeId;
+ } else {
+ return false;
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [mode]);
+
+ const isFirstNode = nodes?.[0]?.id === nodeId;
+ const isLastNode = nodes?.[nodes.length - 1]?.id === nodeId;
+ const deleteNode = useCallback(() => {
+ setTimeout(() => {
+ setNodes(
+ updateNodesPositions(
+ nodes.filter((nd) => nd.id !== nodeId),
+ Y,
+ ),
+ );
+ resetMode();
+ }, 300);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+ Are you sure you want to delete this node?
+
+
+
+ Yup!
+
+
+ Nope!
+
+
+
+
+
+ {!isFirstNode && (
+
+ )}
+
+
{
+ nodeObj && setMode({ type: "edit-node", node: nodeObj });
+ }}
+ className={cn(
+ "group h-full w-full cursor-pointer select-none rounded border bg-white transition-all duration-300 ease-in-out",
+ isActive
+ ? "border-primary [box-shadow:inset_0px_2.5px_0px_0px_theme(colors.indigo.500)]"
+ : "border-border hover:shadow",
+ )}
+ >
+
+
+ {data.path}
+
+
+ {data.description}
+
+
+ {data.method}
+
+
+
+ {isLastNode && (
+
+
setMode({ type: "append-node" })}
+ className={cn(
+ "rounded bg-[#ddd] p-0.5 text-sm transition-all duration-300 ease-in-out",
+ mode.type === "append-node" &&
+ "text-primbg-primary bg-primary ring-4 ring-primary/20 ring-offset-transparent",
+ )}
+ >
+
+
+
+ )}
+
+
+
+ >
+ );
+}
+
+const MemoizedEndpointNode = memo(EndpointNode);
+export default MemoizedEndpointNode;
diff --git a/dashboard/components/domain/flows-editor/editor/FlowArena.tsx b/dashboard/components/domain/flows-editor/editor/FlowArena.tsx
new file mode 100644
index 000000000..233056c2f
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/editor/FlowArena.tsx
@@ -0,0 +1,133 @@
+import ReactFlow, {
+ Background,
+ OnConnect,
+ addEdge,
+ useEdgesState,
+ MarkerType,
+ Edge,
+ applyNodeChanges,
+ NodeChange,
+ useReactFlow,
+} from "reactflow";
+import { useCallback, useEffect, useMemo } from "react";
+import "reactflow/dist/style.css";
+import { NodeEdge } from "./EndpointEdge";
+import EndpointNode from "./EndpointNode";
+import { AsideMenu } from "./AsideMenu";
+import { useMode } from "../stores/ModeProvider";
+import { BUILDER_SCALE } from "./consts";
+import { useController } from "../stores/Controller";
+
+export function FlowArena() {
+ const nodeTypes = useMemo(
+ () => ({
+ endpointNode: EndpointNode,
+ }),
+ [],
+ );
+ const edgeTypes = useMemo(
+ () => ({
+ endpointEdge: NodeEdge,
+ }),
+ [],
+ );
+ const { fitView } = useReactFlow();
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
+ const {
+ activeNodes,
+ setNodes,
+ state: { activeFlowId },
+ } = useController();
+ const { setMode } = useMode();
+ // auto connect nodes
+ useEffect(() => {
+ if (!activeNodes) return;
+ if (activeNodes.length === 0) {
+ setMode({ type: "append-node" });
+ return;
+ }
+ fitView();
+ const newEdges = activeNodes
+ .map((v, i, a) => {
+ const curr = v;
+ const next = a.at(i + 1);
+ if (curr && next) {
+ const id = curr.id + "-" + next.id;
+ return {
+ id: id,
+ target: curr.id,
+ source: next.id,
+ type: "endpointEdge",
+ markerStart: {
+ type: MarkerType.ArrowClosed,
+ },
+ };
+ }
+ })
+ .filter((v) => typeof v !== "undefined") as Edge[];
+ setEdges(newEdges);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [activeNodes]);
+
+ const onConnect: OnConnect = useCallback(
+ (connection) => setEdges((eds) => addEdge(connection, eds)),
+ [setEdges],
+ );
+ const onNodesChange = useCallback(
+ (changes: NodeChange[]) => {
+ const nodeChange = applyNodeChanges(changes, activeNodes || []);
+ setNodes(nodeChange);
+ },
+ [setNodes, activeNodes],
+ );
+ const empty = useMemo(() => {
+ return activeNodes?.length === 0 || activeFlowId === undefined;
+ }, [activeNodes, activeFlowId]);
+ return (
+ <>
+
+
+
+ {empty && (
+
+
+
+ {activeFlowId
+ ? "Start by selecting an endpoint from the menu"
+ : "Start by creating a flow"}
+
+
+
+ )}
+
{
+ event.stopPropagation();
+ event.bubbles = false;
+ setMode({
+ type: "add-node-between",
+ edge: edge,
+ });
+ }}
+ className="h-full w-full origin-center transition-all duration-300"
+ edgeTypes={edgeTypes}
+ maxZoom={BUILDER_SCALE}
+ minZoom={BUILDER_SCALE}
+ onNodesChange={onNodesChange}
+ onEdgesChange={onEdgesChange}
+ onConnect={onConnect}
+ deleteKeyCode={[]}
+ fitView
+ >
+
+
+
+
+ >
+ );
+}
diff --git a/dashboard/components/domain/flows-editor/editor/FlowsList.tsx b/dashboard/components/domain/flows-editor/editor/FlowsList.tsx
new file mode 100644
index 000000000..1d170ee66
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/editor/FlowsList.tsx
@@ -0,0 +1,179 @@
+import React, { useState } from "react";
+import {
+ Box,
+ ChevronRightIcon,
+ Pencil,
+ PlusIcon,
+ TrashIcon,
+} from "lucide-react";
+import { useController } from "../stores/Controller";
+import { useMode } from "../stores/ModeProvider";
+import { useSettings } from "../stores/Config";
+import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
+import { DialogHeader } from "@/components/ui/dialog";
+import { EmptyBlock } from "../../EmptyBlock";
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+export function FlowsList() {
+ const [flowsPanelOpened, setFlowsPanel] = useState(false);
+ const [modalOpen, setModalOpen] = useState(false);
+ const { reset } = useMode();
+ const { maxFlows } = useSettings();
+ const {
+ createFlow,
+ state: { flows, activeFlowId },
+ setActiveFlow,
+ deleteFlow,
+ } = useController();
+ function onSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ const data = new FormData(e.currentTarget);
+ const [name, description, focus] = [
+ data.get("name"),
+ data.get("description"),
+ data.get("focus"),
+ ];
+ if (name && description) {
+ createFlow({
+ createdAt: Date.now(),
+ name: name.toString(),
+ description: description.toString(),
+ focus: focus === "on" ? true : false,
+ });
+ setModalOpen(false);
+ }
+ }
+ return (
+
+
+
setFlowsPanel((pre) => !pre)}
+ >
+
+ flows
+
+
+
+
+ Create new flow
+
+
+
+ {
+ if (maxFlows && flows.length >= maxFlows) {
+ alert(`You can only have ${maxFlows} flows at a time.`);
+ ev.preventDefault();
+ return;
+ }
+ }}
+ >
+
+
+
+
+
+
+
+ {flows.length === 0 ? (
+
+ ) : (
+
+ {flows?.map((flow) => {
+ const isActive = flow.id === activeFlowId;
+ return (
+
+
+
+
+ {flow.name}
+
+
+
{
+ if (isActive) return;
+ setActiveFlow(flow.id);
+ reset();
+ }}
+ className="text-slate-500"
+ >
+
+
+
{
+ confirm(
+ "Are you sure you want to delete this flow?",
+ ) && deleteFlow(flow.id);
+ }}
+ >
+
+
+
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/dashboard/components/domain/flows-editor/editor/MethodRenderer.tsx b/dashboard/components/domain/flows-editor/editor/MethodRenderer.tsx
new file mode 100644
index 000000000..9ca87c925
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/editor/MethodRenderer.tsx
@@ -0,0 +1,34 @@
+import { ComponentPropsWithoutRef, ElementRef, forwardRef } from "react";
+import { Method } from "../types/Swagger";
+import { cn } from "@/lib/utils";
+// BG colors for method buttons
+const methodStyles = (method: Method) => {
+ switch (method.toUpperCase()) {
+ case "GET":
+ return "bg-green-400";
+ case "POST":
+ return "bg-blue-400";
+ case "PUT":
+ return "bg-yellow-400";
+ case "DELETE":
+ return "bg-red-400";
+ default:
+ return "bg-gray-400";
+ }
+};
+export const MethodBtn = forwardRef<
+ ElementRef<"button">,
+ { method: Method } & ComponentPropsWithoutRef<"button">
+>(({ method, className, ...props }, _ref) => (
+
+));
+
+MethodBtn.displayName = "MethodBtn";
diff --git a/dashboard/components/domain/flows-editor/editor/PathButton.tsx b/dashboard/components/domain/flows-editor/editor/PathButton.tsx
new file mode 100644
index 000000000..41f696d1c
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/editor/PathButton.tsx
@@ -0,0 +1,128 @@
+import { useReactFlow, type Node, Edge } from "reactflow";
+import { MethodBtn } from "./MethodRenderer";
+import { Y } from "./consts";
+import { useMode } from "../stores/ModeProvider";
+import { useCallback } from "react";
+import { useController } from "../stores/Controller";
+import { Method, NodeData, TransformedPath } from "../types/Swagger";
+import { genId, updateNodesPositions } from "../utils";
+import {
+ Tooltip,
+ TooltipProvider,
+ TooltipContent,
+ TooltipTrigger,
+} from "@/components/ui/tooltip";
+
+export function PathButton({ path }: { path: TransformedPath }) {
+ const { mode } = useMode();
+ const { setNodes, getNodes, setEdges } = useReactFlow();
+ const nodes = getNodes();
+ const {
+ state: { activeFlowId },
+ } = useController();
+ const appendNode = useCallback(
+ (payload: NodeData) => {
+ const id = genId();
+ const newNode: Node = {
+ id: id,
+ type: "endpointNode",
+ data: payload,
+ draggable: false,
+ position: { x: 0, y: Y * nodes.length },
+ };
+ setNodes((nds) => updateNodesPositions([...nds, newNode], Y));
+ },
+ [nodes.length, setNodes],
+ );
+
+ const addNodeBetween = useCallback(
+ (edge: Edge, payload: NodeData) => {
+ const targetNode = nodes.find((node) => node.id === edge.target);
+ const sourceNode = nodes.find((node) => node.id === edge.source);
+ if (!targetNode || !sourceNode) {
+ return;
+ }
+ // delete the edge
+ setEdges((eds) => eds.filter((ed) => ed.id !== edge.id));
+ // add the new node
+ const id = genId();
+ const newNode: Node = {
+ id: id,
+ type: "endpointNode",
+ data: payload,
+ draggable: false,
+ position: {
+ x: 0,
+ y: (sourceNode.position.y + targetNode.position.y) / 2,
+ },
+ };
+ // put the new node in the middle of the two nodes that were connected (make sure the node is sorted in array too)
+ const sourceIndex = nodes.findIndex((node) => node.id === sourceNode.id);
+ const newNodes = nodes
+ .slice(0, sourceIndex)
+ .concat(newNode)
+ .concat(nodes.slice(sourceIndex));
+ setNodes(updateNodesPositions(newNodes, Y));
+ },
+ [nodes, setEdges, setNodes],
+ );
+
+ const isPresentInNodes = useCallback(
+ (method: Method) => {
+ return !!nodes.find((node) => {
+ return (
+ node.data.path === path.path &&
+ node.data.method.toLowerCase() === method.toLowerCase()
+ );
+ });
+ },
+ [nodes, path],
+ );
+
+ return (
+
+
+ {path.path}
+
+
+ {path.methods.map((method, i) => {
+ return (
+
+ {method.description}
+
+ {
+ if (isPresentInNodes(method.method)) {
+ return;
+ }
+ if (!activeFlowId) {
+ alert("Please create a flow first");
+ return;
+ }
+ const newNode: NodeData = {
+ path: path.path,
+ ...method,
+ };
+ if (mode.type === "append-node") {
+ appendNode(newNode);
+ } else if (mode.type === "add-node-between") {
+ addNodeBetween(mode.edge, newNode);
+ }
+ }}
+ >
+ {method.method.toUpperCase()}
+
+
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/dashboard/components/domain/flows-editor/editor/consts.ts b/dashboard/components/domain/flows-editor/editor/consts.ts
new file mode 100644
index 000000000..bad594e9a
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/editor/consts.ts
@@ -0,0 +1,6 @@
+export const Y = 150;
+export const nodedimensions = {
+ width: 250,
+ height: 100,
+}
+export const BUILDER_SCALE = 1.25;
\ No newline at end of file
diff --git a/dashboard/components/domain/flows-editor/index.ts b/dashboard/components/domain/flows-editor/index.ts
new file mode 100644
index 000000000..abfded67e
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/index.ts
@@ -0,0 +1,10 @@
+export { FlowArena } from "./editor/FlowArena";
+export { Controller, useController } from "./stores/Controller";
+export type { Paths, TransformedPath } from "./types/Swagger";
+export type { Flow, FlowSchema, Step } from "./types/Flow";
+export { transformPaths } from "./utils/transformSwagger";
+// some exported utils
+export {
+ transformaEndpointToNode,
+ trasnformEndpointNodesData,
+} from "./utils/transformEndpointNodes";
diff --git a/dashboard/components/domain/flows-editor/stores/Config.ts b/dashboard/components/domain/flows-editor/stores/Config.ts
new file mode 100644
index 000000000..6337d4c26
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/stores/Config.ts
@@ -0,0 +1,10 @@
+import { createSafeContext } from "@/lib/createSafeContext";
+
+export type Settings = {
+ standalone?: boolean;
+ maxFlows?: number;
+};
+
+const [SettingsProvider, useSettings] = createSafeContext("");
+
+export { SettingsProvider, useSettings };
diff --git a/dashboard/components/domain/flows-editor/stores/Controller.tsx b/dashboard/components/domain/flows-editor/stores/Controller.tsx
new file mode 100644
index 000000000..2ddf08114
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/stores/Controller.tsx
@@ -0,0 +1,226 @@
+import {
+ useReducer,
+ type ReactNode,
+ useCallback,
+ useMemo,
+ useEffect,
+} from "react";
+import { produce } from "immer";
+import { Node } from "reactflow";
+import { ModeProvider } from "./ModeProvider";
+import { TransformedPath } from "../types/Swagger";
+import { genId } from "../utils";
+import { ReactFlowProvider } from "reactflow";
+import { EndpointNodeType, Flow, FlowType } from "../types/Flow";
+import { Settings, SettingsProvider } from "./Config";
+import { getDef } from "../utils/getDef";
+import { createSafeContext } from "@/lib/createSafeContext";
+import { transformaEndpointToNode } from "..";
+
+type StateShape = {
+ paths: TransformedPath[];
+ activeFlowId?: string;
+ flows: FlowType[];
+};
+
+type ControllerContextType = {
+ loadPaths: (paths: TransformedPath[]) => void;
+ state: StateShape;
+ activeNodes?: EndpointNodeType[];
+ createFlow: (data: CreateFlowPayload) => void;
+ setActiveFlow: (id: string) => void;
+ setNodes: (nodes: Node[]) => void;
+ reset: () => void;
+ deleteFlow: (id: string) => void;
+ getData: () => ReturnType;
+ loadFlows: (data: Flow[]) => void;
+};
+
+type ActionType =
+ | { type: "reset" }
+ | { type: "load-paths"; pyload: TransformedPath[] }
+ | { type: "set-active-flow"; pyload: string }
+ | {
+ type: "create-flow";
+ pyload: {
+ name: string;
+ description?: string;
+ createdAt?: number;
+ focus: boolean;
+ };
+ }
+ | {
+ type: "delete-flow";
+ pyload: string;
+ }
+ | { type: "set-flows"; pyload: StateShape["flows"] }
+ | { type: "set-nodes"; payload: Node[] }
+ | { type: "load-flows"; payload: Flow[] };
+
+type CreateFlowPayload = Extract["pyload"];
+
+const initialStateValue: StateShape = {
+ paths: [],
+ flows: [],
+ activeFlowId: undefined,
+};
+const [SafeProvider, useController] =
+ createSafeContext("");
+function stateReducer(state: StateShape, action: ActionType) {
+ if (action.type === "reset") return initialStateValue;
+ return produce(state, (draft) => {
+ switch (action.type) {
+ case "load-paths":
+ draft.paths = action.pyload;
+ break;
+ case "set-active-flow":
+ draft.activeFlowId = action.pyload;
+ break;
+ case "create-flow": {
+ const id = genId();
+ const $newFlow = {
+ steps: [],
+ id,
+ ...action.pyload,
+ };
+ draft.flows.push($newFlow);
+ if (action.pyload.focus) draft.activeFlowId = id;
+ break;
+ }
+ case "set-nodes":
+ {
+ const flow = draft.flows.find((f) => f.id === state.activeFlowId);
+ if (!flow) return;
+ flow.steps = action.payload;
+ flow.updatedAt = Date.now();
+ }
+ break;
+ case "delete-flow":
+ draft.flows = draft.flows.filter((f) => f.id !== action.pyload);
+ if (draft.activeFlowId === action.pyload) {
+ draft.activeFlowId = undefined;
+ }
+ break;
+ case "load-flows": {
+ const id = genId();
+ if (action.payload) {
+ draft.flows = action.payload.map((f) => ({
+ ...f,
+ id,
+ name: f.name,
+ description: f.description,
+ // @ts-ignore
+ steps: transformaEndpointToNode(f.steps),
+ }));
+ draft.activeFlowId = id;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ });
+}
+
+function Controller({
+ children,
+ onChange,
+ initialState,
+ ...settings
+}: {
+ children: ReactNode;
+ initialState?: StateShape;
+ onChange?: (state: StateShape) => void;
+} & Settings) {
+ const [state, dispatch] = useReducer(
+ stateReducer,
+ initialState ? initialState : initialStateValue,
+ );
+
+ useEffect(() => {
+ onChange?.(state);
+ }, [state, onChange]);
+ const loadPaths = useCallback(
+ (paths: TransformedPath[]) =>
+ dispatch({
+ type: "load-paths",
+ pyload: paths,
+ }),
+ [],
+ );
+ const createFlow = useCallback((data: CreateFlowPayload) => {
+ if (settings.maxFlows && state.flows.length >= settings.maxFlows) return;
+ dispatch({
+ type: "create-flow",
+ pyload: data,
+ });
+ }, []);
+ const setActiveFlow = useCallback(
+ (id: string) =>
+ dispatch({
+ type: "set-active-flow",
+ pyload: id,
+ }),
+ [],
+ );
+ const activeNodes = useMemo(() => {
+ if (!state.activeFlowId) return undefined;
+ const flow = state.flows.find((f) => f.id === state.activeFlowId);
+ if (!flow) return undefined;
+ return flow.steps;
+ }, [state]);
+
+ const setNodes = useCallback(
+ (nodes: Node[]) =>
+ dispatch({
+ type: "set-nodes",
+ payload: nodes,
+ }),
+ [],
+ );
+ const deleteFlow = useCallback(
+ (id: string) =>
+ dispatch({
+ type: "delete-flow",
+ pyload: id,
+ }),
+ [],
+ );
+ // TODO: @bug: when we reset, the nodes(in the arena) are not reset
+ const reset = useCallback(() => dispatch({ type: "reset" }), []);
+ const getData = useCallback(() => getDef(state.flows), [state]);
+ const loadFlows = useCallback(
+ (data: Flow[]) =>
+ dispatch({
+ type: "load-flows",
+ payload: data,
+ }),
+ [],
+ );
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
+
+export { useController, Controller };
diff --git a/dashboard/components/domain/flows-editor/stores/ModeProvider.tsx b/dashboard/components/domain/flows-editor/stores/ModeProvider.tsx
new file mode 100644
index 000000000..b89489bcc
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/stores/ModeProvider.tsx
@@ -0,0 +1,53 @@
+import { type Edge, type Node } from "reactflow";
+import React, { useCallback, useMemo, useState } from "react";
+import { NodeData } from "../types/Swagger";
+import { createSafeContext } from "@/lib/createSafeContext";
+type IdleMode = {
+ type: "idle";
+};
+type AppendNodeMode = {
+ type: "append-node";
+};
+type AddNodeBetweenMode = {
+ type: "add-node-between";
+ edge: Edge;
+};
+type EditNodeMode = {
+ type: "edit-node";
+ node: Node;
+};
+
+export type Mode =
+ | AppendNodeMode
+ | AddNodeBetweenMode
+ | EditNodeMode
+ | IdleMode;
+const DEFAULT: Mode = { type: "append-node" };
+type ModeContextType = {
+ mode: Mode;
+ setMode: React.Dispatch>;
+ reset: () => void;
+ isAdd: boolean;
+ isEdit: boolean;
+ isIdle: boolean;
+};
+
+const [ModeSafeProvider, useMode] = createSafeContext("");
+function ModeProvider({ children }: { children: React.ReactNode }) {
+ const [mode, $setMode] = useState(DEFAULT);
+ const reset = useCallback(() => $setMode(DEFAULT), []);
+ const setMode = useCallback($setMode, [$setMode]);
+ const isAdd = useMemo(
+ () => mode.type === "append-node" || mode.type === "add-node-between",
+ [mode],
+ );
+ const isIdle = useMemo(() => mode.type === "idle", [mode]);
+ const isEdit = useMemo(() => mode.type === "edit-node", [mode]);
+ return (
+
+ {children}
+
+ );
+}
+
+export { ModeProvider, useMode };
diff --git a/dashboard/components/domain/flows-editor/types/Flow.ts b/dashboard/components/domain/flows-editor/types/Flow.ts
new file mode 100644
index 000000000..7125e7a42
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/types/Flow.ts
@@ -0,0 +1,40 @@
+import type { Node } from "reactflow";
+import type { NodeData } from "./Swagger";
+
+export type Step = {
+ stepId?: string;
+ operation: string;
+ open_api_operation_id: string;
+ parameters?: Record[];
+};
+
+export type Flow = {
+ name: string;
+ description?: string;
+ requires_confirmation: boolean;
+ steps: Step[];
+};
+
+export type FlowSchema = {
+ opencopilot: string;
+ info: {
+ title: string;
+ version: string;
+ };
+ flows: Flow[];
+ on_success: {
+ handler: string;
+ }[];
+ on_failure: {
+ handler: string;
+ }[];
+};
+export type FlowType = {
+ id: string;
+ name: string;
+ description?: string;
+ createdAt?: number;
+ updatedAt?: number;
+ steps: EndpointNodeType[];
+};
+export type EndpointNodeType = Node;
diff --git a/dashboard/components/domain/flows-editor/types/Swagger.ts b/dashboard/components/domain/flows-editor/types/Swagger.ts
new file mode 100644
index 000000000..d9d173e8b
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/types/Swagger.ts
@@ -0,0 +1,92 @@
+interface Info {
+ title: string;
+ version: string;
+}
+
+interface Response {
+ description: string;
+ content: {
+ [contentType: string]: any;
+ };
+}
+
+interface RequestBody {
+ required?: boolean;
+ content: {
+ [contentType: string]: {
+ schema: any;
+ };
+ };
+}
+
+// Operation Object
+interface Operation {
+ tags?: string[];
+ summary?: string;
+ description?: string;
+ operationId?: string;
+ parameters?: Array<{
+ name: string;
+ in: "query" | "header" | "path" | "cookie";
+ }>;
+ requestBody?: RequestBody;
+ responses: {
+ [statusCode: string]: Response;
+ };
+}
+
+export const methods = [
+ "get",
+ "post",
+ "put",
+ "delete",
+ "options",
+ "head",
+ "patch",
+ "trace",
+] as const;
+export type Method = (typeof methods)[number];
+type PathItem = Record;
+// Paths Object
+export type Paths = Record;
+// Definitions Object (Schema)
+interface Definitions {
+ [name: string]: any; // You can define more specific types based on your needs
+}
+
+// The main Swagger Object
+export interface Swagger<
+ TPaths extends Paths = Paths,
+ TDefinitions extends Definitions = Definitions,
+> {
+ openapi: string;
+ info: Info;
+ servers?: Array<{
+ url: string;
+ description?: string;
+ }>;
+ tags?: Array<{
+ name: string;
+ description?: string;
+ }>;
+ paths: TPaths;
+ definitions?: TDefinitions;
+}
+
+// transformation types
+
+export type ExtendedOperation = Omit<
+ Operation,
+ "responses" | "requestBody" | "parameters"
+> & {
+ method: Method;
+};
+
+export type TransformedPath = {
+ path: string;
+ methods: ExtendedOperation[];
+};
+
+export type NodeData = ExtendedOperation & {
+ path: string;
+};
diff --git a/dashboard/components/domain/flows-editor/utils/genId.ts b/dashboard/components/domain/flows-editor/utils/genId.ts
new file mode 100644
index 000000000..86933a446
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/utils/genId.ts
@@ -0,0 +1,3 @@
+export function genId() {
+ return Date.now().toString(36) + Math.random().toString(36).slice(2);
+}
diff --git a/dashboard/components/domain/flows-editor/utils/getDef.ts b/dashboard/components/domain/flows-editor/utils/getDef.ts
new file mode 100644
index 000000000..7090c5000
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/utils/getDef.ts
@@ -0,0 +1,22 @@
+import type { FlowType } from "../types/Flow";
+import { trasnformEndpointNodesData } from "./transformEndpointNodes";
+
+export function getDef(flows: FlowType[]) {
+ return {
+ opencopilot: "0.1",
+ info: {
+ title: "My OpenCopilot definition",
+ version: "1.0.0",
+ },
+ flows: flows.map((flow) => {
+ return {
+ name: flow.name,
+ description: flow.description,
+ steps: trasnformEndpointNodesData(flow.steps),
+ requires_confirmation: true,
+ on_success: [{}],
+ on_failure: [{}],
+ };
+ }),
+ };
+}
diff --git a/dashboard/components/domain/flows-editor/utils/helpers.ts b/dashboard/components/domain/flows-editor/utils/helpers.ts
new file mode 100644
index 000000000..e272a3a44
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/utils/helpers.ts
@@ -0,0 +1,25 @@
+export function parse(value: string) {
+ if (value === null || value === undefined) return undefined;
+ try {
+ return JSON.parse(value);
+ } catch (error) {
+ return value;
+ }
+}
+export function stringify(value: any): string {
+ try {
+ return JSON.stringify(value);
+ } catch (error) {
+ return value || undefined;
+ }
+}
+export function getStorageValue(key: string, initialValue: any) {
+ const value = window.localStorage.getItem(key);
+ if (value) {
+ return parse(value);
+ }
+ return initialValue;
+}
+export function setStorageValue(key: string, value: any) {
+ localStorage.setItem(key, stringify(value));
+}
diff --git a/dashboard/components/domain/flows-editor/utils/index.ts b/dashboard/components/domain/flows-editor/utils/index.ts
new file mode 100644
index 000000000..6b45ec0ca
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/utils/index.ts
@@ -0,0 +1,3 @@
+export { genId } from './genId';
+export { updateNodesPositions } from './updateNodePosition';
+export { parse, getStorageValue, setStorageValue, stringify } from './helpers';
\ No newline at end of file
diff --git a/dashboard/components/domain/flows-editor/utils/transformEndpointNodes.ts b/dashboard/components/domain/flows-editor/utils/transformEndpointNodes.ts
new file mode 100644
index 000000000..6dbc9a9fb
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/utils/transformEndpointNodes.ts
@@ -0,0 +1,28 @@
+import { Y } from "../editor/consts";
+import type { EndpointNodeType } from "../types/Flow";
+import { genId } from "./genId";
+export function trasnformEndpointNodesData(nodes: EndpointNodeType[]) {
+ return nodes
+ .map((node) => node.data)
+ .map((data) => ({
+ operation: "call",
+ stepId: data.operationId,
+ open_api_operation_id: data.operationId,
+ ...data,
+ }));
+}
+// the reverse of the above function
+export function transformaEndpointToNode(
+ data: EndpointNodeType["data"][],
+): EndpointNodeType[] {
+ return data.map((nodeData, index) => ({
+ id: genId(),
+ type: "endpointNode",
+ draggable: false,
+ position: {
+ x: 0,
+ y: index * Y,
+ },
+ data: nodeData,
+ }));
+}
diff --git a/dashboard/components/domain/flows-editor/utils/transformSwagger.ts b/dashboard/components/domain/flows-editor/utils/transformSwagger.ts
new file mode 100644
index 000000000..e622abcbd
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/utils/transformSwagger.ts
@@ -0,0 +1,28 @@
+import type { ExtendedOperation, Method, Paths, TransformedPath } from "../types/Swagger";
+import { methods as methodsArray } from "../types/Swagger";
+
+/**
+ * @description Transforms the paths object from the swagger file into a more usable format
+ */
+export function transformPaths(paths: Paths): TransformedPath[] {
+ const trasnformedPaths = new Set();
+ Object.keys(paths).forEach((pathString) => {
+ const endpoint = paths[pathString];
+ const methods = new Set()
+ endpoint && Object.keys(endpoint).forEach((method) => {
+ if (methodsArray.includes(method as Method)) {
+ const operation = endpoint[method as Method];
+ operation && methods.add({
+ method: method as Method,
+ ...operation
+ });
+ }
+
+ });
+ trasnformedPaths.add({
+ path: pathString,
+ methods: Array.from(methods)
+ });
+ });
+ return Array.from(trasnformedPaths);
+}
\ No newline at end of file
diff --git a/dashboard/components/domain/flows-editor/utils/updateNodePosition.ts b/dashboard/components/domain/flows-editor/utils/updateNodePosition.ts
new file mode 100644
index 000000000..379b46718
--- /dev/null
+++ b/dashboard/components/domain/flows-editor/utils/updateNodePosition.ts
@@ -0,0 +1,9 @@
+import type { Node } from 'reactflow';
+export function updateNodesPositions(nodes: Node[], Y: number): Node[] {
+ // Choose a suitable distance
+ const updatedNodes = nodes.map((node, index) => ({
+ ...node,
+ position: { x: 0, y: index * Y },
+ }));
+ return updatedNodes;
+}
diff --git a/dashboard/components/ui/NavLink.tsx b/dashboard/components/ui/NavLink.tsx
index 80950351a..b322788e8 100644
--- a/dashboard/components/ui/NavLink.tsx
+++ b/dashboard/components/ui/NavLink.tsx
@@ -56,13 +56,11 @@ export const NavLink = React.forwardRef, Props>(
pathname,
searchParams,
);
- const href = props.href.toString().endsWith("/")
- ? props.href
- : props.href + "/";
+
const isActive = segment
? segments.includes(segment)
- : href ===
- (matchSearchParams ? $pathnamePlusSearchParams : pathname + "/");
+ : props.href ===
+ (matchSearchParams ? $pathnamePlusSearchParams : pathname);
return (
,
VariantProps {
asChild?: boolean;
+ loading?: boolean;
}
const Button = React.forwardRef(
- ({ className, variant, size, asChild = false, ...props }, ref) => {
+ ({ className, variant, size, asChild = false, loading, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
);
- }
+ },
);
Button.displayName = "Button";
diff --git a/dashboard/data/flow.ts b/dashboard/data/flow.ts
index dd013401f..50df7cdc2 100644
--- a/dashboard/data/flow.ts
+++ b/dashboard/data/flow.ts
@@ -44,8 +44,8 @@ export const getWorkflowsByBotId = async (bot_id: string, page: number = 1) => {
`/get/b/${bot_id}?page=${page}`,
);
};
-export const getWorkflowById = (id: string) => {
- return instance.get(`/${id}`);
+export const getWorkflowById = async (id: string) => {
+ return await instance.get(`/${id}`);
};
export const createWorkflowFromSwagger = (
diff --git a/dashboard/package.json b/dashboard/package.json
index ab2efee57..3cc94f2a2 100644
--- a/dashboard/package.json
+++ b/dashboard/package.json
@@ -11,7 +11,6 @@
},
"dependencies": {
"@hookform/resolvers": "^3.3.1",
- "@openchatai/copilot-flows-editor": "^1.5.2",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-avatar": "^1.0.4",
@@ -48,6 +47,7 @@
"react-syntax-highlighter": "^15.5.0",
"react-textarea-autosize": "^8.5.3",
"react-use-wizard": "^2.2.3",
+ "reactflow": "^11.9.4",
"swr": "^2.2.4",
"tailwind-merge": "^1.14.0",
"tailwindcss-animate": "^1.0.7",
diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml
index 2ee8f21dc..047bcc208 100644
--- a/dashboard/pnpm-lock.yaml
+++ b/dashboard/pnpm-lock.yaml
@@ -8,9 +8,6 @@ dependencies:
'@hookform/resolvers':
specifier: ^3.3.1
version: 3.3.2(react-hook-form@7.47.0)
- '@openchatai/copilot-flows-editor':
- specifier: ^1.5.2
- version: 1.5.2(@radix-ui/react-alert-dialog@1.0.5)(@radix-ui/react-dialog@1.0.5)(@radix-ui/react-tooltip@1.0.7)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-accordion':
specifier: ^1.1.2
version: 1.1.2(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0)
@@ -119,6 +116,9 @@ dependencies:
react-use-wizard:
specifier: ^2.2.3
version: 2.2.3(react-dom@18.2.0)(react@18.2.0)
+ reactflow:
+ specifier: ^11.9.4
+ version: 11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
swr:
specifier: ^2.2.4
version: 2.2.4(react@18.2.0)
@@ -420,22 +420,6 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
- /@openchatai/copilot-flows-editor@1.5.2(@radix-ui/react-alert-dialog@1.0.5)(@radix-ui/react-dialog@1.0.5)(@radix-ui/react-tooltip@1.0.7)(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-LxjfBQzlkWP5zP5s9XnBlnTVL2gJpHY4ZoEUk9K3R9wDWyH91DqW+z8g76q4m/MP3ydGXc2IJ/R7IE/FchmSUw==}
- peerDependencies:
- '@radix-ui/react-alert-dialog': ^1.0.4
- '@radix-ui/react-dialog': ^1.0.4
- '@radix-ui/react-tooltip': ^1.0.6
- react: ^18.x
- react-dom: ^18.x
- dependencies:
- '@radix-ui/react-alert-dialog': 1.0.5(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0)
- '@radix-ui/react-dialog': 1.0.5(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0)
- '@radix-ui/react-tooltip': 1.0.7(@types/react-dom@18.2.13)(@types/react@18.2.28)(react-dom@18.2.0)(react@18.2.0)
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
- dev: false
-
/@radix-ui/number@1.0.1:
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
dependencies:
@@ -1651,6 +1635,114 @@ packages:
'@babel/runtime': 7.23.2
dev: false
+ /@reactflow/background@11.3.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-bgwvqWxF09chwmdkyClpYEMaewBspdwjgLbbFlLf4SpWPFMYyuvCBQrcISsvy/EDEWO9i3Uj9ktgGAhvtSQsmA==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ classcat: 5.0.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.6(@types/react@18.2.28)(immer@10.0.3)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/controls@11.2.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-x6e5p9iHjC6gd+4SoZ3DOOp0F1MefGKQ8hT6yPVdqxfo1+rV2WhrWvrX/MCoEu12Dp7457LdLfa0giy3aho8tQ==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ classcat: 5.0.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.6(@types/react@18.2.28)(immer@10.0.3)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/core@11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-Ko7nKPOYalwDTTbRHi2+QXDiidSAcpUzGN3G+0B+QysLZkcaPCkpkMjjHiDC4c/Z1BJBzs1FRJg/T6BXaBnYkg==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@types/d3': 7.4.2
+ '@types/d3-drag': 3.0.5
+ '@types/d3-selection': 3.0.8
+ '@types/d3-zoom': 3.0.6
+ classcat: 5.0.4
+ d3-drag: 3.0.0
+ d3-selection: 3.0.0
+ d3-zoom: 3.0.0
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.6(@types/react@18.2.28)(immer@10.0.3)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/minimap@11.7.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-Jo1R+uDey9IV7O2s3m0gK2+cZpg9M8hq2EZJb3NGfOSzMAPhj3mby0fNJIgTzycreuht0TpA51c2YfjGI3YIOw==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ '@types/d3-selection': 3.0.8
+ '@types/d3-zoom': 3.0.6
+ classcat: 5.0.4
+ d3-selection: 3.0.0
+ d3-zoom: 3.0.0
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.6(@types/react@18.2.28)(immer@10.0.3)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/node-resizer@2.2.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-+p271/hAsM5M1+RQTWW/02pbNkCHeGXwxGimIlL1tMIagyuko0NX2vOz2B8jxJnPKlF09Wj18BcXBNUm3nDcSg==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ classcat: 5.0.4
+ d3-drag: 3.0.0
+ d3-selection: 3.0.0
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.6(@types/react@18.2.28)(immer@10.0.3)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
+ /@reactflow/node-toolbar@1.3.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-TfcmpXHRBb2mUfzKGjburiU6FWqRME9pPFs1OwIC1z5e9BjupQhNDEKEk8XHi7PKL/mAiDfwuGXaM1BVVFuPqw==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/core': 11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ classcat: 5.0.4
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ zustand: 4.4.6(@types/react@18.2.28)(immer@10.0.3)(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
/@rushstack/eslint-patch@1.5.1:
resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==}
dev: true
@@ -1661,6 +1753,189 @@ packages:
tslib: 2.6.2
dev: false
+ /@types/d3-array@3.2.0:
+ resolution: {integrity: sha512-tjU8juPSfhMnu6mJZPOCVVGba4rZoE0tjHDPb81PYwA8CzbaFscGjgkUM7juUJu6iWA1cCVWNEVwxZ5HN9Jj8Q==}
+ dev: false
+
+ /@types/d3-axis@3.0.5:
+ resolution: {integrity: sha512-ufDAV3SQzju+uB3Jlty7SUb/jMigjpIlvDDcSGvGmmO6OT/sNO93UE0dRzwWOZeBLzrLSA0CQM4bf3iq1std3A==}
+ dependencies:
+ '@types/d3-selection': 3.0.8
+ dev: false
+
+ /@types/d3-brush@3.0.5:
+ resolution: {integrity: sha512-JROQXZNq1X6QdWstESDUv1VilwZ2hBCQnWB91yal+5yZvYwGQvYsGCjrkHGfKK/8/AcX1JnERmpQzdDDuLRUsA==}
+ dependencies:
+ '@types/d3-selection': 3.0.8
+ dev: false
+
+ /@types/d3-chord@3.0.5:
+ resolution: {integrity: sha512-rs26AIhJjtc+XLR4YQU8IjPTLOlDVO4PR1y+pVFYEHzKh2tE5tYz3MF4QV6iz7HboXQEaYpJQt8dH9uUkne8yA==}
+ dev: false
+
+ /@types/d3-color@3.1.2:
+ resolution: {integrity: sha512-At+Ski7dL8Bs58E8g8vPcFJc8tGcaC12Z4m07+p41+DRqnZQcAlp3NfYjLrhNYv+zEyQitU1CUxXNjqUyf+c0g==}
+ dev: false
+
+ /@types/d3-contour@3.0.5:
+ resolution: {integrity: sha512-wLvjwdOQVd1NL1IcW90CCt1VtpeZ3V20p/OTXlkT8uAiprrJnq2PNNnRNe1QCez4U9aMU29Z14zpJQVLW1+Lcg==}
+ dependencies:
+ '@types/d3-array': 3.2.0
+ '@types/geojson': 7946.0.12
+ dev: false
+
+ /@types/d3-delaunay@6.0.3:
+ resolution: {integrity: sha512-+Lf5NPKZ4JBC9tbudVkKceQXRxU3jJs0el9aKQvinMtdnFSOG84eVXyhCNgIFuXNQO3iIcYs7sgzN359FEOZnQ==}
+ dev: false
+
+ /@types/d3-dispatch@3.0.5:
+ resolution: {integrity: sha512-hxvq2kc+9hydVppo21JCGfcM0tLTh1DXnG3MLN0KlxsNZJH4bsdl1iXDuWtXFpWWlBrCMwSqlnoLPDxNAZU3Bg==}
+ dev: false
+
+ /@types/d3-drag@3.0.5:
+ resolution: {integrity: sha512-arHyAGvO0NEGGPCU2jTb31TlXeSxwty1bIxr5wOFOCVqVjgriXloLWXoRp39Oa0Y/qXxcAVMIonAWLrtLxUZAQ==}
+ dependencies:
+ '@types/d3-selection': 3.0.8
+ dev: false
+
+ /@types/d3-dsv@3.0.5:
+ resolution: {integrity: sha512-73WZR3QFOaSRVz9iOrebTbTnbo7xjcgS/i0Cq5zy0jMXPO3v/JbkTD3Zqii1eYE6v4EJ78g5VP407rm+p8fdlA==}
+ dev: false
+
+ /@types/d3-ease@3.0.1:
+ resolution: {integrity: sha512-VZofjpEt8HWv3nxUAosj5o/+4JflnJ7Bbv07k17VO3T2WRuzGdZeookfaF60iVh5RdhVG49LE5w6LIshVUC6rg==}
+ dev: false
+
+ /@types/d3-fetch@3.0.5:
+ resolution: {integrity: sha512-Rc8pb6H0RRLpAV2hEXduykUgcDUOhjSLTLmCIeo6ejzgs4SaITh/EteMb3p5Env3Hqjsqw0fCksyqopHHzMkMg==}
+ dependencies:
+ '@types/d3-dsv': 3.0.5
+ dev: false
+
+ /@types/d3-force@3.0.7:
+ resolution: {integrity: sha512-rsok4CEvPLyVWRPsFiBhanJc3up03H/EARVz4d8soPh8drv82YMuAckYy4yv8g4/81JwCng5U5/o9aj9d0T6bQ==}
+ dev: false
+
+ /@types/d3-format@3.0.3:
+ resolution: {integrity: sha512-kxuLXSAEJykTeL/EI3tUiEfGqru7PRdqEy099YBnqFl+fF167UVSB4+wntlZv86ZdoYf0DHjsRHnTIm8kcH7qw==}
+ dev: false
+
+ /@types/d3-geo@3.0.6:
+ resolution: {integrity: sha512-wblAES3b+C3hvp4VakwECEKtHquT/xc6K4HOna95LM1j1fd7s7WmU4V+JMQZfKhNCMkV2vWD+ZUgY2Uj6gqfuA==}
+ dependencies:
+ '@types/geojson': 7946.0.12
+ dev: false
+
+ /@types/d3-hierarchy@3.1.5:
+ resolution: {integrity: sha512-DEcBUj1IL3WyPLDlh4m2nsNXnMLITXM5Vwcu4G85yJHtf2cVGPBjgky3L11WBnT+ayHKf06Tchk5mY1eGmd4WQ==}
+ dev: false
+
+ /@types/d3-interpolate@3.0.3:
+ resolution: {integrity: sha512-6OZ2EIB4lLj+8cUY7I/Cgn9Q+hLdA4DjJHYOQDiHL0SzqS1K9DL5xIOVBSIHgF+tiuO9MU1D36qvdIvRDRPh+Q==}
+ dependencies:
+ '@types/d3-color': 3.1.2
+ dev: false
+
+ /@types/d3-path@3.0.1:
+ resolution: {integrity: sha512-blRhp7ki7pVznM8k6lk5iUU9paDbVRVq+/xpf0RRgSJn5gr6SE7RcFtxooYGMBOc1RZiGyqRpVdu5AD0z0ooMA==}
+ dev: false
+
+ /@types/d3-polygon@3.0.1:
+ resolution: {integrity: sha512-nrcWPk7B9qs6xnpq60Cls44zm9eDmFAv65qi/N/emh/oftnG6uYz49aIS0mdFaGeJxVN8H3pHneMuZMV8EwFdw==}
+ dev: false
+
+ /@types/d3-quadtree@3.0.4:
+ resolution: {integrity: sha512-B725MopFDIOQ6njFbeOxIEf42HVO2Xv+FmcxQISdOKErvLbFqWz3Riu+OWujUYoogreqqyHBHcGGL/JzzXQYsw==}
+ dev: false
+
+ /@types/d3-random@3.0.2:
+ resolution: {integrity: sha512-8QhsqkKs6mymAZMrg3ZFXPxKA34rdgp3ZrtB8o6mhFsKAd1gOvR1gocWnca+kmXypQdwgnzKm9gZE2Uw8NjjKw==}
+ dev: false
+
+ /@types/d3-scale-chromatic@3.0.1:
+ resolution: {integrity: sha512-Ob7OrwiTeQXY/WBBbRHGZBOn6rH1h7y3jjpTSKYqDEeqFjktql6k2XSgNwLrLDmAsXhEn8P9NHDY4VTuo0ZY1w==}
+ dev: false
+
+ /@types/d3-scale@4.0.6:
+ resolution: {integrity: sha512-lo3oMLSiqsQUovv8j15X4BNEDOsnHuGjeVg7GRbAuB2PUa1prK5BNSOu6xixgNf3nqxPl4I1BqJWrPvFGlQoGQ==}
+ dependencies:
+ '@types/d3-time': 3.0.2
+ dev: false
+
+ /@types/d3-selection@3.0.8:
+ resolution: {integrity: sha512-pxCZUfQyedq/DIlPXIR5wE1mIH37omOdx1yxRudL3KZ4AC+156jMjOv1z5RVlGq62f8WX2kyO0hTVgEx627QFg==}
+ dev: false
+
+ /@types/d3-shape@3.1.4:
+ resolution: {integrity: sha512-M2/xsWPsjaZc5ifMKp1EBp0gqJG0eO/zlldJNOC85Y/5DGsBQ49gDkRJ2h5GY7ZVD6KUumvZWsylSbvTaJTqKg==}
+ dependencies:
+ '@types/d3-path': 3.0.1
+ dev: false
+
+ /@types/d3-time-format@4.0.2:
+ resolution: {integrity: sha512-wr08C1Gh77qaN8JIkrn5Rz/bdt5M9bdEqFmEOcYhUSq2t2sHvLTBfb4XAtGB3D4hm0ubj50NXWWXoXyp5tPXDg==}
+ dev: false
+
+ /@types/d3-time@3.0.2:
+ resolution: {integrity: sha512-kbdRXTmUgNfw5OTE3KZnFQn6XdIc4QGroN5UixgdrXATmYsdlPQS6pEut9tVlIojtzuFD4txs/L+Rq41AHtLpg==}
+ dev: false
+
+ /@types/d3-timer@3.0.1:
+ resolution: {integrity: sha512-GGTvzKccVEhxmRfJEB6zhY9ieT4UhGVUIQaBzFpUO9OXy2ycAlnPCSJLzmGGgqt3KVjqN3QCQB4g1rsZnHsWhg==}
+ dev: false
+
+ /@types/d3-transition@3.0.6:
+ resolution: {integrity: sha512-K0To23B5UxNwFtKORnS5JoNYvw/DnknU5MzhHIS9czJ/lTqFFDeU6w9lArOdoTl0cZFNdNrMJSFCbRCEHccH2w==}
+ dependencies:
+ '@types/d3-selection': 3.0.8
+ dev: false
+
+ /@types/d3-zoom@3.0.6:
+ resolution: {integrity: sha512-dGZQaXEu7aNcCL71LPpjB58IjoQNM9oDPfQuMUJ7N/fbkcIWGX2PnmUWO1jPJ+RLbZBpRUggJUX8twKRvo2hKQ==}
+ dependencies:
+ '@types/d3-interpolate': 3.0.3
+ '@types/d3-selection': 3.0.8
+ dev: false
+
+ /@types/d3@7.4.2:
+ resolution: {integrity: sha512-Y4g2Yb30ZJmmtqAJTqMRaqXwRawfvpdpVmyEYEcyGNhrQI/Zvkq3k7yE1tdN07aFSmNBfvmegMQ9Fe2qy9ZMhw==}
+ dependencies:
+ '@types/d3-array': 3.2.0
+ '@types/d3-axis': 3.0.5
+ '@types/d3-brush': 3.0.5
+ '@types/d3-chord': 3.0.5
+ '@types/d3-color': 3.1.2
+ '@types/d3-contour': 3.0.5
+ '@types/d3-delaunay': 6.0.3
+ '@types/d3-dispatch': 3.0.5
+ '@types/d3-drag': 3.0.5
+ '@types/d3-dsv': 3.0.5
+ '@types/d3-ease': 3.0.1
+ '@types/d3-fetch': 3.0.5
+ '@types/d3-force': 3.0.7
+ '@types/d3-format': 3.0.3
+ '@types/d3-geo': 3.0.6
+ '@types/d3-hierarchy': 3.1.5
+ '@types/d3-interpolate': 3.0.3
+ '@types/d3-path': 3.0.1
+ '@types/d3-polygon': 3.0.1
+ '@types/d3-quadtree': 3.0.4
+ '@types/d3-random': 3.0.2
+ '@types/d3-scale': 4.0.6
+ '@types/d3-scale-chromatic': 3.0.1
+ '@types/d3-selection': 3.0.8
+ '@types/d3-shape': 3.1.4
+ '@types/d3-time': 3.0.2
+ '@types/d3-time-format': 4.0.2
+ '@types/d3-timer': 3.0.1
+ '@types/d3-transition': 3.0.6
+ '@types/d3-zoom': 3.0.6
+ dev: false
+
+ /@types/geojson@7946.0.12:
+ resolution: {integrity: sha512-uK2z1ZHJyC0nQRbuovXFt4mzXDwf27vQeUWNhfKGwRcWW429GOhP8HxUHlM6TLH4bzmlv/HlEjpvJh3JfmGsAA==}
+ dev: false
+
/@types/hast@2.3.6:
resolution: {integrity: sha512-47rJE80oqPmFdVDCD7IheXBrVdwuBgsYwoczFvKmwfo2Mzsnt+V9OONsYauFmICb6lQPpCuXYJWejBNs4pDJRg==}
dependencies:
@@ -2076,6 +2351,10 @@ packages:
clsx: 2.0.0
dev: false
+ /classcat@5.0.4:
+ resolution: {integrity: sha512-sbpkOw6z413p+HDGcBENe498WM9woqWHiJxCq7nvmxe9WmrUmqfAcxpIwAiMtM5Q3AhYkzXcNQHqsWq0mND51g==}
+ dev: false
+
/client-only@0.0.1:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
dev: false
@@ -2149,6 +2428,71 @@ packages:
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
+ /d3-color@3.1.0:
+ resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-dispatch@3.0.1:
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-drag@3.0.0:
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-selection: 3.0.0
+ dev: false
+
+ /d3-ease@3.0.1:
+ resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-interpolate@3.0.1:
+ resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-color: 3.1.0
+ dev: false
+
+ /d3-selection@3.0.0:
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-timer@3.0.1:
+ resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+ engines: {node: '>=12'}
+ dev: false
+
+ /d3-transition@3.0.1(d3-selection@3.0.0):
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
+ engines: {node: '>=12'}
+ peerDependencies:
+ d3-selection: 2 - 3
+ dependencies:
+ d3-color: 3.1.0
+ d3-dispatch: 3.0.1
+ d3-ease: 3.0.1
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-timer: 3.0.1
+ dev: false
+
+ /d3-zoom@3.0.0:
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
+ engines: {node: '>=12'}
+ dependencies:
+ d3-dispatch: 3.0.1
+ d3-drag: 3.0.0
+ d3-interpolate: 3.0.1
+ d3-selection: 3.0.0
+ d3-transition: 3.0.1(d3-selection@3.0.0)
+ dev: false
+
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
@@ -3892,6 +4236,25 @@ packages:
loose-envify: 1.4.0
dev: false
+ /reactflow@11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-IHAKBkJngNvU9y1vZ5Nw9rvA3Z+zc9geTgQQIi9qq9Y9knGLlDDr9KfsjbFMew9AycAAgVg8TvBEakF4IT5lqg==}
+ peerDependencies:
+ react: '>=17'
+ react-dom: '>=17'
+ dependencies:
+ '@reactflow/background': 11.3.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/controls': 11.2.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/core': 11.9.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/minimap': 11.7.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/node-resizer': 2.2.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ '@reactflow/node-toolbar': 1.3.4(@types/react@18.2.28)(immer@10.0.3)(react-dom@18.2.0)(react@18.2.0)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ transitivePeerDependencies:
+ - '@types/react'
+ - immer
+ dev: false
+
/read-cache@1.0.0:
resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==}
dependencies:
@@ -4520,3 +4883,24 @@ packages:
/zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: false
+
+ /zustand@4.4.6(@types/react@18.2.28)(immer@10.0.3)(react@18.2.0):
+ resolution: {integrity: sha512-Rb16eW55gqL4W2XZpJh0fnrATxYEG3Apl2gfHTyDSE965x/zxslTikpNch0JgNjJA9zK6gEFW8Fl6d1rTZaqgg==}
+ engines: {node: '>=12.7.0'}
+ peerDependencies:
+ '@types/react': '>=16.8'
+ immer: '>=9.0'
+ react: '>=16.8'
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ immer:
+ optional: true
+ react:
+ optional: true
+ dependencies:
+ '@types/react': 18.2.28
+ immer: 10.0.3
+ react: 18.2.0
+ use-sync-external-store: 1.2.0(react@18.2.0)
+ dev: false
diff --git a/nginx/nginx.conf b/nginx/nginx.conf
index e6b028bf1..d80dbf9ba 100644
--- a/nginx/nginx.conf
+++ b/nginx/nginx.conf
@@ -9,7 +9,7 @@ http {
listen 80;
location /backend/flows/ {
add_header 'Access-Control-Allow-Origin' '*';
- add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+ add_header 'Access-Control-Allow-Methods' '*';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
rewrite /backend/flows/(.*) /workflow/$1 break;
@@ -18,7 +18,7 @@ http {
location /backend/swagger_api/ {
add_header 'Access-Control-Allow-Origin' '*';
- add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+ add_header 'Access-Control-Allow-Methods' '*';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
rewrite /backend/swagger_api/(.*) /swagger_api/$1 break;