refac: langflow_backend -> langflow
This commit is contained in:
parent
aa8e1aee8a
commit
70dbc7eb1e
104 changed files with 174 additions and 50 deletions
42
src/frontend/src/App.css
Normal file
42
src/frontend/src/App.css
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
132
src/frontend/src/App.tsx
Normal file
132
src/frontend/src/App.tsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import "reactflow/dist/style.css";
|
||||
import { useState, useEffect, useContext } from "react";
|
||||
import "./App.css";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import ErrorAlert from "./alerts/error";
|
||||
import NoticeAlert from "./alerts/notice";
|
||||
import SuccessAlert from "./alerts/success";
|
||||
import ExtraSidebar from "./components/ExtraSidebarComponent";
|
||||
import { alertContext } from "./contexts/alertContext";
|
||||
import { locationContext } from "./contexts/locationContext";
|
||||
import TabsManagerComponent from "./pages/FlowPage/components/tabsManagerComponent";
|
||||
|
||||
export default function App() {
|
||||
var _ = require("lodash");
|
||||
|
||||
let { setCurrent, setShowSideBar, setIsStackedOpen } =
|
||||
useContext(locationContext);
|
||||
let location = useLocation();
|
||||
useEffect(() => {
|
||||
setCurrent(location.pathname.replace(/\/$/g, "").split("/"));
|
||||
setShowSideBar(true);
|
||||
setIsStackedOpen(true);
|
||||
}, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]);
|
||||
|
||||
const {
|
||||
errorData,
|
||||
errorOpen,
|
||||
setErrorOpen,
|
||||
noticeData,
|
||||
noticeOpen,
|
||||
setNoticeOpen,
|
||||
successData,
|
||||
successOpen,
|
||||
setSuccessOpen,
|
||||
} = useContext(alertContext);
|
||||
|
||||
// Initialize state variable for the list of alerts
|
||||
const [alertsList, setAlertsList] = useState<Array<{type:string,data:{title:string,list?:Array<string>,link?:string},id:string}>>([]);
|
||||
|
||||
// Use effect hook to update alertsList when a new alert is added
|
||||
useEffect(() => {
|
||||
// If there is an error alert open with data, add it to the alertsList
|
||||
if (errorOpen && errorData) {
|
||||
setErrorOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "error", data: _.cloneDeep(errorData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a notice alert open with data, add it to the alertsList
|
||||
else if (noticeOpen && noticeData) {
|
||||
setNoticeOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "notice", data: _.cloneDeep(noticeData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
// If there is a success alert open with data, add it to the alertsList
|
||||
else if (successOpen && successData) {
|
||||
setSuccessOpen(false);
|
||||
setAlertsList((old) => {
|
||||
let newAlertsList = [
|
||||
...old,
|
||||
{ type: "success", data: _.cloneDeep(successData), id: _.uniqueId() },
|
||||
];
|
||||
return newAlertsList;
|
||||
});
|
||||
}
|
||||
}, [_, errorData, errorOpen, noticeData, noticeOpen, setErrorOpen, setNoticeOpen, setSuccessOpen, successData, successOpen]);
|
||||
|
||||
const removeAlert = (id: string) => {
|
||||
setAlertsList((prevAlertsList) =>
|
||||
prevAlertsList.filter((alert) => alert.id !== id)
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
//need parent component with width and height
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex grow-0 shrink basis-auto">
|
||||
</div>
|
||||
<div className="flex grow shrink basis-auto min-h-0 flex-1 overflow-hidden">
|
||||
<ExtraSidebar />
|
||||
{/* Main area */}
|
||||
<main className="min-w-0 flex-1 border-t border-gray-200 dark:border-gray-700 flex">
|
||||
{/* Primary column */}
|
||||
<div className="w-full h-full">
|
||||
<TabsManagerComponent></TabsManagerComponent>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<div></div>
|
||||
<div className="flex z-40 flex-col-reverse fixed bottom-5 left-5">
|
||||
{alertsList.map((alert) => (
|
||||
<div key={alert.id}>
|
||||
{alert.type === "error" ? (
|
||||
<ErrorAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
list={alert.data.list}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : alert.type === "notice" ? (
|
||||
<NoticeAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
link={alert.data.link}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
) : (
|
||||
<SuccessAlert
|
||||
key={alert.id}
|
||||
title={alert.data.title}
|
||||
id={alert.id}
|
||||
removeAlert={removeAlert}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<a target={"_blank"} href="https://logspace.ai/" className="absolute bottom-1 left-1 text-gray-500 text-xs cursor-pointer font-sans tracking-wide">Created by Logspace</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import { Handle, Position, useUpdateNodeInternals } from "reactflow";
|
||||
import Tooltip from "../../../../components/TooltipComponent";
|
||||
import { classNames, isValidConnection } from "../../../../utils";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import InputComponent from "../../../../components/inputComponent";
|
||||
import ToggleComponent from "../../../../components/toggleComponent";
|
||||
import InputListComponent from "../../../../components/inputListComponent";
|
||||
import TextAreaComponent from "../../../../components/textAreaComponent";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { ParameterComponentType } from "../../../../types/components";
|
||||
import FloatComponent from "../../../../components/floatComponent";
|
||||
|
||||
export default function ParameterComponent({
|
||||
left,
|
||||
id,
|
||||
data,
|
||||
tooltipTitle,
|
||||
title,
|
||||
color,
|
||||
type,
|
||||
name = "",
|
||||
required = false,
|
||||
}: ParameterComponentType) {
|
||||
const ref = useRef(null);
|
||||
const updateNodeInternals = useUpdateNodeInternals();
|
||||
const [position, setPosition] = useState(0);
|
||||
useEffect(() => {
|
||||
if (ref.current && ref.current.offsetTop && ref.current.clientHeight) {
|
||||
setPosition(ref.current.offsetTop + ref.current.clientHeight / 2);
|
||||
updateNodeInternals(data.id);
|
||||
}
|
||||
}, [data.id, ref, updateNodeInternals]);
|
||||
|
||||
useEffect(() => {
|
||||
updateNodeInternals(data.id);
|
||||
}, [data.id, position, updateNodeInternals]);
|
||||
|
||||
const [enabled, setEnabled] = useState(
|
||||
data.node.template[name]?.value ?? false
|
||||
);
|
||||
const { reactFlowInstance } = useContext(typesContext);
|
||||
let disabled =
|
||||
reactFlowInstance?.getEdges().some((e) => e.targetHandle === id) ?? false;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="w-full flex flex-wrap justify-between items-center bg-gray-50 dark:bg-gray-800 dark:text-white mt-1 px-5 py-2"
|
||||
>
|
||||
<>
|
||||
<div className="text-sm truncate">
|
||||
{title}
|
||||
<span className="text-red-600">{required ? " *" : ""}</span>
|
||||
</div>
|
||||
{left && (type === "str" || type === "bool" || type === "float") ?
|
||||
<></>
|
||||
:
|
||||
<Tooltip title={tooltipTitle + (required ? " (required)" : "")}>
|
||||
<Handle
|
||||
type={left ? "target" : "source"}
|
||||
position={left ? Position.Left : Position.Right}
|
||||
id={id}
|
||||
isValidConnection={(connection) =>
|
||||
isValidConnection(connection, reactFlowInstance)
|
||||
}
|
||||
className={classNames(
|
||||
left ? "-ml-0.5 " : "-mr-0.5 ",
|
||||
"w-3 h-3 rounded-full border-2 bg-white dark:bg-gray-800"
|
||||
)}
|
||||
style={{
|
||||
borderColor: color,
|
||||
top: position,
|
||||
}}
|
||||
></Handle>
|
||||
</Tooltip>
|
||||
}
|
||||
|
||||
{left === true && type === "str" ? (
|
||||
<div className="mt-2 w-full">
|
||||
{data.node.template[name].list ? (
|
||||
<InputListComponent
|
||||
disabled={disabled}
|
||||
value={
|
||||
!data.node.template[name].value ||
|
||||
data.node.template[name].value === ""
|
||||
? [""]
|
||||
: data.node.template[name].value
|
||||
}
|
||||
onChange={(t: string[]) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
) : data.node.template[name].multiline ? (
|
||||
<TextAreaComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t: string) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<InputComponent
|
||||
disabled={disabled}
|
||||
password={data.node.template[name].password ?? true}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : left === true && type === "bool" ? (
|
||||
<div className="mt-2">
|
||||
<ToggleComponent
|
||||
disabled={disabled}
|
||||
enabled={enabled}
|
||||
setEnabled={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
setEnabled(t);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : left === true && type === "float" ? (
|
||||
<FloatComponent
|
||||
disabled={disabled}
|
||||
value={data.node.template[name].value ?? ""}
|
||||
onChange={(t) => {
|
||||
data.node.template[name].value = t;
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
92
src/frontend/src/CustomNodes/GenericNode/index.tsx
Normal file
92
src/frontend/src/CustomNodes/GenericNode/index.tsx
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import {
|
||||
TrashIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import {
|
||||
classNames,
|
||||
nodeColors,
|
||||
nodeIcons,
|
||||
snakeToNormalCase,
|
||||
} from "../../utils";
|
||||
import ParameterComponent from "./components/parameterComponent";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import { useContext } from "react";
|
||||
import { NodeDataType} from "../../types/flow";
|
||||
|
||||
export default function GenericNode({ data, selected}:{data:NodeDataType,selected:boolean}) {
|
||||
const {types, deleteNode} = useContext(typesContext);
|
||||
const Icon = nodeIcons[types[data.type]];
|
||||
|
||||
|
||||
return (
|
||||
<div className={ classNames(selected?"border border-blue-500":"border dark:border-gray-700","prompt-node relative bg-white dark:bg-gray-900 w-96 rounded-lg flex flex-col justify-center")}>
|
||||
<div className="w-full dark:text-white flex items-center justify-between p-4 gap-8 bg-gray-50 rounded-t-lg dark:bg-gray-800 border-b dark:border-b-gray-700 ">
|
||||
<div className="w-full flex items-center truncate gap-4 text-lg">
|
||||
<Icon
|
||||
className="w-10 h-10 p-1 rounded"
|
||||
style={{ color: nodeColors[types[data.type]] }}
|
||||
/>
|
||||
<div className="truncate">{data.type}</div>
|
||||
</div>
|
||||
<button onClick={() => {deleteNode(data.id)}}>
|
||||
<TrashIcon className="w-6 h-6 hover:text-red-500 dark:text-gray-500 dark:hover:text-red-500"></TrashIcon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full py-5">
|
||||
<div className="w-full text-gray-500 px-5 text-sm">
|
||||
{data.node.description}
|
||||
</div>
|
||||
|
||||
<>
|
||||
{Object.keys(data.node.template)
|
||||
.filter((t) => t.charAt(0) !== "_")
|
||||
.map((t:string, idx) => (
|
||||
<div key={idx}>
|
||||
{idx === 0 ? (
|
||||
<div className="px-5 py-2 mt-2 dark:text-white text-center">Inputs:</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{data.node.template[t].show ? (
|
||||
<ParameterComponent
|
||||
|
||||
data={data}
|
||||
color={
|
||||
nodeColors[types[data.node.template[t].type]] ??
|
||||
nodeColors[types[data.node.template[t].type]] ??
|
||||
"black"
|
||||
}
|
||||
title={
|
||||
snakeToNormalCase(t)
|
||||
}
|
||||
name={t}
|
||||
tooltipTitle={
|
||||
"Type: " +
|
||||
data.node.template[t].type +
|
||||
(data.node.template[t].list ? " list" : "")
|
||||
}
|
||||
required={data.node.template[t].required}
|
||||
id={data.node.template[t].type + "|" + t + "|" + data.id}
|
||||
left={true}
|
||||
type={data.node.template[t].type}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="px-5 py-2 mt-2 dark:text-white text-center">Output:</div>
|
||||
<ParameterComponent
|
||||
data={data}
|
||||
color={nodeColors[types[data.type]]}
|
||||
title={data.type}
|
||||
tooltipTitle={"Type: str"}
|
||||
id={data.type + "|" + data.id + data.node.base_classes.map((b) => ("|" + b))}
|
||||
type={'str'}
|
||||
left={false}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import { XCircleIcon, XMarkIcon, InformationCircleIcon, CheckCircleIcon } from "@heroicons/react/24/outline";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Transition } from "@headlessui/react";
|
||||
import { useState } from "react";
|
||||
import { SingleAlertComponentType } from "../../../../types/alerts";
|
||||
|
||||
export default function SingleAlert({ dropItem, removeAlert}:SingleAlertComponentType) {
|
||||
const [show, setShow] = useState(true);
|
||||
const type = dropItem.type;
|
||||
console.log(dropItem.id)
|
||||
|
||||
return (
|
||||
<Transition
|
||||
className="relative"
|
||||
show={show}
|
||||
appear={true}
|
||||
enter="transition-transform duration-500 ease-out"
|
||||
enterFrom={"transform translate-x-[-100%]"}
|
||||
enterTo={"transform translate-x-0"}
|
||||
leave="transition-transform duration-500 ease-in"
|
||||
leaveFrom={"transform translate-x-0"}
|
||||
leaveTo={"transform translate-x-[-100%]"}
|
||||
>
|
||||
{type === "error"?
|
||||
<div className="flex bg-red-50 rounded-md p-3 mb-2 mx-2" key={dropItem.id}>
|
||||
<div className="flex-shrink-0">
|
||||
<XCircleIcon
|
||||
className="h-5 w-5 text-red-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">
|
||||
{dropItem.title}
|
||||
</h3>
|
||||
{dropItem.list ? (
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<ul className="list-disc space-y-1 pl-5">
|
||||
{dropItem.list.map((item, idx) => (
|
||||
<li key={idx}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-auto pl-3">
|
||||
<div className="-mx-1.5 -my-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
|
||||
}}
|
||||
className="inline-flex rounded-md bg-red-50 p-1.5 text-red-500"
|
||||
>
|
||||
<span className="sr-only">Dismiss</span>
|
||||
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:(type === "notice" ?
|
||||
<div className="flex rounded-md bg-blue-50 p-3 mb-2 mx-2" key={dropItem.id}>
|
||||
<div className="flex-shrink-0">
|
||||
<InformationCircleIcon
|
||||
className="h-5 w-5 text-blue-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm text-blue-700">{dropItem.title}</p>
|
||||
<p className="mt-3 text-sm md:mt-0 md:ml-6">
|
||||
{dropItem.link ? (
|
||||
<Link
|
||||
to={dropItem.link}
|
||||
className="whitespace-nowrap font-medium text-blue-700 hover:text-blue-600"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto pl-3">
|
||||
<div className="-mx-1.5 -my-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
|
||||
}}
|
||||
className="inline-flex rounded-md bg-blue-50 p-1.5 text-blue-500"
|
||||
>
|
||||
<span className="sr-only">Dismiss</span>
|
||||
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
:
|
||||
<div className="flex bg-green-50 p-3 mb-2 mx-2 rounded-md" key={dropItem.id}>
|
||||
<div className="flex-shrink-0">
|
||||
<CheckCircleIcon
|
||||
className="h-5 w-5 text-green-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-green-800">
|
||||
{dropItem.title}
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto pl-3">
|
||||
<div className="-mx-1.5 -my-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShow(false); setTimeout(() => {removeAlert(dropItem.id);}, 500);
|
||||
}}
|
||||
className="inline-flex rounded-md bg-green-50 p-1.5 text-green-500"
|
||||
>
|
||||
<span className="sr-only">Dismiss</span>
|
||||
<XMarkIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
</Transition>
|
||||
)
|
||||
|
||||
}
|
||||
78
src/frontend/src/alerts/alertDropDown/index.tsx
Normal file
78
src/frontend/src/alerts/alertDropDown/index.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { useContext, useEffect, useRef } from "react";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { TrashIcon } from "@heroicons/react/24/outline";
|
||||
import SingleAlert from "./components/singleAlertComponent";
|
||||
import { AlertDropdownType } from "../../types/alerts";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
|
||||
export default function AlertDropdown({}: AlertDropdownType) {
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (
|
||||
componentRef.current &&
|
||||
!componentRef.current.contains(event.target as Node)
|
||||
) {
|
||||
console.log(event)
|
||||
closePopUp();
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the event listener
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
// Cleanup the event listener when the component is unmounted
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, [componentRef]);
|
||||
|
||||
const {
|
||||
notificationList,
|
||||
clearNotificationList,
|
||||
removeFromNotificationList,
|
||||
} = useContext(alertContext);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={componentRef}
|
||||
className="z-10 py-3 pb-4 rounded-md bg-white ring-1 ring-black ring-opacity-5 shadow-lg focus:outline-none overflow-hidden w-[16rem] h-[28rem] flex flex-col"
|
||||
>
|
||||
<div className="flex pl-3 flex-row justify-between text-md font-medium text-gray-800">
|
||||
Notifications
|
||||
<div className="flex gap-2 pr-3 ">
|
||||
<button
|
||||
className="hover:text-red-500"
|
||||
onClick={() => {
|
||||
closePopUp();
|
||||
setTimeout(clearNotificationList, 100);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="w-[1.1rem] h-[1.1rem]" />
|
||||
</button>
|
||||
<button className="hover:text-red-500" onClick={closePopUp}>
|
||||
<XMarkIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col overflow-y-scroll w-full h-full scrollbar-hide">
|
||||
{notificationList.length !== 0 ? (
|
||||
notificationList.map((alertItem, index) => (
|
||||
<SingleAlert
|
||||
key={alertItem.id}
|
||||
dropItem={alertItem}
|
||||
removeAlert={removeFromNotificationList}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="h-full w-full pb-16 text-gray-500 flex justify-center items-center">
|
||||
No new notifications
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/frontend/src/alerts/error/index.tsx
Normal file
66
src/frontend/src/alerts/error/index.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { XCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ErrorAlertType } from "../../types/alerts";
|
||||
|
||||
export default function ErrorAlert({
|
||||
title,
|
||||
list = [],
|
||||
id,
|
||||
removeAlert,
|
||||
}: ErrorAlertType) {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(id);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}, [id, removeAlert, show]);
|
||||
return (
|
||||
<Transition
|
||||
className="relative"
|
||||
show={show}
|
||||
appear={true}
|
||||
enter="transition-transform duration-500 ease-out"
|
||||
enterFrom={"transform translate-x-[-100%]"}
|
||||
enterTo={"transform translate-x-0"}
|
||||
leave="transition-transform duration-500 ease-in"
|
||||
leaveFrom={"transform translate-x-0"}
|
||||
leaveTo={"transform translate-x-[-100%]"}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(id);
|
||||
}, 500);
|
||||
}}
|
||||
className="rounded-md w-96 mt-6 shadow-xl bg-red-50 p-4 cursor-pointer"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<XCircleIcon className="h-5 w-5 text-red-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-red-800">{title}</h3>
|
||||
{list.length !== 0 ? (
|
||||
<div className="mt-2 text-sm text-red-700">
|
||||
<ul className="list-disc space-y-1 pl-5">
|
||||
{list.map((item, index) => (
|
||||
<li key={index}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
67
src/frontend/src/alerts/notice/index.tsx
Normal file
67
src/frontend/src/alerts/notice/index.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { InformationCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { NoticeAlertType } from "../../types/alerts";
|
||||
|
||||
export default function NoticeAlert({
|
||||
title,
|
||||
link = "",
|
||||
id,
|
||||
removeAlert,
|
||||
}: NoticeAlertType) {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(id);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}, [id, removeAlert, show]);
|
||||
return (
|
||||
<Transition
|
||||
show={show}
|
||||
enter="transition-transform duration-500 ease-out"
|
||||
enterFrom={"transform translate-x-[-100%]"}
|
||||
enterTo={"transform translate-x-0"}
|
||||
leave="transition-transform duration-500 ease-in"
|
||||
leaveFrom={"transform translate-x-0"}
|
||||
leaveTo={"transform translate-x-[-100%]"}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
removeAlert(id);
|
||||
}}
|
||||
className="rounded-md w-96 mt-6 shadow-xl bg-blue-50 p-4"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<InformationCircleIcon
|
||||
className="h-5 w-5 text-blue-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3 flex-1 md:flex md:justify-between">
|
||||
<p className="text-sm text-blue-700">{title}</p>
|
||||
<p className="mt-3 text-sm md:mt-0 md:ml-6">
|
||||
{link !== "" ? (
|
||||
<Link
|
||||
to={link}
|
||||
className="whitespace-nowrap font-medium text-blue-700 hover:text-blue-600"
|
||||
>
|
||||
Details
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
53
src/frontend/src/alerts/success/index.tsx
Normal file
53
src/frontend/src/alerts/success/index.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import { CheckCircleIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SuccessAlertType } from "../../types/alerts";
|
||||
|
||||
export default function SuccessAlert({
|
||||
title,
|
||||
id,
|
||||
removeAlert,
|
||||
}: SuccessAlertType) {
|
||||
const [show, setShow] = useState(true);
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setTimeout(() => {
|
||||
setShow(false);
|
||||
setTimeout(() => {
|
||||
removeAlert(id);
|
||||
}, 500);
|
||||
}, 5000);
|
||||
}
|
||||
}, [id, removeAlert, show]);
|
||||
return (
|
||||
<Transition
|
||||
show={show}
|
||||
enter="transition-transform duration-500 ease-out"
|
||||
enterFrom={"transform translate-x-[-100%]"}
|
||||
enterTo={"transform translate-x-0"}
|
||||
leave="transition-transform duration-500 ease-in"
|
||||
leaveFrom={"transform translate-x-0"}
|
||||
leaveTo={"transform translate-x-[-100%]"}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
setShow(false);
|
||||
removeAlert(id);
|
||||
}}
|
||||
className="rounded-md w-96 mt-6 shadow-xl bg-green-50 p-4"
|
||||
>
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<CheckCircleIcon
|
||||
className="h-5 w-5 text-green-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium text-green-800">{title}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
);
|
||||
}
|
||||
124
src/frontend/src/components/ExtraSidebarComponent/index.tsx
Normal file
124
src/frontend/src/components/ExtraSidebarComponent/index.tsx
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import { Disclosure } from "@headlessui/react";
|
||||
import { ChevronLeftIcon } from "@heroicons/react/24/outline";
|
||||
import { useContext } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { classNames } from "../../utils";
|
||||
import { locationContext } from "../../contexts/locationContext";
|
||||
|
||||
export default function ExtraSidebar() {
|
||||
const {
|
||||
current,
|
||||
isStackedOpen,
|
||||
setIsStackedOpen,
|
||||
extraNavigation,
|
||||
extraComponent,
|
||||
} = useContext(locationContext);
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
className={` ${
|
||||
isStackedOpen ? "w-52" : "w-0 "
|
||||
} flex-shrink-0 flex overflow-hidden flex-col border-r dark:border-r-gray-700 transition-all duration-500`}
|
||||
>
|
||||
<div className="w-52 dark:bg-gray-800 border dark:border-gray-700 overflow-y-auto scrollbar-hide h-full flex flex-col items-start">
|
||||
<div className="flex pt-1 px-4 justify-between align-middle w-full">
|
||||
<span className="text-gray-900 dark:text-white py-[2px] font-medium ">
|
||||
{extraNavigation.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-grow flex-col w-full">
|
||||
{extraNavigation.options ? (
|
||||
<div className="p-4">
|
||||
<nav className="flex-1 space-y-1">
|
||||
{extraNavigation.options.map((item) =>
|
||||
!item.children ? (
|
||||
<div key={item.name}>
|
||||
<Link
|
||||
to={item.href}
|
||||
className={classNames(
|
||||
item.href.split("/")[2] === current[4]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
"group w-full flex items-center pl-2 py-2 text-sm font-medium rounded-md"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
item.href.split("/")[2] === current[4]
|
||||
? "text-gray-500"
|
||||
: "text-gray-400 group-hover:text-gray-500",
|
||||
"mr-3 flex-shrink-0 h-6 w-6"
|
||||
)}
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<Disclosure
|
||||
as="div"
|
||||
key={item.name}
|
||||
className="space-y-1"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Disclosure.Button
|
||||
className={classNames(
|
||||
item.href.split("/")[2] === current[4]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
"group w-full flex items-center pl-2 pr-1 py-2 text-left text-sm font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className="mr-3 h-6 w-6 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="flex-1">{item.name}</span>
|
||||
<svg
|
||||
className={classNames(
|
||||
open
|
||||
? "text-gray-400 rotate-90"
|
||||
: "text-gray-300",
|
||||
"ml-3 h-5 w-5 flex-shrink-0 transition-rotate duration-150 ease-in-out group-hover:text-gray-400"
|
||||
)}
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M6 6L14 10L6 14V6Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</Disclosure.Button>
|
||||
<Disclosure.Panel className="space-y-1">
|
||||
{item.children.map((subItem) => (
|
||||
<Link
|
||||
key={subItem.name}
|
||||
to={subItem.href}
|
||||
className={classNames(
|
||||
subItem.href.split("/")[3] === current[5]
|
||||
? "bg-gray-100 text-gray-900"
|
||||
: "bg-white text-gray-600 hover:bg-gray-50 hover:text-gray-900",
|
||||
"group flex w-full items-center rounded-md py-2 pl-11 pr-2 text-sm font-medium"
|
||||
)}
|
||||
>
|
||||
{subItem.name}
|
||||
</Link>
|
||||
))}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
)
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
) : (
|
||||
extraComponent
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
17
src/frontend/src/components/LightTooltipComponent/index.tsx
Normal file
17
src/frontend/src/components/LightTooltipComponent/index.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { styled } from '@mui/material/styles';
|
||||
import Tooltip, { TooltipProps, tooltipClasses } from '@mui/material/Tooltip';
|
||||
|
||||
export const LightTooltip = styled(({ className, ...props }: TooltipProps) => (
|
||||
<Tooltip {...props} classes={{ popper: className }} />
|
||||
))(({ theme }) => ({
|
||||
[`& .${tooltipClasses.tooltip}`]: {
|
||||
backgroundColor: theme.palette.common.white,
|
||||
color: 'rgba(0, 0, 0, 0.87)',
|
||||
boxShadow: theme.shadows[2],
|
||||
fontSize: 14,
|
||||
},
|
||||
[`& .${tooltipClasses.arrow}:before`]: {
|
||||
color: theme.palette.common.white,
|
||||
boxShadow: theme.shadows[1],
|
||||
},
|
||||
}));
|
||||
6
src/frontend/src/components/TooltipComponent/index.tsx
Normal file
6
src/frontend/src/components/TooltipComponent/index.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { ReactElement } from "react";
|
||||
import { LightTooltip } from "../LightTooltipComponent";
|
||||
|
||||
export default function Tooltip({ children, title }:{children:ReactElement,title:string}) {
|
||||
return <LightTooltip title={title} arrow>{children}</LightTooltip>;
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { ChatBubbleLeftEllipsisIcon, ChatBubbleOvalLeftEllipsisIcon, PlusSmallIcon } from "@heroicons/react/24/outline";
|
||||
import { useState } from "react";
|
||||
import { ChatMessageType } from "../../../types/chat";
|
||||
import { nodeColors } from "../../../utils";
|
||||
|
||||
export default function ChatMessage({ chat }: { chat: ChatMessageType }) {
|
||||
const [hidden, setHidden] = useState(true);
|
||||
return (
|
||||
<div>
|
||||
{!chat.isSend ? (
|
||||
<div className="w-full text-start">
|
||||
<div
|
||||
style={{ backgroundColor: nodeColors["chat"] }}
|
||||
className=" relative text-start inline-block text-white rounded-xl overflow-hidden w-fit max-w-[280px] text-sm font-normal rounded-tl-none"
|
||||
>
|
||||
{hidden && chat.thought && chat.thought !== "" && (
|
||||
<div
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
className="absolute top-2 right-2 cursor-pointer"
|
||||
>
|
||||
<ChatBubbleOvalLeftEllipsisIcon className="w-5 h-5 animate-bounce" />
|
||||
</div>
|
||||
)}
|
||||
{chat.thought && chat.thought !== "" && !hidden && (
|
||||
<div
|
||||
onClick={() => setHidden((prev) => !prev)}
|
||||
style={{ backgroundColor: nodeColors["thought"] }}
|
||||
className=" text-start inline-block w-full pb-3 pt-3 px-5 cursor-pointer"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: chat.thought.replace(/\n/g, "<br />"),
|
||||
}}
|
||||
></div>
|
||||
)}
|
||||
{chat.thought && chat.thought !== "" && !hidden && <br></br>}
|
||||
<div className="w-full rounded-b-md px-4 pb-3 pt-3 pr-8" style={{ backgroundColor: nodeColors["chat"] }}>
|
||||
{chat.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full text-end">
|
||||
<div className="text-start inline-block rounded-xl p-3 overflow-hidden w-fit max-w-[280px] px-5 text-sm text-black dark:text-white dark:bg-gray-700 bg-gray-200 font-normal rounded-tr-none">
|
||||
{chat.message}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
243
src/frontend/src/components/chatComponent/index.tsx
Normal file
243
src/frontend/src/components/chatComponent/index.tsx
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { Transition } from "@headlessui/react";
|
||||
import {
|
||||
Bars3CenterLeftIcon,
|
||||
LockClosedIcon,
|
||||
PaperAirplaneIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { MouseEventHandler, useContext, useEffect, useRef, useState } from "react";
|
||||
import { sendAll } from "../../controllers/NodesServices";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { classNames, nodeColors } from "../../utils";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { ChatType } from "../../types/chat";
|
||||
import ChatMessage from "./chatMessage";
|
||||
|
||||
const _ = require("lodash");
|
||||
|
||||
export default function Chat({ flow, reactFlowInstance }: ChatType) {
|
||||
const { updateFlow,lockChat,setLockChat,flows,tabIndex } = useContext(TabsContext);
|
||||
const [saveChat, setSaveChat] = useState(false);
|
||||
const [open, setOpen] = useState(true);
|
||||
const [chatValue, setChatValue] = useState("");
|
||||
const [chatHistory, setChatHistory] = useState(flow.chat);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const addChatHistory = (
|
||||
message: string,
|
||||
isSend: boolean,
|
||||
thought?: string,
|
||||
) => {
|
||||
let tabsChange = false;
|
||||
setChatHistory((old) => {
|
||||
let newChat = _.cloneDeep(old);
|
||||
if(JSON.stringify(flow.chat) !==JSON.stringify(old)){
|
||||
console.log(old,flow.chat)
|
||||
tabsChange = true
|
||||
return old
|
||||
}
|
||||
if (thought) {
|
||||
newChat.push({ message, isSend, thought });
|
||||
} else {
|
||||
newChat.push({ message, isSend });
|
||||
}
|
||||
return newChat;
|
||||
});
|
||||
if(tabsChange){
|
||||
console.log(flow.chat)
|
||||
if(thought){
|
||||
updateFlow({..._.cloneDeep(flow),chat:[...flow.chat,{isSend,message,thought}]})
|
||||
}
|
||||
else{
|
||||
updateFlow({..._.cloneDeep(flow),chat:[...flow.chat,{isSend,message}]})
|
||||
}
|
||||
}
|
||||
setSaveChat((chat) => !chat);
|
||||
};
|
||||
useEffect(() => {
|
||||
updateFlow({ ..._.cloneDeep(flow), chat: chatHistory });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [saveChat]);
|
||||
useEffect(() => {
|
||||
setChatHistory(flow.chat);
|
||||
}, [flow]);
|
||||
useEffect(() => {
|
||||
if (ref.current) ref.current.scrollIntoView({ behavior: "smooth" });
|
||||
}, [chatHistory]);
|
||||
function validateNodes() {
|
||||
if (
|
||||
reactFlowInstance
|
||||
.getNodes()
|
||||
.some(
|
||||
(n) =>
|
||||
n.data.node &&
|
||||
Object.keys(n.data.node.template).some(
|
||||
(t: any) =>
|
||||
n.data.node.template[t].required &&
|
||||
n.data.node.template[t].value === "" &&
|
||||
n.data.node.template[t].required &&
|
||||
!reactFlowInstance
|
||||
.getEdges()
|
||||
.some(
|
||||
(e) =>
|
||||
e.sourceHandle.split("|")[1] === t &&
|
||||
e.sourceHandle.split("|")[2] === n.id
|
||||
)
|
||||
)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const ref = useRef(null);
|
||||
|
||||
function sendMessage() {
|
||||
if (chatValue !== "") {
|
||||
if (validateNodes()) {
|
||||
setLockChat(true);
|
||||
let message = chatValue;
|
||||
setChatValue("");
|
||||
addChatHistory(message, true);
|
||||
console.log({ ...reactFlowInstance.toObject(), message, chatHistory });
|
||||
|
||||
sendAll({ ...reactFlowInstance.toObject(), message, chatHistory})
|
||||
.then((r) => {
|
||||
console.log(r.data);
|
||||
addChatHistory(r.data.result, false, r.data.thought);
|
||||
setLockChat(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
setErrorData({ title: error.message ?? "unknow error" });
|
||||
setLockChat(false);
|
||||
});
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Error sending message",
|
||||
list: [ "Oops! Looks like you missed some required information. Please fill in all the required fields before continuing."],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setErrorData({
|
||||
title: "Error sending message",
|
||||
list: ["The message cannot be empty."],
|
||||
});
|
||||
}
|
||||
}
|
||||
function clearChat() {
|
||||
setChatHistory([])
|
||||
updateFlow({ ..._.cloneDeep(flow), chat: []});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
show={open}
|
||||
appear={true}
|
||||
enter="transition ease-out duration-300"
|
||||
enterFrom="translate-y-96"
|
||||
enterTo="translate-y-0"
|
||||
leave="transition ease-in duration-300"
|
||||
leaveFrom="translate-y-0"
|
||||
leaveTo="translate-y-96"
|
||||
>
|
||||
<div className="w-[340px] absolute bottom-0 right-1">
|
||||
<div className="border dark:border-gray-700 h-full rounded-xl rounded-b-none bg-white dark:bg-gray-800 shadow">
|
||||
<div
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
className="flex justify-between cursor-pointer items-center px-5 py-2 border-b dark:border-b-gray-700"
|
||||
>
|
||||
<div className="flex gap-3 text-lg dark:text-white font-medium items-center">
|
||||
<Bars3CenterLeftIcon
|
||||
className="h-5 w-5 mt-1"
|
||||
style={{ color: nodeColors["chat"] }}
|
||||
/>
|
||||
Chat
|
||||
</div>
|
||||
<button className="hover:text-blue-500"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
clearChat();
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div className="w-full h-[400px] flex gap-3 mb-auto overflow-y-auto scrollbar-hide flex-col bg-gray-50 dark:bg-gray-900 p-3 py-5">
|
||||
{chatHistory.map((c, i) => (
|
||||
<ChatMessage chat={c} key={i} />
|
||||
))}
|
||||
<div ref={ref}></div>
|
||||
</div>
|
||||
<div className="w-full bg-white dark:bg-gray-800 border-t dark:border-t-gray-600 flex items-center justify-between p-3">
|
||||
<div className="relative w-full mt-1 rounded-md shadow-sm">
|
||||
<input
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !lockChat) {
|
||||
sendMessage();
|
||||
}
|
||||
}}
|
||||
type="text"
|
||||
disabled={lockChat}
|
||||
value={lockChat ? "Thinking..." : chatValue}
|
||||
onChange={(e) => {
|
||||
setChatValue(e.target.value);
|
||||
}}
|
||||
className={classNames(
|
||||
lockChat ? "bg-gray-500 text-white" : "dark:bg-gray-700",
|
||||
"form-input block w-full rounded-md border-gray-300 dark:border-gray-600 dark:text-white pr-10 sm:text-sm"
|
||||
)}
|
||||
placeholder={"Send a message..."}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||
<button disabled={lockChat} onClick={() => sendMessage()}>
|
||||
{lockChat ? (
|
||||
<LockClosedIcon
|
||||
className="h-5 w-5 text-gray-400 dark:hover:text-gray-300 animate-pulse"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<PaperAirplaneIcon
|
||||
className="h-5 w-5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition
|
||||
show={!open}
|
||||
appear={true}
|
||||
enter="transition ease-out duration-300"
|
||||
enterFrom="translate-y-96"
|
||||
enterTo="translate-y-0"
|
||||
leave="transition ease-in duration-300"
|
||||
leaveFrom="translate-y-0"
|
||||
leaveTo="translate-y-96"
|
||||
>
|
||||
<div className="absolute bottom-0 right-1">
|
||||
<div className="border flex justify-center align-center py-1 px-3 rounded-xl rounded-b-none bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-white shadow">
|
||||
<button
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-3 items-center">
|
||||
<Bars3CenterLeftIcon
|
||||
className="h-6 w-6 mt-1"
|
||||
style={{ color: nodeColors["chat"] }}
|
||||
/>
|
||||
Chat
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</>
|
||||
);
|
||||
}
|
||||
82
src/frontend/src/components/dropdownComponent/index.tsx
Normal file
82
src/frontend/src/components/dropdownComponent/index.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { ChevronUpDownIcon, CheckIcon } from "@heroicons/react/24/outline";
|
||||
import { Fragment } from "react";
|
||||
import { DropDownComponentType } from "../../types/components";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
export default function Dropdown({title, value, options, onSelect}:DropDownComponentType) {
|
||||
return (
|
||||
<>
|
||||
<Listbox value={value} onChange={onSelect}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Label className="block text-sm font-medium text-gray-700">
|
||||
{title}
|
||||
</Listbox.Label>
|
||||
<div className="relative mt-1">
|
||||
<Listbox.Button className="relative w-full cursor-default rounded-md border border-gray-300 bg-white py-2 pl-3 pr-10 text-left shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 sm:text-sm">
|
||||
<span className="block truncate">{value}</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<ChevronUpDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{options.map((option, id) => (
|
||||
<Listbox.Option
|
||||
key={id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "text-white bg-indigo-600" : "text-gray-900",
|
||||
"relative cursor-default select-none py-2 pl-3 pr-9"
|
||||
)
|
||||
}
|
||||
value={option}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames(
|
||||
selected ? "font-semibold" : "font-normal",
|
||||
"block truncate"
|
||||
)}
|
||||
>
|
||||
{option}
|
||||
</span>
|
||||
|
||||
{selected ? (
|
||||
<span
|
||||
className={classNames(
|
||||
active ? "text-white" : "text-indigo-600",
|
||||
"absolute inset-y-0 right-0 flex items-center pr-4"
|
||||
)}
|
||||
>
|
||||
<CheckIcon
|
||||
className="h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
26
src/frontend/src/components/floatComponent/index.tsx
Normal file
26
src/frontend/src/components/floatComponent/index.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { FloatComponentType } from "../../types/components";
|
||||
|
||||
export default function FloatComponent({value, onChange, disabled}: FloatComponentType){
|
||||
const [myValue, setMyValue] = useState(value ?? "");
|
||||
useEffect(()=> {
|
||||
if(disabled){
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange])
|
||||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||
<input
|
||||
type="number"
|
||||
value={myValue}
|
||||
className={"block w-full form-input dark:bg-gray-900 arrow-hide dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + (disabled ? " bg-gray-200 dark:bg-gray-700" : "")}
|
||||
placeholder="Type a number from zero to one"
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
36
src/frontend/src/components/inputComponent/index.tsx
Normal file
36
src/frontend/src/components/inputComponent/index.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { InputComponentType } from "../../types/components";
|
||||
import { classNames } from "../../utils";
|
||||
|
||||
export default function InputComponent({
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
password,
|
||||
}: InputComponentType) {
|
||||
const [myValue, setMyValue] = useState(value ?? "");
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setMyValue("");
|
||||
onChange("");
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||
<input
|
||||
type="text"
|
||||
value={myValue}
|
||||
className={classNames(
|
||||
"block w-full form-input dark:bg-gray-900 dark:border-gray-600 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
|
||||
disabled ? " bg-gray-200 dark:bg-gray-700" : "",
|
||||
password?"password":""
|
||||
)}
|
||||
placeholder="Type a text"
|
||||
onChange={(e) => {
|
||||
setMyValue(e.target.value);
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
54
src/frontend/src/components/inputListComponent/index.tsx
Normal file
54
src/frontend/src/components/inputListComponent/index.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import { useEffect, useState } from "react";
|
||||
import { InputListComponentType } from "../../types/components";
|
||||
|
||||
var _ = require("lodash");
|
||||
|
||||
export default function InputListComponent({ value, onChange, disabled}:InputListComponentType) {
|
||||
const [inputList, setInputList] = useState(value ?? [""]);
|
||||
useEffect(()=> {
|
||||
if(disabled){
|
||||
setInputList([""]);
|
||||
onChange([""]);
|
||||
}
|
||||
}, [disabled, onChange])
|
||||
return (
|
||||
<div className={(disabled ? "pointer-events-none cursor-not-allowed" : "") + "flex flex-col gap-3"}>
|
||||
{inputList.map((i, idx) => (
|
||||
<div key={idx} className="w-full flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={i}
|
||||
className={"block w-full form-input rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" + (disabled ? " bg-gray-200" : "")}
|
||||
placeholder="Type a text"
|
||||
onChange={(e) => {
|
||||
setInputList((old) => {
|
||||
let newInputList = _.cloneDeep(old);
|
||||
newInputList[idx] = e.target.value;
|
||||
return newInputList;
|
||||
});
|
||||
onChange(inputList);
|
||||
}}
|
||||
/>
|
||||
{idx === inputList.length - 1 ?
|
||||
<button onClick={() => {setInputList((old) => {
|
||||
let newInputList = _.cloneDeep(old);
|
||||
newInputList.push('');
|
||||
return newInputList;
|
||||
});
|
||||
onChange(inputList);}}>
|
||||
<PlusIcon className="w-4 h-4 hover:text-blue-600" />
|
||||
</button>
|
||||
: <button onClick={() => {setInputList((old) => {
|
||||
let newInputList = _.cloneDeep(old);
|
||||
newInputList.splice(idx, 1);
|
||||
return newInputList;
|
||||
});
|
||||
onChange(inputList);}}>
|
||||
<XMarkIcon className="w-4 h-4 hover:text-red-600" />
|
||||
</button>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
16
src/frontend/src/components/loadingComponent/index.tsx
Normal file
16
src/frontend/src/components/loadingComponent/index.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
type LoadingComponentProps={
|
||||
remSize:number
|
||||
}
|
||||
|
||||
|
||||
export default function LoadingComponent({remSize}:LoadingComponentProps){
|
||||
return(
|
||||
<div role="status" className="w-min m-auto">
|
||||
<svg aria-hidden="true" className={`w-${remSize} h-${remSize} mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`} viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
||||
</svg>
|
||||
<span className="animate-pulse text-blue-600 text-lg">Loading...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
src/frontend/src/components/textAreaComponent/index.tsx
Normal file
33
src/frontend/src/components/textAreaComponent/index.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
import TextAreaModal from "../../modals/textAreaModal";
|
||||
import { TextAreaComponentType } from "../../types/components";
|
||||
|
||||
export default function TextAreaComponent({ value, onChange, disabled }:TextAreaComponentType) {
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
useEffect(() => {
|
||||
if (disabled) {
|
||||
setMyValue([""]);
|
||||
onChange([""]);
|
||||
}
|
||||
}, [disabled, onChange]);
|
||||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||
<div className="w-full flex items-center gap-3">
|
||||
<span
|
||||
className={
|
||||
"truncate block w-full text-gray-500 px-3 py-2 rounded-md border border-gray-300 dark:border-gray-700 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm" +
|
||||
(disabled ? " bg-gray-200" : "")
|
||||
}
|
||||
>
|
||||
{myValue !== "" ? myValue : 'Text empty'}
|
||||
</span>
|
||||
<button onClick={()=>{openPopUp(<TextAreaModal value={myValue} setValue={(t:string) => {setMyValue(t); onChange(t);}}/>)}}>
|
||||
<ArrowTopRightOnSquareIcon className="w-6 h-6 hover:text-blue-600" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
src/frontend/src/components/toggleComponent/index.tsx
Normal file
75
src/frontend/src/components/toggleComponent/index.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Switch } from "@headlessui/react";
|
||||
import { classNames } from "../../utils";
|
||||
import { useEffect } from "react";
|
||||
import { ToggleComponentType } from "../../types/components";
|
||||
|
||||
export default function ToggleComponent({ enabled, setEnabled, disabled }:ToggleComponentType) {
|
||||
useEffect(()=> {
|
||||
if(disabled){
|
||||
setEnabled(false);
|
||||
}
|
||||
}, [disabled, setEnabled])
|
||||
return (
|
||||
<div className={disabled ? "pointer-events-none cursor-not-allowed" : ""}>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={(x:boolean) => {
|
||||
setEnabled(x);
|
||||
}}
|
||||
className={classNames(
|
||||
enabled ? "bg-indigo-600" : "bg-gray-200 dark:bg-gray-600",
|
||||
"relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out "
|
||||
)}
|
||||
>
|
||||
<span className="sr-only">Use setting</span>
|
||||
<span
|
||||
className={classNames(
|
||||
enabled ? "translate-x-5" : "translate-x-0",
|
||||
"pointer-events-none relative inline-block h-5 w-5 transform rounded-full shadow ring-0 transition duration-200 ease-in-out", disabled ? "bg-gray-200 dark:bg-gray-600" : "bg-white dark:bg-gray-800"
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
enabled
|
||||
? "opacity-0 ease-out duration-100"
|
||||
: "opacity-100 ease-in duration-200",
|
||||
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3 text-gray-400"
|
||||
fill="none"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path
|
||||
d="M4 8l2-2m0 0l2-2M6 6L4 4m2 2l2 2"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
className={classNames(
|
||||
enabled
|
||||
? "opacity-100 ease-in duration-200"
|
||||
: "opacity-0 ease-out duration-100",
|
||||
"absolute inset-0 flex h-full w-full items-center justify-center transition-opacity"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
className="h-3 w-3 text-indigo-600"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 12 12"
|
||||
>
|
||||
<path d="M3.707 5.293a1 1 0 00-1.414 1.414l1.414-1.414zM5 8l-.707.707a1 1 0 001.414 0L5 8zm4.707-3.293a1 1 0 00-1.414-1.414l1.414 1.414zm-7.414 2l2 2 1.414-1.414-2-2-1.414 1.414zm3.414 2l4-4-1.414-1.414-4 4 1.414 1.414z" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
164
src/frontend/src/contexts/alertContext.tsx
Normal file
164
src/frontend/src/contexts/alertContext.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
import { createContext, ReactNode, useState } from "react";
|
||||
import { AlertItemType } from "../types/alerts";
|
||||
|
||||
var _ = require("lodash");
|
||||
|
||||
//types for alertContextType
|
||||
type alertContextType = {
|
||||
errorData: { title: string; list?: Array<string> };
|
||||
setErrorData: (newState: { title: string; list?: Array<string> }) => void;
|
||||
errorOpen: boolean;
|
||||
setErrorOpen: (newState: boolean) => void;
|
||||
noticeData: { title: string; link?: string };
|
||||
setNoticeData: (newState: { title: string; link?: string }) => void;
|
||||
noticeOpen: boolean;
|
||||
setNoticeOpen: (newState: boolean) => void;
|
||||
successData: { title: string };
|
||||
setSuccessData: (newState: { title: string }) => void;
|
||||
successOpen: boolean;
|
||||
setSuccessOpen: (newState: boolean) => void;
|
||||
notificationCenter: boolean;
|
||||
setNotificationCenter: (newState: boolean) => void;
|
||||
notificationList: Array<AlertItemType>;
|
||||
pushNotificationList: (Object:AlertItemType) => void;
|
||||
clearNotificationList: () => void;
|
||||
removeFromNotificationList: (index: string) => void;
|
||||
};
|
||||
|
||||
//initial values to alertContextType
|
||||
const initialValue:alertContextType = {
|
||||
errorData: { title: "", list: [] },
|
||||
setErrorData: () => {},
|
||||
errorOpen: false,
|
||||
setErrorOpen: () => {},
|
||||
noticeData: { title: "", link: "" },
|
||||
setNoticeData: () => {},
|
||||
noticeOpen: false,
|
||||
setNoticeOpen: () => {},
|
||||
successData: { title: "" },
|
||||
setSuccessData: () => {},
|
||||
successOpen: false,
|
||||
setSuccessOpen: () => {},
|
||||
notificationCenter: false,
|
||||
setNotificationCenter: () => {},
|
||||
notificationList: [],
|
||||
pushNotificationList: () => {},
|
||||
clearNotificationList: () => {},
|
||||
removeFromNotificationList: () => {},
|
||||
};
|
||||
|
||||
export const alertContext = createContext<alertContextType>(initialValue);
|
||||
|
||||
export function AlertProvider({ children }:{children:ReactNode}) {
|
||||
const [errorData, setErrorDataState] = useState<{
|
||||
title: string;
|
||||
list?: Array<string>;
|
||||
}>({ title: "", list: [] });
|
||||
const [errorOpen, setErrorOpen] = useState(false);
|
||||
const [noticeData, setNoticeDataState] = useState<{
|
||||
title: string;
|
||||
link?: string;
|
||||
}>({ title: "", link: "" });
|
||||
const [noticeOpen, setNoticeOpen] = useState(false);
|
||||
const [successData, setSuccessDataState] = useState<{ title: string }>({
|
||||
title: "",
|
||||
});
|
||||
const [successOpen, setSuccessOpen] = useState(false);
|
||||
const [notificationCenter, setNotificationCenter] = useState(false);
|
||||
const [notificationList, setNotificationList] = useState([]);
|
||||
const pushNotificationList = (notification: AlertItemType) => {
|
||||
setNotificationList((old) => {
|
||||
let newNotificationList = _.cloneDeep(old);
|
||||
newNotificationList.unshift(notification);
|
||||
return newNotificationList;
|
||||
});
|
||||
};
|
||||
/**
|
||||
* Sets the error data state, opens the error dialog and pushes the new error notification to the notification list
|
||||
* @param newState An object containing the new error data, including title and optional list of error messages
|
||||
*/
|
||||
function setErrorData(newState: { title: string; list?: Array<string> }) {
|
||||
setErrorDataState(newState);
|
||||
setErrorOpen(true);
|
||||
if (newState.title) {
|
||||
setNotificationCenter(true);
|
||||
pushNotificationList({
|
||||
type: "error",
|
||||
title: newState.title,
|
||||
list: newState.list,
|
||||
id: _.uniqueId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sets the state of the notice data and opens the notice modal, also adds a new notice to the notification center if the title is defined.
|
||||
* @param newState An object containing the title of the notice and optionally a link.
|
||||
*/
|
||||
function setNoticeData(newState: { title: string; link?: string }) {
|
||||
setNoticeDataState(newState);
|
||||
setNoticeOpen(true);
|
||||
if (newState.title) {
|
||||
// Add new notice to notification center
|
||||
setNotificationCenter(true);
|
||||
pushNotificationList({
|
||||
type: "notice",
|
||||
title: newState.title,
|
||||
link: newState.link,
|
||||
id: _.uniqueId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Update the success data state and show a success alert notification.
|
||||
* @param newState - A state object with a "title" property to set in the success data state.
|
||||
*/
|
||||
function setSuccessData(newState: { title: string }) {
|
||||
setSuccessDataState(newState); // update the success data state with the provided new state
|
||||
setSuccessOpen(true); // open the success alert
|
||||
|
||||
// If the new state has a "title" property, add a new success notification to the list
|
||||
if (newState.title) {
|
||||
setNotificationCenter(true); // show the notification center
|
||||
pushNotificationList({ // add the new notification to the list
|
||||
type: "success",
|
||||
title: newState.title,
|
||||
id: _.uniqueId(),
|
||||
});
|
||||
}
|
||||
}
|
||||
function clearNotificationList() {
|
||||
setNotificationList([]);
|
||||
}
|
||||
function removeFromNotificationList(index: string) {
|
||||
// set the notification list to a new array that filters out the alert with the matching id
|
||||
setNotificationList((prevAlertsList) =>
|
||||
prevAlertsList.filter((alert) => alert.id !== index)
|
||||
);
|
||||
}
|
||||
return (
|
||||
<alertContext.Provider
|
||||
value={{
|
||||
removeFromNotificationList,
|
||||
clearNotificationList,
|
||||
notificationList,
|
||||
pushNotificationList,
|
||||
setNotificationCenter,
|
||||
notificationCenter,
|
||||
errorData,
|
||||
setErrorData,
|
||||
errorOpen,
|
||||
setErrorOpen,
|
||||
noticeData,
|
||||
setNoticeData,
|
||||
noticeOpen,
|
||||
setNoticeOpen,
|
||||
successData,
|
||||
setSuccessData,
|
||||
successOpen,
|
||||
setSuccessOpen,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</alertContext.Provider>
|
||||
);
|
||||
}
|
||||
34
src/frontend/src/contexts/darkContext.tsx
Normal file
34
src/frontend/src/contexts/darkContext.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { createContext, useEffect, useState } from "react";
|
||||
|
||||
type darkContextType = {
|
||||
dark: {};
|
||||
setDark: (newState: {}) => void;
|
||||
};
|
||||
|
||||
const initialValue = {
|
||||
dark: {},
|
||||
setDark: () => {},
|
||||
};
|
||||
|
||||
export const darkContext = createContext<darkContextType>(initialValue);
|
||||
|
||||
export function DarkProvider({ children }) {
|
||||
const [dark, setDark] = useState(false);
|
||||
useEffect(()=>{
|
||||
if(dark){
|
||||
document.getElementById("body").classList.add("dark");
|
||||
} else {
|
||||
document.getElementById("body").classList.remove("dark");
|
||||
}
|
||||
}, [dark])
|
||||
return (
|
||||
<darkContext.Provider
|
||||
value={{
|
||||
dark,
|
||||
setDark,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</darkContext.Provider>
|
||||
);
|
||||
}
|
||||
26
src/frontend/src/contexts/index.tsx
Normal file
26
src/frontend/src/contexts/index.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { ReactNode } from "react";
|
||||
import { AlertProvider } from "./alertContext";
|
||||
import { DarkProvider } from "./darkContext";
|
||||
import { LocationProvider } from "./locationContext";
|
||||
import PopUpProvider from "./popUpContext";
|
||||
import { TabsProvider } from "./tabsContext";
|
||||
import { TypesProvider } from "./typesContext";
|
||||
|
||||
export default function ContextWrapper({ children }: { children: ReactNode }) {
|
||||
//element to wrap all context
|
||||
return (
|
||||
<>
|
||||
<DarkProvider>
|
||||
<LocationProvider>
|
||||
<AlertProvider>
|
||||
<PopUpProvider>
|
||||
<TypesProvider>
|
||||
<TabsProvider>{children}</TabsProvider>
|
||||
</TypesProvider>
|
||||
</PopUpProvider>
|
||||
</AlertProvider>
|
||||
</LocationProvider>
|
||||
</DarkProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
79
src/frontend/src/contexts/locationContext.tsx
Normal file
79
src/frontend/src/contexts/locationContext.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { createContext, ReactNode, useState } from "react";
|
||||
|
||||
//types for location context
|
||||
type locationContextType = {
|
||||
current: Array<string>;
|
||||
setCurrent: (newState: Array<string>) => void;
|
||||
isStackedOpen: boolean;
|
||||
setIsStackedOpen: (newState: boolean) => void;
|
||||
showSideBar: boolean;
|
||||
setShowSideBar: (newState: boolean) => void;
|
||||
extraNavigation: {
|
||||
title: string;
|
||||
options?: Array<{
|
||||
name: string;
|
||||
href: string;
|
||||
icon: any;
|
||||
children?: Array<any>;
|
||||
}>;
|
||||
};
|
||||
setExtraNavigation: (newState: {
|
||||
title: string;
|
||||
options?: Array<{
|
||||
name: string;
|
||||
href: string;
|
||||
icon: any;
|
||||
children?: Array<any>;
|
||||
}>;
|
||||
}) => void;
|
||||
extraComponent: any;
|
||||
setExtraComponent: (newState: any) => void;
|
||||
};
|
||||
|
||||
//initial value for location context
|
||||
const initialValue = {
|
||||
//actual
|
||||
current: window.location.pathname.replace(/\/$/g, "").split("/"),
|
||||
isStackedOpen:
|
||||
window.innerWidth > 1024 && window.location.pathname.split("/")[1]
|
||||
? true
|
||||
: false,
|
||||
setCurrent: () => {},
|
||||
setIsStackedOpen: () => {},
|
||||
showSideBar: window.location.pathname.split("/")[1] ? true : false,
|
||||
setShowSideBar: () => {},
|
||||
extraNavigation: { title: "" },
|
||||
setExtraNavigation: () => {},
|
||||
extraComponent: <></>,
|
||||
setExtraComponent: () => {},
|
||||
};
|
||||
|
||||
export const locationContext = createContext<locationContextType>(initialValue);
|
||||
|
||||
export function LocationProvider({ children }:{children:ReactNode}) {
|
||||
const [current, setCurrent] = useState(initialValue.current);
|
||||
const [isStackedOpen, setIsStackedOpen] = useState(
|
||||
initialValue.isStackedOpen
|
||||
);
|
||||
const [showSideBar, setShowSideBar] = useState(initialValue.showSideBar);
|
||||
const [extraNavigation, setExtraNavigation] = useState({ title: "" });
|
||||
const [extraComponent, setExtraComponent] = useState(<></>);
|
||||
return (
|
||||
<locationContext.Provider
|
||||
value={{
|
||||
isStackedOpen,
|
||||
setIsStackedOpen,
|
||||
current,
|
||||
setCurrent,
|
||||
showSideBar,
|
||||
setShowSideBar,
|
||||
extraNavigation,
|
||||
setExtraNavigation,
|
||||
extraComponent,
|
||||
setExtraComponent,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</locationContext.Provider>
|
||||
);
|
||||
}
|
||||
33
src/frontend/src/contexts/popUpContext.tsx
Normal file
33
src/frontend/src/contexts/popUpContext.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { createContext } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
//context to set JSX element on the DOM
|
||||
export const PopUpContext = createContext({
|
||||
openPopUp: (popUpElement: JSX.Element) => {},
|
||||
closePopUp: () => {},
|
||||
});
|
||||
|
||||
interface PopUpProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const PopUpProvider = ({ children }: PopUpProviderProps) => {
|
||||
const [popUpElement, setPopUpElement] = useState<JSX.Element | null>(null);
|
||||
|
||||
const openPopUp = (element: JSX.Element) => {
|
||||
setPopUpElement(element);
|
||||
};
|
||||
|
||||
const closePopUp = () => {
|
||||
setPopUpElement(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<PopUpContext.Provider value={{ openPopUp, closePopUp }}>
|
||||
{children}
|
||||
{popUpElement}
|
||||
</PopUpContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default PopUpProvider;
|
||||
188
src/frontend/src/contexts/tabsContext.tsx
Normal file
188
src/frontend/src/contexts/tabsContext.tsx
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import { createContext, useEffect, useState, useRef, ReactNode, useContext } from "react";
|
||||
import { FlowType } from "../types/flow";
|
||||
import { TabsContextType } from "../types/tabs";
|
||||
import { normalCaseToSnakeCase } from "../utils";
|
||||
import { alertContext } from "./alertContext";
|
||||
|
||||
const TabsContextInitialValue: TabsContextType = {
|
||||
tabIndex: 0,
|
||||
setTabIndex: (index: number) => {},
|
||||
flows: [],
|
||||
removeFlow: (id: string) => {},
|
||||
addFlow: (flowData?: any) => {},
|
||||
updateFlow: (newFlow: FlowType) => {},
|
||||
incrementNodeId: () => 0,
|
||||
downloadFlow: () => {},
|
||||
uploadFlow: () => {},
|
||||
lockChat: false,
|
||||
setLockChat:(prevState:boolean)=>{}
|
||||
};
|
||||
|
||||
export const TabsContext = createContext<TabsContextType>(
|
||||
TabsContextInitialValue
|
||||
);
|
||||
|
||||
export function TabsProvider({ children }: { children: ReactNode }) {
|
||||
const {setNoticeData} = useContext(alertContext)
|
||||
const [tabIndex, setTabIndex] = useState(0);
|
||||
const [flows, setFlows] = useState<Array<FlowType>>([]);
|
||||
const [id, setId] = useState(0);
|
||||
const [lockChat, setLockChat] = useState(false);
|
||||
|
||||
const newNodeId = useRef(0);
|
||||
function incrementNodeId() {
|
||||
newNodeId.current = newNodeId.current + 1;
|
||||
return newNodeId.current;
|
||||
}
|
||||
useEffect(() => {
|
||||
//save tabs locally
|
||||
if (flows.length !== 0)
|
||||
window.localStorage.setItem(
|
||||
"tabsData",
|
||||
JSON.stringify({ tabIndex, flows, id, nodeId: newNodeId.current })
|
||||
);
|
||||
}, [flows, id, tabIndex, newNodeId]);
|
||||
|
||||
useEffect(() => {
|
||||
//get tabs locally saved
|
||||
let cookie = window.localStorage.getItem("tabsData");
|
||||
if (cookie) {
|
||||
let cookieObject = JSON.parse(cookie);
|
||||
setTabIndex(cookieObject.tabIndex);
|
||||
setFlows(cookieObject.flows);
|
||||
setId(cookieObject.id);
|
||||
newNodeId.current = cookieObject.nodeId;
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Downloads the current flow as a JSON file
|
||||
*/
|
||||
function downloadFlow() {
|
||||
// create a data URI with the current flow data
|
||||
const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent(
|
||||
JSON.stringify(flows[tabIndex])
|
||||
)}`;
|
||||
|
||||
// create a link element and set its properties
|
||||
const link = document.createElement("a");
|
||||
link.href = jsonString;
|
||||
link.download = `${normalCaseToSnakeCase(flows[tabIndex].name)}.json`;
|
||||
|
||||
// simulate a click on the link element to trigger the download
|
||||
link.click();
|
||||
setNoticeData({title:"Warning: Critical data, including API keys, in JSON file. Keep secure and do not share."})
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a file input and listens to a change event to upload a JSON flow file.
|
||||
* If the file type is application/json, the file is read and parsed into a JSON object.
|
||||
* The resulting JSON object is passed to the addFlow function.
|
||||
*/
|
||||
function uploadFlow() {
|
||||
// create a file input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
// add a change event listener to the file input
|
||||
input.onchange = (e: Event) => {
|
||||
// check if the file type is application/json
|
||||
if ((e.target as HTMLInputElement).files[0].type === "application/json") {
|
||||
// get the file from the file input
|
||||
const file = (e.target as HTMLInputElement).files[0];
|
||||
// read the file as text
|
||||
file.text().then((text) => {
|
||||
// parse the text into a JSON object
|
||||
addFlow(JSON.parse(text));
|
||||
});
|
||||
}
|
||||
};
|
||||
// trigger the file input click event to open the file dialog
|
||||
input.click();
|
||||
}
|
||||
/**
|
||||
* Removes a flow from an array of flows based on its id.
|
||||
* 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) {
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState];
|
||||
const index = newFlows.findIndex((flow) => flow.id === id);
|
||||
if (index >= 0) {
|
||||
if (index === tabIndex) {
|
||||
setTabIndex(flows.length - 2);
|
||||
newFlows.splice(index, 1);
|
||||
} else {
|
||||
let flowId = flows[tabIndex].id;
|
||||
newFlows.splice(index, 1);
|
||||
setTabIndex(newFlows.findIndex((flow) => flow.id === flowId));
|
||||
}
|
||||
}
|
||||
return newFlows;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Add a new flow to the list of flows.
|
||||
* @param flow Optional flow to add.
|
||||
*/
|
||||
function addFlow(flow?: FlowType) {
|
||||
// Get data from the flow or set it to null if there's no flow provided.
|
||||
const data = flow?.data ? flow.data : null;
|
||||
|
||||
// Create a new flow with a default name if no flow is provided.
|
||||
let newFlow: FlowType = {
|
||||
name: flow ? flow.name : "New Flow " + (flows.length===0?"":flows.length),
|
||||
id: id.toString(),
|
||||
data,
|
||||
chat: flow ? flow.chat : [],
|
||||
};
|
||||
|
||||
// Increment the ID counter.
|
||||
setId((old) => old + 1);
|
||||
|
||||
// Add the new flow to the list of flows.
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState, newFlow];
|
||||
return newFlows;
|
||||
});
|
||||
|
||||
// Set the tab index to the new flow.
|
||||
setTabIndex(flows.length);
|
||||
}
|
||||
/**
|
||||
* Updates an existing flow with new data
|
||||
* @param newFlow - The new flow object containing the updated data
|
||||
*/
|
||||
function updateFlow(newFlow: FlowType) {
|
||||
setFlows((prevState) => {
|
||||
const newFlows = [...prevState];
|
||||
const index = newFlows.findIndex((flow) => flow.id === newFlow.id);
|
||||
if (index !== -1) {
|
||||
newFlows[index].data = newFlow.data;
|
||||
newFlows[index].name = newFlow.name;
|
||||
newFlows[index].chat = newFlow.chat;
|
||||
}
|
||||
return newFlows;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<TabsContext.Provider
|
||||
value={{
|
||||
lockChat,
|
||||
setLockChat,
|
||||
tabIndex,
|
||||
setTabIndex,
|
||||
flows,
|
||||
incrementNodeId,
|
||||
removeFlow,
|
||||
addFlow,
|
||||
updateFlow,
|
||||
downloadFlow,
|
||||
uploadFlow,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
}
|
||||
39
src/frontend/src/contexts/typesContext.tsx
Normal file
39
src/frontend/src/contexts/typesContext.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { createContext, ReactNode, useState } from "react";
|
||||
import { Node} from "reactflow";
|
||||
import { typesContextType } from "../types/typesContext";
|
||||
|
||||
//context to share types adn functions from nodes to flow
|
||||
|
||||
const initialValue:typesContextType = {
|
||||
reactFlowInstance: null,
|
||||
setReactFlowInstance: () => {},
|
||||
deleteNode: () => {},
|
||||
types: {},
|
||||
setTypes: () => {},
|
||||
};
|
||||
|
||||
export const typesContext = createContext<typesContextType>(initialValue);
|
||||
|
||||
export function TypesProvider({ children }:{children:ReactNode}) {
|
||||
const [types, setTypes] = useState({});
|
||||
const [reactFlowInstance, setReactFlowInstance] = useState(null);
|
||||
function deleteNode(idx:string) {
|
||||
reactFlowInstance.setNodes(
|
||||
reactFlowInstance.getNodes().filter((n:Node) => n.id !== idx)
|
||||
);
|
||||
reactFlowInstance.setEdges(reactFlowInstance.getEdges().filter((ns) => ns.source !== idx && ns.target !== idx));
|
||||
}
|
||||
return (
|
||||
<typesContext.Provider
|
||||
value={{
|
||||
types,
|
||||
setTypes,
|
||||
reactFlowInstance,
|
||||
setReactFlowInstance,
|
||||
deleteNode,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</typesContext.Provider>
|
||||
);
|
||||
}
|
||||
13
src/frontend/src/controllers/NodesServices/index.ts
Normal file
13
src/frontend/src/controllers/NodesServices/index.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { APIObjectType, sendAllProps } from '../../types/api/index';
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
|
||||
const backendUrl = process.env.BACKEND || "http://localhost:5003";
|
||||
|
||||
export async function getAll():Promise<AxiosResponse<APIObjectType>> {
|
||||
return await axios.get(`${backendUrl}/all`);
|
||||
}
|
||||
|
||||
export async function sendAll(data:sendAllProps) {
|
||||
console.log(data);
|
||||
return await axios.post(`${backendUrl}/predict`, data);
|
||||
}
|
||||
15
src/frontend/src/index.css
Normal file
15
src/frontend/src/index.css
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
18
src/frontend/src/index.tsx
Normal file
18
src/frontend/src/index.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import ContextWrapper from './contexts';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
root.render(
|
||||
<ContextWrapper>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ContextWrapper>
|
||||
);
|
||||
reportWebVitals();
|
||||
1
src/frontend/src/logo.svg
Normal file
1
src/frontend/src/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
104
src/frontend/src/modals/textAreaModal/index.tsx
Normal file
104
src/frontend/src/modals/textAreaModal/index.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import { XMarkIcon, ClipboardDocumentListIcon } from "@heroicons/react/24/outline";
|
||||
import { Fragment, useContext, useRef, useState } from "react";
|
||||
import { PopUpContext } from "../../contexts/popUpContext";
|
||||
|
||||
export default function TextAreaModal({value, setValue}:{setValue:(value:string)=>void,value:string|string[]}){
|
||||
const [open, setOpen] = useState(true);
|
||||
const [myValue, setMyValue] = useState(value);
|
||||
const { closePopUp } = useContext(PopUpContext);
|
||||
const ref = useRef();
|
||||
function setModalOpen(x:boolean){
|
||||
setOpen(x);
|
||||
if(x === false){
|
||||
setTimeout(() => {closePopUp()}, 300);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Transition.Root show={open} appear={true} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-10"
|
||||
onClose={setModalOpen}
|
||||
initialFocus={ref}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-500 dark:bg-gray-600 dark:bg-opacity-75 bg-opacity-75 transition-opacity" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||
<div className="flex h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative flex flex-col justify-between transform h-[600px] overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 w-[700px]">
|
||||
<div className=" z-50 absolute top-0 right-0 hidden pt-4 pr-4 sm:block">
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="sr-only">Close</span>
|
||||
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-full w-full flex flex-col justify-center items-center">
|
||||
<div className="flex w-full pb-4 z-10 justify-center shadow-sm">
|
||||
<div className="mx-auto mt-4 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 dark:bg-gray-900 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<ClipboardDocumentListIcon
|
||||
className="h-6 w-6 text-blue-600"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 text-center sm:ml-4 sm:text-left">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg font-medium dark:text-white leading-10 text-gray-900"
|
||||
>
|
||||
Edit text
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full w-full bg-gray-200 dark:bg-gray-900 p-4 gap-4 flex flex-row justify-center items-center">
|
||||
<div className="flex h-full w-full">
|
||||
<div className="overflow-hidden px-4 py-5 sm:p-6 w-full rounded-lg bg-white dark:bg-gray-800 shadow">
|
||||
<textarea ref={ref} className="form-input h-full w-full rounded-lg border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-white" value={myValue} onChange={(e) => {setMyValue(e.target.value); setValue(e.target.value)}}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-200 dark:bg-gray-900 w-full pb-3 flex flex-row-reverse px-4">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
|
||||
onClick={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
>
|
||||
Finish editing
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { ConnectionLineComponentProps } from 'reactflow';
|
||||
|
||||
|
||||
|
||||
const ConnectionLineComponent = ({
|
||||
fromX,
|
||||
fromY,
|
||||
toX,
|
||||
toY,
|
||||
connectionLineStyle = {} // provide a default value for connectionLineStyle
|
||||
}:ConnectionLineComponentProps) => {
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="#222"
|
||||
strokeWidth={1.5}
|
||||
className="animated dark:stroke-gray-400"
|
||||
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
|
||||
style={connectionLineStyle}
|
||||
/>
|
||||
<circle
|
||||
cx={toX}
|
||||
cy={toY}
|
||||
fill="#fff"
|
||||
r={3}
|
||||
stroke="#222"
|
||||
className="dark:stroke-gray-400 dark:fill-gray-800"
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionLineComponent;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import {
|
||||
ChevronRightIcon,
|
||||
} from "@heroicons/react/24/solid";
|
||||
import { Disclosure } from "@headlessui/react";
|
||||
import { DisclosureComponentType } from "../../../../types/components";
|
||||
|
||||
export default function DisclosureComponent({
|
||||
button: { title, Icon, buttons = [] },
|
||||
children,
|
||||
}: DisclosureComponentType
|
||||
) {
|
||||
return (
|
||||
<Disclosure as="div" key={title}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<div className="select-none bg-gray-50 dark:bg-gray-700 dark:border-y-gray-600 w-full flex justify-between items-center -mt-px px-3 py-2 border-y border-y-gray-200">
|
||||
<div className="flex gap-4">
|
||||
<Icon className="w-6 text-gray-800 dark:text-white" />
|
||||
<span className="flex items-center text-sm text-gray-800 dark:text-white font-medium">
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{buttons.map((x, index) => (
|
||||
<button key={index} onClick={x.onClick}>
|
||||
{x.Icon}
|
||||
</button>
|
||||
))}
|
||||
<Disclosure.Button as="button">
|
||||
<ChevronRightIcon
|
||||
className={`${
|
||||
open ? "rotate-90 transform" : ""
|
||||
} h-4 w-4 text-gray-800 dark:text-white`}
|
||||
/>
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Disclosure.Panel as="div" className="-mt-px">
|
||||
{children}
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
import { Bars2Icon } from "@heroicons/react/24/outline";
|
||||
import DisclosureComponent from "../DisclosureComponent";
|
||||
import {
|
||||
nodeColors,
|
||||
nodeIcons,
|
||||
nodeNames,
|
||||
} from "../../../../utils";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { getAll } from "../../../../controllers/NodesServices";
|
||||
import { typesContext } from "../../../../contexts/typesContext";
|
||||
import { APIClassType, APIKindType, APIObjectType } from "../../../../types/api";
|
||||
|
||||
export default function ExtraSidebar() {
|
||||
const [data, setData] = useState({});
|
||||
const { setTypes} = useContext(typesContext);
|
||||
|
||||
useEffect(() => {
|
||||
async function getTypes():Promise<void>{
|
||||
|
||||
// Make an asynchronous API call to retrieve all data.
|
||||
let result = await getAll();
|
||||
|
||||
// Update the state of the component with the retrieved data.
|
||||
setData(result.data);
|
||||
|
||||
// Set the types by reducing over the keys of the result data and updating the accumulator.
|
||||
setTypes(
|
||||
Object.keys(result.data).reduce(
|
||||
(acc, curr) => {
|
||||
Object.keys(result.data[curr]).forEach((c:keyof APIKindType) => {
|
||||
acc[c] = curr;
|
||||
// Add the base classes to the accumulator as well.
|
||||
result.data[curr][c].base_classes?.forEach((b) => {
|
||||
acc[b] = curr;
|
||||
});
|
||||
});
|
||||
return acc;
|
||||
},{}
|
||||
)
|
||||
);
|
||||
}
|
||||
// Call the getTypes function.
|
||||
getTypes();
|
||||
}, [setTypes]);
|
||||
|
||||
|
||||
function onDragStart(event: React.DragEvent<any>, data:{type:string,node?:APIClassType}) {
|
||||
//start drag event
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData("json", JSON.stringify(data));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-1 w-full">
|
||||
{Object.keys(data).map((d:keyof APIObjectType, i) => (
|
||||
<DisclosureComponent
|
||||
key={i}
|
||||
button={{ title: nodeNames[d], Icon: nodeIcons[d] }}
|
||||
>
|
||||
<div className="p-2 flex flex-col gap-2">
|
||||
{Object.keys(data[d]).map((t: string, k) => (
|
||||
<div key={k}>
|
||||
<div
|
||||
draggable
|
||||
className={" cursor-grab border-l-8 rounded-l-md"}
|
||||
style={{ borderLeftColor: nodeColors[d] }}
|
||||
onDragStart={(event) =>
|
||||
onDragStart(event, {
|
||||
type: t,
|
||||
node: data[d][t],
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex w-full justify-between text-sm px-3 py-1 items-center border-dashed border-gray-400 dark:border-gray-600 border-l-0 rounded-md rounded-l-none border">
|
||||
<span className="text-black dark:text-white w-36 truncate text-xs">{t}</span>
|
||||
<Bars2Icon className="w-4 h-6 text-gray-400 dark:text-gray-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</DisclosureComponent>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/solid";
|
||||
import { useContext, useState } from "react";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import { FlowType } from "../../../../types/flow";
|
||||
|
||||
var _ = require("lodash");
|
||||
|
||||
export default function TabComponent({ selected, flow, onClick }:{flow:FlowType,selected:boolean,onClick:()=>void}) {
|
||||
const { removeFlow, updateFlow, flows } =
|
||||
useContext(TabsContext);
|
||||
const [isRename, setIsRename] = useState(false);
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<>
|
||||
{flow ? (
|
||||
!selected ? (
|
||||
<div
|
||||
className="dark:text-white flex justify-between select-none truncate w-44 items-center px-4 my-1.5 border-x border-x-gray-300 dark:border-x-gray-600 -ml-px"
|
||||
onClick={onClick}
|
||||
>
|
||||
{flow.name}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeFlow(flow.id);
|
||||
}}
|
||||
>
|
||||
<XMarkIcon className="h-4 hover:bg-white dark:hover:bg-gray-600 rounded-full" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white dark:text-white dark:bg-gray-700 flex select-none justify-between w-44 items-center border border-b-0 border-gray-300 dark:border-gray-600 px-4 py-1 rounded-t-xl -ml-px">
|
||||
{isRename ? (
|
||||
<input
|
||||
autoFocus
|
||||
className="bg-transparent focus:border-none active:outline hover:outline focus:outline outline-gray-300 rounded-md w-28"
|
||||
onBlur={() => {
|
||||
setIsRename(false);
|
||||
if (value !== "") {
|
||||
let newFlow = _.cloneDeep(flow);
|
||||
newFlow.name = value;
|
||||
updateFlow(newFlow);
|
||||
}
|
||||
}}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="text-left truncate"
|
||||
onDoubleClick={() => {
|
||||
setIsRename(true);
|
||||
setValue(flow.name);
|
||||
}}
|
||||
>
|
||||
{flow.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
removeFlow(flow.id);
|
||||
}}
|
||||
>
|
||||
{flows.length > 1 && (
|
||||
<XMarkIcon className="h-4 hover:bg-gray-100 dark:hover:bg-gray-600 rounded-full" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<div className="h-full py-1.5 flex justify-center items-center">
|
||||
<button
|
||||
className="px-3 flex items-center h-full pb-0.5 pt-0.5 border-x-gray-300 dark:border-x-gray-600 dark:text-white -ml-px"
|
||||
onClick={onClick}
|
||||
>
|
||||
<PlusIcon className="h-5 rounded-full hover:bg-white dark:hover:bg-gray-600" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { useContext, useEffect, useState } from "react";
|
||||
import { ReactFlowProvider } from "reactflow";
|
||||
import TabComponent from "../tabComponent";
|
||||
import { TabsContext } from "../../../../contexts/tabsContext";
|
||||
import FlowPage from "../..";
|
||||
import { darkContext } from "../../../../contexts/darkContext";
|
||||
import { BellIcon, MoonIcon, SunIcon } from "@heroicons/react/24/outline";
|
||||
import { PopUpContext } from "../../../../contexts/popUpContext";
|
||||
import AlertDropdown from "../../../../alerts/alertDropDown";
|
||||
import { alertContext } from "../../../../contexts/alertContext";
|
||||
|
||||
export default function TabsManagerComponent() {
|
||||
const { flows, addFlow, tabIndex, setTabIndex } = useContext(TabsContext);
|
||||
const { openPopUp } = useContext(PopUpContext);
|
||||
const AlertWidth = 256
|
||||
const { dark, setDark } = useContext(darkContext);
|
||||
const {notificationCenter, setNotificationCenter} = useContext(alertContext)
|
||||
useEffect(() => {
|
||||
//create the first flow
|
||||
if (flows.length === 0) {
|
||||
addFlow();
|
||||
}
|
||||
}, [addFlow, flows.length]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col">
|
||||
<div className="w-full flex pr-2 flex-row text-center items-center bg-gray-100 dark:bg-gray-800 px-2">
|
||||
{flows.map((flow, index) => {
|
||||
return (
|
||||
<TabComponent
|
||||
onClick={() => setTabIndex(index)}
|
||||
selected={index === tabIndex}
|
||||
key={index}
|
||||
flow={flow}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<TabComponent
|
||||
onClick={() => {
|
||||
addFlow();
|
||||
}}
|
||||
selected={false}
|
||||
flow={null}
|
||||
/>
|
||||
<div className="ml-auto mr-2 flex gap-3">
|
||||
<button
|
||||
className="text-gray-400 hover:text-gray-500 "
|
||||
onClick={() => {
|
||||
setDark(!dark);
|
||||
}}
|
||||
>
|
||||
{dark ? (
|
||||
<SunIcon className="h-5 w-5" />
|
||||
) : (
|
||||
<MoonIcon className="h-5 w-5" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="text-gray-400 hover:text-gray-500 relative"
|
||||
onClick={(event: React.MouseEvent<HTMLElement>) => {
|
||||
setNotificationCenter(false)
|
||||
const top = (event.target as Element).getBoundingClientRect().top
|
||||
const left = (event.target as Element).getBoundingClientRect().left
|
||||
openPopUp(<div className="z-10 absolute" style={{top:top+20, left:left-AlertWidth}}><AlertDropdown/></div>)
|
||||
}}
|
||||
>
|
||||
{notificationCenter&&<div className='absolute w-1.5 h-1.5 rounded-full bg-red-600 right-[3px]'></div>}
|
||||
<BellIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<ReactFlowProvider>
|
||||
{flows[tabIndex] ? (
|
||||
<FlowPage flow={flows[tabIndex]}></FlowPage>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ReactFlowProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
205
src/frontend/src/pages/FlowPage/index.tsx
Normal file
205
src/frontend/src/pages/FlowPage/index.tsx
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
import { useCallback, useContext, useEffect, useRef } from "react";
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
addEdge,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useReactFlow,
|
||||
ControlButton,
|
||||
EdgeChange,
|
||||
Connection,
|
||||
} from "reactflow";
|
||||
import { locationContext } from "../../contexts/locationContext";
|
||||
import ExtraSidebar from "./components/extraSidebarComponent";
|
||||
import Chat from "../../components/chatComponent";
|
||||
import GenericNode from "../../CustomNodes/GenericNode";
|
||||
import { alertContext } from "../../contexts/alertContext";
|
||||
import { TabsContext } from "../../contexts/tabsContext";
|
||||
import { typesContext } from "../../contexts/typesContext";
|
||||
import {
|
||||
ArrowDownTrayIcon,
|
||||
ArrowUpTrayIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import ConnectionLineComponent from "./components/ConnectionLineComponent";
|
||||
import { FlowType, NodeType } from "../../types/flow";
|
||||
import { APIClassType } from "../../types/api";
|
||||
|
||||
const nodeTypes = {
|
||||
genericNode: GenericNode,
|
||||
};
|
||||
|
||||
var _ = require("lodash");
|
||||
|
||||
export default function FlowPage({ flow }:{flow:FlowType}) {
|
||||
let { updateFlow, incrementNodeId, downloadFlow, uploadFlow } =
|
||||
useContext(TabsContext);
|
||||
const { types, reactFlowInstance, setReactFlowInstance } =
|
||||
useContext(typesContext);
|
||||
const reactFlowWrapper = useRef(null);
|
||||
|
||||
const { setExtraComponent, setExtraNavigation } = useContext(locationContext);
|
||||
const { setErrorData } = useContext(alertContext);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(
|
||||
flow.data?.nodes ?? []
|
||||
);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(
|
||||
flow.data?.edges ?? []
|
||||
);
|
||||
const { setViewport } = useReactFlow();
|
||||
|
||||
useEffect(() => {
|
||||
if (reactFlowInstance && flow) {
|
||||
flow.data = reactFlowInstance.toObject();
|
||||
updateFlow(flow);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodes, edges]);
|
||||
//update flow when tabs change
|
||||
useEffect(() => {
|
||||
setNodes(flow?.data?.nodes ?? []);
|
||||
setEdges(flow?.data?.edges ?? []);
|
||||
if (reactFlowInstance) {
|
||||
setViewport(flow?.data?.viewport ?? { x: 1, y: 0, zoom: 0.5 });
|
||||
}
|
||||
}, [flow, reactFlowInstance, setEdges, setNodes, setViewport]);
|
||||
//set extra sidebar
|
||||
useEffect(() => {
|
||||
setExtraComponent(<ExtraSidebar />);
|
||||
setExtraNavigation({ title: "Components" });
|
||||
}, [setExtraComponent, setExtraNavigation]);
|
||||
|
||||
const onEdgesChangeMod = useCallback(
|
||||
(s:EdgeChange[]) => {
|
||||
onEdgesChange(s);
|
||||
setNodes((x) => {
|
||||
let newX = _.cloneDeep(x);
|
||||
return newX;
|
||||
});
|
||||
},
|
||||
[onEdgesChange, setNodes]
|
||||
);
|
||||
|
||||
const onConnect = useCallback(
|
||||
(params:Connection) => {
|
||||
setEdges((eds) =>
|
||||
addEdge({ ...params, className: "animate-pulse" }, eds)
|
||||
);
|
||||
setNodes((x) => {
|
||||
let newX = _.cloneDeep(x);
|
||||
return newX;
|
||||
});
|
||||
},
|
||||
[setEdges, setNodes]
|
||||
);
|
||||
|
||||
const onDragOver = useCallback((event:React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}, []);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(event:React.DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
// Helper function to generate a unique node ID
|
||||
function getId() {
|
||||
return `dndnode_` + incrementNodeId();
|
||||
}
|
||||
|
||||
// Get the current bounds of the ReactFlow wrapper element
|
||||
const reactflowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
|
||||
// Extract the data from the drag event and parse it as a JSON object
|
||||
let data:{type:string,node?:APIClassType} = JSON.parse(event.dataTransfer.getData("json"));
|
||||
|
||||
// If data type is not "chatInput" or if there are no "chatInputNode" nodes present in the ReactFlow instance, create a new node
|
||||
if (
|
||||
data.type !== "chatInput" ||
|
||||
(data.type === "chatInput" &&
|
||||
!reactFlowInstance.getNodes().some((n) => n.type === "chatInputNode"))
|
||||
) {
|
||||
// Calculate the position where the node should be created
|
||||
const position = reactFlowInstance.project({
|
||||
x: event.clientX - reactflowBounds.left,
|
||||
y: event.clientY - reactflowBounds.top,
|
||||
});
|
||||
|
||||
// Generate a unique node ID
|
||||
let newId = getId();
|
||||
|
||||
// Create a new node object
|
||||
const newNode:NodeType = {
|
||||
id: newId,
|
||||
type: "genericNode",
|
||||
position,
|
||||
data: {
|
||||
...data,
|
||||
id: newId,
|
||||
value: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Add the new node to the list of nodes in state
|
||||
setNodes((nds) => nds.concat(newNode));
|
||||
} else {
|
||||
// If a chat input node already exists, set an error message
|
||||
setErrorData({
|
||||
title: "Error creating node",
|
||||
list: ["There can't be more than one chat input."],
|
||||
});
|
||||
}
|
||||
},
|
||||
// Specify dependencies for useCallback
|
||||
[incrementNodeId, reactFlowInstance, setErrorData, setNodes]
|
||||
);
|
||||
|
||||
|
||||
const onDelete = (mynodes) => {
|
||||
setEdges(edges.filter((ns) => !nodes.some((n) => ns.source === n.id || ns.target === n.id)));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full h-full" ref={reactFlowWrapper}>
|
||||
{Object.keys(types).length > 0 ? (
|
||||
<>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
onMove={() =>
|
||||
updateFlow({ ...flow, data: reactFlowInstance.toObject() })
|
||||
}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChangeMod}
|
||||
onConnect={onConnect}
|
||||
onLoad={setReactFlowInstance}
|
||||
onInit={setReactFlowInstance}
|
||||
nodeTypes={nodeTypes}
|
||||
connectionLineComponent={ConnectionLineComponent}
|
||||
onDragOver={onDragOver}
|
||||
onDrop={onDrop}
|
||||
onNodesDelete={onDelete}
|
||||
>
|
||||
<Background className="dark:bg-gray-900"/>
|
||||
<Controls className="[&>button]:text-black [&>button]:dark:bg-gray-800 hover:[&>button]:dark:bg-gray-700 [&>button]:dark:text-gray-400 [&>button]:dark:fill-gray-400 [&>button]:dark:border-gray-600">
|
||||
<ControlButton
|
||||
onClick={() => uploadFlow()}
|
||||
>
|
||||
<ArrowUpTrayIcon />
|
||||
</ControlButton>
|
||||
|
||||
<ControlButton
|
||||
onClick={() => downloadFlow()}
|
||||
>
|
||||
<ArrowDownTrayIcon />
|
||||
</ControlButton>
|
||||
</Controls>
|
||||
</ReactFlow>
|
||||
<Chat flow={flow} reactFlowInstance={reactFlowInstance} />
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/frontend/src/reportWebVitals.ts
Normal file
15
src/frontend/src/reportWebVitals.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
12
src/frontend/src/types/alerts/index.ts
Normal file
12
src/frontend/src/types/alerts/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export type ErrorAlertType = {title:string,list:Array<string>,id:string,removeAlert:(id:string)=>void}
|
||||
export type NoticeAlertType = {title:string,link:string,id:string,removeAlert:(id:string)=>void}
|
||||
export type SuccessAlertType = {title:string,id:string, removeAlert:(id:string)=>void}
|
||||
export type SingleAlertComponentType = {dropItem:AlertItemType,removeAlert:(index:string)=>void}
|
||||
export type AlertDropdownType = {};
|
||||
export type AlertItemType = {
|
||||
type: "notice" | "error" | "success";
|
||||
title: string;
|
||||
link?: string;
|
||||
list?: Array<string>;
|
||||
id: string;
|
||||
};
|
||||
15
src/frontend/src/types/api/index.ts
Normal file
15
src/frontend/src/types/api/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Node,Edge,Viewport } from "reactflow"
|
||||
//kind and class are just representative names to represent the actual structure of the object received by the API
|
||||
|
||||
export type APIObjectType = {kind:APIKindType,[key:string]:APIKindType}
|
||||
export type APIKindType= {class:APIClassType,[key:string]:APIClassType}
|
||||
export type APITemplateType = {variable:TemplateVariableType,[key:string]:TemplateVariableType}
|
||||
export type APIClassType ={base_classes:Array<string>,description:string,template:APITemplateType,[key:string]:Array<string>|string|APITemplateType}
|
||||
export type TemplateVariableType = {type:string,required:boolean,placeholder?:string,list:boolean,show:boolean,multiline?:boolean,value?:any,[key:string]:any}
|
||||
export type sendAllProps={
|
||||
nodes: Node[];
|
||||
edges: Edge[];
|
||||
viewport: Viewport;
|
||||
message:string;
|
||||
chatHistory:{message:string,isSend:boolean}[],
|
||||
};
|
||||
5
src/frontend/src/types/chat/index.ts
Normal file
5
src/frontend/src/types/chat/index.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { ReactFlowInstance } from 'reactflow';
|
||||
import { FlowType } from "../flow";
|
||||
|
||||
export type ChatType = {flow:FlowType,reactFlowInstance:ReactFlowInstance}
|
||||
export type ChatMessageType = { message: string; isSend: boolean, thought?:string }
|
||||
59
src/frontend/src/types/components/index.ts
Normal file
59
src/frontend/src/types/components/index.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { ForwardRefExoticComponent, ReactElement, ReactNode } from "react";
|
||||
import { NodeDataType } from "../flow/index";
|
||||
export type InputComponentType = {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
password: boolean;
|
||||
};
|
||||
export type ToggleComponentType = {
|
||||
enabled: boolean;
|
||||
setEnabled: (state: boolean) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
export type DropDownComponentType = {
|
||||
title: string;
|
||||
value: string;
|
||||
options: string[];
|
||||
onSelect: (value: string) => void;
|
||||
};
|
||||
export type ParameterComponentType = {
|
||||
data: NodeDataType;
|
||||
title: string;
|
||||
id: string;
|
||||
color: string;
|
||||
left: boolean;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
tooltipTitle: string;
|
||||
};
|
||||
export type InputListComponentType = {
|
||||
value: string[];
|
||||
onChange: (value: string[]) => void;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export type TextAreaComponentType = {
|
||||
disabled: boolean;
|
||||
onChange: (value: string[] | string) => void;
|
||||
value: string[] | string;
|
||||
};
|
||||
|
||||
export type DisclosureComponentType = {
|
||||
children: ReactNode;
|
||||
button: {
|
||||
title: string;
|
||||
Icon: ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>;
|
||||
buttons?: {
|
||||
Icon: ReactElement;
|
||||
title: string;
|
||||
onClick: (event?: React.MouseEvent) => void;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
export type FloatComponentType = {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
3
src/frontend/src/types/entities/index.ts
Normal file
3
src/frontend/src/types/entities/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { HomeIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
export type sidebarNavigationItemType = { name: string, href: string, icon: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>, current: boolean }
|
||||
12
src/frontend/src/types/flow/index.ts
Normal file
12
src/frontend/src/types/flow/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { ChatMessageType } from './../chat/index';
|
||||
import { APIClassType } from '../api/index';
|
||||
import { ReactFlowJsonObject, XYPosition } from "reactflow";
|
||||
|
||||
export type FlowType = {
|
||||
name: string;
|
||||
id: string;
|
||||
data: ReactFlowJsonObject;
|
||||
chat: Array<ChatMessageType>;
|
||||
};
|
||||
export type NodeType = {id:string,type:string,position:XYPosition,data:NodeDataType}
|
||||
export type NodeDataType = {type:string,node?:APIClassType,id:string,value:any}
|
||||
15
src/frontend/src/types/tabs/index.ts
Normal file
15
src/frontend/src/types/tabs/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { FlowType } from "../flow";
|
||||
|
||||
export type TabsContextType = {
|
||||
tabIndex: number;
|
||||
setTabIndex: (index: number) => void;
|
||||
flows: Array<FlowType>;
|
||||
removeFlow: (id: string) => void;
|
||||
addFlow: (flowData?: any) => void;
|
||||
updateFlow: (newFlow: FlowType) => void;
|
||||
incrementNodeId: () => number;
|
||||
downloadFlow: () => void;
|
||||
uploadFlow: () => void;
|
||||
lockChat:boolean,
|
||||
setLockChat:(prevState:boolean)=>void
|
||||
};
|
||||
11
src/frontend/src/types/typesContext/index.ts
Normal file
11
src/frontend/src/types/typesContext/index.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import { ReactFlowInstance } from "reactflow";
|
||||
|
||||
const types:{[char: string]: string}={}
|
||||
|
||||
export type typesContextType = {
|
||||
reactFlowInstance: ReactFlowInstance|null;
|
||||
setReactFlowInstance: any;
|
||||
deleteNode: (idx: string) => void;
|
||||
types: typeof types;
|
||||
setTypes: (newState: {}) => void;
|
||||
};
|
||||
343
src/frontend/src/utils.ts
Normal file
343
src/frontend/src/utils.ts
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
import {
|
||||
RocketLaunchIcon,
|
||||
LinkIcon,
|
||||
CpuChipIcon,
|
||||
LightBulbIcon,
|
||||
CommandLineIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
ComputerDesktopIcon,
|
||||
Bars3CenterLeftIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { Connection, Edge, Node, ReactFlowInstance } from "reactflow";
|
||||
|
||||
export function classNames(...classes:Array<string>) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
export const textColors = {
|
||||
white: "text-white",
|
||||
red: "text-red-700",
|
||||
orange: "text-orange-700",
|
||||
amber: "text-amber-700",
|
||||
yellow: "text-yellow-700",
|
||||
lime: "text-lime-700",
|
||||
green: "text-green-700",
|
||||
emerald: "text-emerald-700",
|
||||
teal: "text-teal-700",
|
||||
cyan: "text-cyan-700",
|
||||
sky: "text-sky-700",
|
||||
blue: "text-blue-700",
|
||||
indigo: "text-indigo-700",
|
||||
violet: "text-violet-700",
|
||||
purple: "text-purple-700",
|
||||
fuchsia: "text-fuchsia-700",
|
||||
pink: "text-pink-700",
|
||||
rose: "text-rose-700",
|
||||
black: "text-black-700",
|
||||
gray: "text-gray-700",
|
||||
};
|
||||
|
||||
export const borderLColors = {
|
||||
white: "border-l-white",
|
||||
red: "border-l-red-500",
|
||||
orange: "border-l-orange-500",
|
||||
amber: "border-l-amber-500",
|
||||
yellow: "border-l-yellow-500",
|
||||
lime: "border-l-lime-500",
|
||||
green: "border-l-green-500",
|
||||
emerald: "border-l-emerald-500",
|
||||
teal: "border-l-teal-500",
|
||||
cyan: "border-l-cyan-500",
|
||||
sky: "border-l-sky-500",
|
||||
blue: "border-l-blue-500",
|
||||
indigo: "border-l-indigo-500",
|
||||
violet: "border-l-violet-500",
|
||||
purple: "border-l-purple-500",
|
||||
fuchsia: "border-l-fuchsia-500",
|
||||
pink: "border-l-pink-500",
|
||||
rose: "border-l-rose-500",
|
||||
black: "border-l-black-500",
|
||||
gray: "border-l-gray-500",
|
||||
};
|
||||
|
||||
export const nodeColors: {[char: string]: string} = {
|
||||
prompts: "#4367BF",
|
||||
llms: "#6344BE",
|
||||
chains: "#FE7500",
|
||||
agents: "#903BBE",
|
||||
tools: "#FF3434",
|
||||
memories: "#FF9135",
|
||||
advanced: "#000000",
|
||||
chat: "#454173",
|
||||
thought:"#272541"
|
||||
};
|
||||
|
||||
export const nodeNames:{[char: string]: string} = {
|
||||
prompts: "Prompts",
|
||||
llms: "LLMs",
|
||||
chains: "Chains",
|
||||
agents: "Agents",
|
||||
tools: "Tools",
|
||||
memories: "Memories",
|
||||
advanced: "Advanced",
|
||||
chat: "Chat",
|
||||
|
||||
};
|
||||
|
||||
export const nodeIcons:{[char: string]: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement>>} = {
|
||||
agents: RocketLaunchIcon,
|
||||
chains: LinkIcon,
|
||||
memories: CpuChipIcon,
|
||||
llms: LightBulbIcon,
|
||||
prompts: CommandLineIcon,
|
||||
tools: WrenchScrewdriverIcon,
|
||||
advanced: ComputerDesktopIcon,
|
||||
chat: Bars3CenterLeftIcon,
|
||||
};
|
||||
|
||||
export const bgColors = {
|
||||
white: "bg-white",
|
||||
red: "bg-red-100",
|
||||
orange: "bg-orange-100",
|
||||
amber: "bg-amber-100",
|
||||
yellow: "bg-yellow-100",
|
||||
lime: "bg-lime-100",
|
||||
green: "bg-green-100",
|
||||
emerald: "bg-emerald-100",
|
||||
teal: "bg-teal-100",
|
||||
cyan: "bg-cyan-100",
|
||||
sky: "bg-sky-100",
|
||||
blue: "bg-blue-100",
|
||||
indigo: "bg-indigo-100",
|
||||
violet: "bg-violet-100",
|
||||
purple: "bg-purple-100",
|
||||
fuchsia: "bg-fuchsia-100",
|
||||
pink: "bg-pink-100",
|
||||
rose: "bg-rose-100",
|
||||
black: "bg-black-100",
|
||||
gray: "bg-gray-100",
|
||||
};
|
||||
|
||||
export const bgColorsHover = {
|
||||
white: "hover:bg-white",
|
||||
black: "hover:bg-black-50",
|
||||
gray: "hover:bg-gray-50",
|
||||
red: "hover:bg-red-50",
|
||||
orange: "hover:bg-orange-50",
|
||||
amber: "hover:bg-amber-50",
|
||||
yellow: "hover:bg-yellow-50",
|
||||
lime: "hover:bg-lime-50",
|
||||
green: "hover:bg-green-50",
|
||||
emerald: "hover:bg-emerald-50",
|
||||
teal: "hover:bg-teal-50",
|
||||
cyan: "hover:bg-cyan-50",
|
||||
sky: "hover:bg-sky-50",
|
||||
blue: "hover:bg-blue-50",
|
||||
indigo: "hover:bg-indigo-50",
|
||||
violet: "hover:bg-violet-50",
|
||||
purple: "hover:bg-purple-50",
|
||||
fuchsia: "hover:bg-fuchsia-50",
|
||||
pink: "hover:bg-pink-50",
|
||||
rose: "hover:bg-rose-50",
|
||||
};
|
||||
|
||||
export const textColorsHex = {
|
||||
red: "rgb(185 28 28)",
|
||||
orange: "rgb(194 65 12)",
|
||||
amber: "rgb(180 83 9)",
|
||||
yellow: "rgb(161 98 7)",
|
||||
lime: "rgb(77 124 15)",
|
||||
green: "rgb(21 128 61)",
|
||||
emerald: "rgb(4 120 87)",
|
||||
teal: "rgb(15 118 110)",
|
||||
cyan: "rgb(14 116 144)",
|
||||
sky: "rgb(3 105 161)",
|
||||
blue: "rgb(29 78 216)",
|
||||
indigo: "rgb(67 56 202)",
|
||||
violet: "rgb(109 40 217)",
|
||||
purple: "rgb(126 34 206)",
|
||||
fuchsia: "rgb(162 28 175)",
|
||||
pink: "rgb(190 24 93)",
|
||||
rose: "rgb(190 18 60)",
|
||||
};
|
||||
|
||||
export const bgColorsHex = {
|
||||
red: "rgb(254 226 226)",
|
||||
orange: "rgb(255 237 213)",
|
||||
amber: "rgb(254 243 199)",
|
||||
yellow: "rgb(254 249 195)",
|
||||
lime: "rgb(236 252 203)",
|
||||
green: "rgb(220 252 231)",
|
||||
emerald: "rgb(209 250 229)",
|
||||
teal: "rgb(204 251 241)",
|
||||
cyan: "rgb(207 250 254)",
|
||||
sky: "rgb(224 242 254)",
|
||||
blue: "rgb(219 234 254)",
|
||||
indigo: "rgb(224 231 255)",
|
||||
violet: "rgb(237 233 254)",
|
||||
purple: "rgb(243 232 255)",
|
||||
fuchsia: "rgb(250 232 255)",
|
||||
pink: "rgb(252 231 243)",
|
||||
rose: "rgb(255 228 230)",
|
||||
};
|
||||
|
||||
export const taskTypeMap: { [key: string]: string } = {
|
||||
MULTICLASS_CLASSIFICATION: "Multiclass Classification",
|
||||
};
|
||||
|
||||
const charWidths:{[char: string]: number} = {
|
||||
" ": 0.2,
|
||||
"!": 0.2,
|
||||
'"': 0.3,
|
||||
"#": 0.5,
|
||||
$: 0.5,
|
||||
"%": 0.5,
|
||||
"&": 0.5,
|
||||
"(": 0.2,
|
||||
")": 0.2,
|
||||
"*": 0.5,
|
||||
"+": 0.5,
|
||||
",": 0.2,
|
||||
"-": 0.2,
|
||||
".": 0.1,
|
||||
"/": 0.5,
|
||||
":": 0.2,
|
||||
";": 0.2,
|
||||
"<": 0.5,
|
||||
"=": 0.5,
|
||||
">": 0.5,
|
||||
"?": 0.2,
|
||||
"@": 0.5,
|
||||
"[": 0.2,
|
||||
"\\": 0.5,
|
||||
"]": 0.2,
|
||||
"^": 0.5,
|
||||
_: 0.2,
|
||||
"`": 0.5,
|
||||
"{": 0.2,
|
||||
"|": 0.2,
|
||||
"}": 0.2,
|
||||
"~": 0.5,
|
||||
};
|
||||
|
||||
for (let i = 65; i <= 90; i++) {
|
||||
charWidths[String.fromCharCode(i)] = 0.6;
|
||||
}
|
||||
for (let i = 97; i <= 122; i++) {
|
||||
charWidths[String.fromCharCode(i)] = 0.5;
|
||||
}
|
||||
|
||||
export function measureTextWidth(text: string, fontSize:number) {
|
||||
let wordWidth = 0;
|
||||
for (let j = 0; j < text.length; j++) {
|
||||
let char = text[j];
|
||||
let charWidth = charWidths[char] || 0.5;
|
||||
wordWidth += charWidth * fontSize;
|
||||
}
|
||||
return wordWidth;
|
||||
}
|
||||
|
||||
export function measureTextHeight(text: string, width:number, fontSize:number) {
|
||||
const charHeight = fontSize;
|
||||
const lineHeight = charHeight * 1.5;
|
||||
const words = text.split(" ");
|
||||
let lineWidth = 0;
|
||||
let totalHeight = 0;
|
||||
for (let i = 0; i < words.length; i++) {
|
||||
let word = words[i];
|
||||
let wordWidth = measureTextWidth(word, fontSize);
|
||||
if (lineWidth + wordWidth + charWidths[" "] * fontSize <= width) {
|
||||
lineWidth += wordWidth + charWidths[" "] * fontSize;
|
||||
} else {
|
||||
totalHeight += lineHeight;
|
||||
lineWidth = wordWidth;
|
||||
}
|
||||
}
|
||||
totalHeight += lineHeight;
|
||||
return totalHeight;
|
||||
}
|
||||
|
||||
export function toCamelCase(str: string) {
|
||||
return str
|
||||
.split(" ")
|
||||
.map((word, index) =>
|
||||
index === 0
|
||||
? word.toLowerCase()
|
||||
: word[0].toUpperCase() + word.slice(1).toLowerCase()
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
export function toFirstUpperCase(str: string) {
|
||||
return str
|
||||
.split(" ")
|
||||
.map((word, index) => word[0].toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function snakeToNormalCase(str: string) {
|
||||
return str
|
||||
.split("_")
|
||||
.map((word, index) => {
|
||||
if (index === 0) {
|
||||
return word[0].toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
return word.toLowerCase();
|
||||
})
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
export function normalCaseToSnakeCase(str:string){
|
||||
return str
|
||||
.split(" ")
|
||||
.map((word, index) => {
|
||||
if (index === 0) {
|
||||
return word[0].toUpperCase() + word.slice(1).toLowerCase();
|
||||
}
|
||||
return word.toLowerCase();
|
||||
})
|
||||
.join("_");
|
||||
}
|
||||
|
||||
export function roundNumber(x:number, decimals:number) {
|
||||
return Math.round(x * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
}
|
||||
|
||||
export function getConnectedNodes(edge: Edge, nodes: Array<Node>): Array<Node> {
|
||||
const sourceId = edge.source;
|
||||
const targetId = edge.target;
|
||||
const connectedNodes = nodes.filter(
|
||||
(node) => node.id === targetId || node.id === sourceId
|
||||
);
|
||||
return connectedNodes;
|
||||
}
|
||||
|
||||
export function isValidConnection(
|
||||
{ source, target, sourceHandle, targetHandle }:Connection,
|
||||
reactFlowInstance:ReactFlowInstance
|
||||
) {
|
||||
if (
|
||||
sourceHandle.split('|')[0] === targetHandle.split("|")[0] ||
|
||||
sourceHandle.split('|').slice(2).some((t) => t === targetHandle.split("|")[0]) ||
|
||||
targetHandle.split("|")[0] === "str"
|
||||
) {
|
||||
let targetNode = reactFlowInstance.getNode(target).data.node;
|
||||
if (!targetNode) {
|
||||
if (
|
||||
!reactFlowInstance
|
||||
.getEdges()
|
||||
.find((e) => e.targetHandle === targetHandle)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} else if (
|
||||
(!targetNode.template[targetHandle.split("|")[1]].list &&
|
||||
!reactFlowInstance
|
||||
.getEdges()
|
||||
.find((e) => e.targetHandle === targetHandle)) ||
|
||||
targetNode.template[targetHandle.split("|")[1]].list
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue