diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index dffaa4665..f0a40a452 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -22,9 +22,11 @@ "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "axios": "^1.3.2", + "lodash": "^4.17.21", "react": "^18.2.0", "react-cookie": "^4.1.1", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.2", "react-icons": "^4.7.1", "react-laag": "^2.0.5", "react-router-dom": "^6.8.1", @@ -14959,6 +14961,17 @@ "react": "^18.2.0" } }, + "node_modules/react-error-boundary": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.0.2.tgz", + "integrity": "sha512-/h21OS80hQ1m/s5UVOp1JKkC8XmUo0rOTRUliGSmWtvswkbbijuQ074K0QLEHwxwwesTt7ksR74/9EHImqWo+A==", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-error-overlay": { "version": "6.0.11", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz", diff --git a/src/frontend/package.json b/src/frontend/package.json index 01be61167..312e52051 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -17,9 +17,11 @@ "@types/react": "^18.0.27", "@types/react-dom": "^18.0.10", "axios": "^1.3.2", + "lodash": "^4.17.21", "react": "^18.2.0", "react-cookie": "^4.1.1", "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.2", "react-icons": "^4.7.1", "react-laag": "^2.0.5", "react-router-dom": "^6.8.1", @@ -54,5 +56,5 @@ "last 1 safari version" ] }, - "proxy": "http://localhost:7860" -} \ No newline at end of file + "proxy": "http://backend:7860" +} diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index f0944be3f..0e5f1887d 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -9,6 +9,9 @@ import ExtraSidebar from "./components/ExtraSidebarComponent"; import { alertContext } from "./contexts/alertContext"; import { locationContext } from "./contexts/locationContext"; import TabsManagerComponent from "./pages/FlowPage/components/tabsManagerComponent"; +import { ErrorBoundary } from "react-error-boundary"; +import CrashErrorComponent from "./components/CrashErrorComponent"; +import { TabsContext } from "./contexts/tabsContext"; export default function App() { var _ = require("lodash"); @@ -21,7 +24,7 @@ export default function App() { setShowSideBar(true); setIsStackedOpen(true); }, [location.pathname, setCurrent, setIsStackedOpen, setShowSideBar]); - + const {hardReset} = useContext(TabsContext) const { errorData, errorOpen, @@ -34,45 +37,62 @@ export default function App() { setSuccessOpen, } = useContext(alertContext); -// Initialize state variable for the list of alerts -const [alertsList, setAlertsList] = useState,link?:string},id:string}>>([]); + // Initialize state variable for the list of alerts + const [alertsList, setAlertsList] = useState< + Array<{ + type: string; + data: { title: string; list?: Array; 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]); + // 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) => @@ -83,18 +103,27 @@ useEffect(() => { return ( //need parent component with width and height
-
-
-
- - {/* Main area */} -
- {/* Primary column */} -
- -
-
-
+
+ { + window.localStorage.removeItem("tabsData"); + window.localStorage.clear(); + hardReset() + window.location.href = window.location.href; + }} + FallbackComponent={CrashErrorComponent} + > +
+ + {/* Main area */} +
+ {/* Primary column */} +
+ +
+
+
+
{alertsList.map((alert) => ( @@ -126,7 +155,13 @@ useEffect(() => {
))}
- Created by Logspace + + Created by Logspace + ); } diff --git a/src/frontend/src/CustomNodes/GenericNode/index.tsx b/src/frontend/src/CustomNodes/GenericNode/index.tsx index acbdde477..ddb9524ee 100644 --- a/src/frontend/src/CustomNodes/GenericNode/index.tsx +++ b/src/frontend/src/CustomNodes/GenericNode/index.tsx @@ -1,92 +1,121 @@ +import { TrashIcon } from "@heroicons/react/24/outline"; import { - TrashIcon, -} from "@heroicons/react/24/outline"; -import { - classNames, - nodeColors, - nodeIcons, - snakeToNormalCase, + 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"; +import { useContext, useRef } from "react"; +import { NodeDataType } from "../../types/flow"; +import { alertContext } from "../../contexts/alertContext"; -export default function GenericNode({ data, selected}:{data:NodeDataType,selected:boolean}) { - const {types, deleteNode} = useContext(typesContext); - const Icon = nodeIcons[types[data.type]]; +export default function GenericNode({ + data, + selected, +}: { + data: NodeDataType; + selected: boolean; +}) { + const { setErrorData } = useContext(alertContext); + const showError = useRef(true); + const { types, deleteNode } = useContext(typesContext); + const Icon = nodeIcons[types[data.type]]; + if (!Icon) { + console.log(data); + if (showError.current) { + setErrorData({ + title: data.type + ? `The ${data.type} node could not be rendered, please review your json file` + : "There was a node that can't be rendered, please review your json file", + }); + showError.current = false; + } + return; + } + return ( +
+
+
+ +
{data.type}
+
+ +
- return ( -
-
-
- -
{data.type}
-
- -
+
+
+ {data.node.description} +
-
-
- {data.node.description} -
- - <> - {Object.keys(data.node.template) - .filter((t) => t.charAt(0) !== "_") - .map((t:string, idx) => ( -
- {idx === 0 ? ( -
Inputs:
- ) : ( - <> - )} - {data.node.template[t].show ? ( - - ) : ( - <> - )} -
- ))} -
Output:
- - -
-
- ); + <> + {Object.keys(data.node.template) + .filter((t) => t.charAt(0) !== "_") + .map((t: string, idx) => ( +
+ {idx === 0 ? ( +
+ Inputs: +
+ ) : ( + <> + )} + {data.node.template[t].show ? ( + + ) : ( + <> + )} +
+ ))} +
+ Output: +
+ + +
+
+ ); } diff --git a/src/frontend/src/components/CrashErrorComponent/index.tsx b/src/frontend/src/components/CrashErrorComponent/index.tsx new file mode 100644 index 000000000..7864e6d65 --- /dev/null +++ b/src/frontend/src/components/CrashErrorComponent/index.tsx @@ -0,0 +1,31 @@ +export default function CrashErrorComponent({ error, resetErrorBoundary }) { + return ( +
+
+

Oops! An unknown error has occurred.

+

+ Please click the 'Reset Application' button + to restore the application's state. If the error persists, please + create an issue on our GitHub page. We apologize for any inconvenience + this may have caused. +

+
+ + + Create Issue + +
+
+
+ ); +} diff --git a/src/frontend/src/components/chatComponent/index.tsx b/src/frontend/src/components/chatComponent/index.tsx index 27bf220f9..037c9cdaf 100644 --- a/src/frontend/src/components/chatComponent/index.tsx +++ b/src/frontend/src/components/chatComponent/index.tsx @@ -97,7 +97,7 @@ export default function Chat({ flow, reactFlowInstance }: ChatType) { setChatValue(""); addChatHistory(message, true); - sendAll({ ...reactFlowInstance.toObject(), message, chatHistory}) + sendAll({ ...reactFlowInstance.toObject(), message, chatHistory,name:flow.name,description:flow.description}) .then((r) => { addChatHistory(r.data.result, false, r.data.thought); setLockChat(false); diff --git a/src/frontend/src/contexts/index.tsx b/src/frontend/src/contexts/index.tsx index 6bb581f44..310606ea5 100644 --- a/src/frontend/src/contexts/index.tsx +++ b/src/frontend/src/contexts/index.tsx @@ -13,11 +13,13 @@ export default function ContextWrapper({ children }: { children: ReactNode }) { + - {children} + {children} + diff --git a/src/frontend/src/contexts/tabsContext.tsx b/src/frontend/src/contexts/tabsContext.tsx index 1bab447b8..10c0b943e 100644 --- a/src/frontend/src/contexts/tabsContext.tsx +++ b/src/frontend/src/contexts/tabsContext.tsx @@ -12,10 +12,11 @@ const TabsContextInitialValue: TabsContextType = { addFlow: (flowData?: any) => {}, updateFlow: (newFlow: FlowType) => {}, incrementNodeId: () => 0, - downloadFlow: () => {}, + downloadFlow: (flow:FlowType) => {}, uploadFlow: () => {}, lockChat: false, - setLockChat:(prevState:boolean)=>{} + setLockChat:(prevState:boolean)=>{}, + hardReset:()=>{} }; export const TabsContext = createContext( @@ -54,14 +55,18 @@ export function TabsProvider({ children }: { children: ReactNode }) { newNodeId.current = cookieObject.nodeId; } }, []); + function hardReset(){ + newNodeId.current=0; + setTabIndex(0);setFlows([]);setId(0); + } /** * Downloads the current flow as a JSON file */ - function downloadFlow() { + function downloadFlow(flow:FlowType) { // create a data URI with the current flow data const jsonString = `data:text/json;chatset=utf-8,${encodeURIComponent( - JSON.stringify(flows[tabIndex]) + JSON.stringify(flow) )}`; // create a link element and set its properties @@ -71,7 +76,7 @@ export function TabsProvider({ children }: { children: ReactNode }) { // 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."}) + setNoticeData({title:"Warning: Critical data,JSON file may including API keys."}) } /** @@ -128,9 +133,11 @@ export function TabsProvider({ children }: { children: ReactNode }) { 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; + const description = flow?.description?flow.description:"" // Create a new flow with a default name if no flow is provided. let newFlow: FlowType = { + description, name: "New Flow", id: id.toString(), data, @@ -158,6 +165,7 @@ export function TabsProvider({ children }: { children: ReactNode }) { const newFlows = [...prevState]; const index = newFlows.findIndex((flow) => flow.id === newFlow.id); if (index !== -1) { + newFlows[index].description = newFlow.description??"" newFlows[index].data = newFlow.data; newFlows[index].name = newFlow.name; newFlows[index].chat = newFlow.chat; @@ -169,6 +177,7 @@ export function TabsProvider({ children }: { children: ReactNode }) { return ( - - - - + + + + + + ); reportWebVitals(); diff --git a/src/frontend/src/modals/exportModal/index.tsx b/src/frontend/src/modals/exportModal/index.tsx new file mode 100644 index 000000000..c1da32946 --- /dev/null +++ b/src/frontend/src/modals/exportModal/index.tsx @@ -0,0 +1,174 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { + XMarkIcon, + ArrowDownTrayIcon, + DocumentDuplicateIcon, + ComputerDesktopIcon, +} from "@heroicons/react/24/outline"; +import { Fragment, useContext, useRef, useState } from "react"; +import { alertContext } from "../../contexts/alertContext"; +import { PopUpContext } from "../../contexts/popUpContext"; +import { TabsContext } from "../../contexts/tabsContext"; +import { removeApiKeys } from "../../utils"; + +export default function ExportModal() { + const [open, setOpen] = useState(true); + const { closePopUp } = useContext(PopUpContext); + const ref = useRef(); + const {setErrorData}= useContext(alertContext) + const { flows, tabIndex, updateFlow, downloadFlow } = useContext(TabsContext); + function setModalOpen(x: boolean) { + setOpen(x); + if (x === false) { + setTimeout(() => { + closePopUp(); + }, 300); + } + } + const [checked,setChecked] = useState(true) + return ( + + + +
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + Export as + +
+
+
+
+ + { + if(event.target.value!=""){ + let newFlow = flows[tabIndex]; + newFlow.name = event.target.value; + updateFlow(newFlow); + } + else{ + setErrorData({title:"Flow name can't be empty"}) + } + }} + type="text" + name="name" + value={flows[tabIndex].name ?? null} + placeholder="File name" + id="name" + className="focus:border focus:border-blue block w-full px-3 py-2 border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:focus:border-blue-500 dark:focus:ring-blue-500" + /> +
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ ); +} diff --git a/src/frontend/src/modals/importModal/buttonBox/index.tsx b/src/frontend/src/modals/importModal/buttonBox/index.tsx new file mode 100644 index 000000000..084981adc --- /dev/null +++ b/src/frontend/src/modals/importModal/buttonBox/index.tsx @@ -0,0 +1,47 @@ +import React, { ReactNode } from "react"; +import { DocumentDuplicateIcon } from "@heroicons/react/solid"; +import { classNames } from "../../../utils"; + +export default function ButtonBox({ + onClick, + title, + description, + icon, + bgColor, + textColor, + deactivate +}: { + onClick: () => void; + title: string; + description: string; + icon: ReactNode; + bgColor: string; + textColor: string; + deactivate?:boolean; +}) { + return ( + + ); +} diff --git a/src/frontend/src/modals/importModal/index.tsx b/src/frontend/src/modals/importModal/index.tsx new file mode 100644 index 000000000..c471f3955 --- /dev/null +++ b/src/frontend/src/modals/importModal/index.tsx @@ -0,0 +1,122 @@ +import { Dialog, Transition } from "@headlessui/react"; +import { + XMarkIcon, + ArrowDownTrayIcon, + DocumentDuplicateIcon, + ComputerDesktopIcon, + ArrowUpTrayIcon, +} from "@heroicons/react/24/outline"; +import { Fragment, useContext, useRef, useState } from "react"; +import { PopUpContext } from "../../contexts/popUpContext"; +import { TabsContext } from "../../contexts/tabsContext"; +import ButtonBox from "./buttonBox"; + +export default function ImportModal() { + const [open, setOpen] = useState(true); + const { closePopUp } = useContext(PopUpContext); + const ref = useRef(); + const {uploadFlow} = useContext(TabsContext) + function setModalOpen(x: boolean) { + setOpen(x); + if (x === false) { + setTimeout(() => { + closePopUp(); + }, 300); + } + } + return ( + + + +
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + Import from + +
+
+
+
+ + } + onClick={() => console.log("sdsds")} + textColor="text-slate-400" + title="Examples" + > + + } + onClick={() => {uploadFlow();setModalOpen(false)}} + textColor="text-blue-500" + title="Local file" + > +
+
+ +
+
+
+
+
+
+
+ ); +} diff --git a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx index bea739333..e489432de 100644 --- a/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/extraSidebarComponent/index.tsx @@ -55,7 +55,7 @@ export default function ExtraSidebar() { {Object.keys(data).map((d:keyof APIObjectType, i) => (
{Object.keys(data[d]).map((t: string, k) => ( @@ -63,7 +63,7 @@ export default function ExtraSidebar() {
onDragStart(event, { type: t, diff --git a/src/frontend/src/pages/FlowPage/components/tabsManagerComponent/index.tsx b/src/frontend/src/pages/FlowPage/components/tabsManagerComponent/index.tsx index 79240f720..71c964cbe 100644 --- a/src/frontend/src/pages/FlowPage/components/tabsManagerComponent/index.tsx +++ b/src/frontend/src/pages/FlowPage/components/tabsManagerComponent/index.tsx @@ -14,9 +14,12 @@ import { import { PopUpContext } from "../../../../contexts/popUpContext"; import AlertDropdown from "../../../../alerts/alertDropDown"; import { alertContext } from "../../../../contexts/alertContext"; +import ImportModal from "../../../../modals/importModal"; +import ExportModal from "../../../../modals/exportModal"; export default function TabsManagerComponent() { - const { flows, addFlow, tabIndex, setTabIndex, uploadFlow, downloadFlow } = useContext(TabsContext); + const { flows, addFlow, tabIndex, setTabIndex, uploadFlow, downloadFlow } = + useContext(TabsContext); const { openPopUp } = useContext(PopUpContext); const AlertWidth = 256; const { dark, setDark } = useContext(darkContext); @@ -50,10 +53,21 @@ export default function TabsManagerComponent() { flow={null} />
- -