-
+
Flows
{
/>
)
-}
+})
export default SideBar
diff --git a/frontend/src/consts.ts b/frontend/src/consts.ts
deleted file mode 100644
index 065e3443..00000000
--- a/frontend/src/consts.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import { conditionLabelType } from "./types/ConditionTypes"
-
-export const NODE_TYPES = {
- default_node: "default_node",
- // start_node: "start_node",
- // fallback_node: "fallback_node",
-}
-
-export const START_FALLBACK_NODE_FLAGS = ["start", "fallback"]
-export const START_NODE_FLAGS = ["start"]
-export const FALLBACK_NODE_FLAGS = ["fallback"]
-
-export const NODES = {
- default_node: {
- name: "Default Node",
- type: "default_node",
- conditions: [],
- global_conditions: [],
- local_conditions: [],
- response: {
- name: "default_response",
- type: "text",
- data: [{ text: "Default node response", priority: 1 }],
- },
- },
- // start_node: {
- // name: "Start Node",
- // type: "start_node",
- // conditions: [],
- // global_conditions: [],
- // local_conditions: [],
- // response: "Start response",
- // },
- // fallback_node: {
- // name: "Fallback Node",
- // type: "fallback_node",
- // conditions: [],
- // global_conditions: [],
- // local_conditions: [],
- // response: "Fallback response",
- // },
- link_node: {
- name: "Link",
- type: "link_node",
- conditions: [],
- global_conditions: [],
- local_conditions: [],
- response: {
- name: "Link response",
- type: "text",
- data: [{ text: "Link response", priority: 1 }],
- }
- },
-}
-
-export const FLOW_COLORS = [
- "#FF3333",
- "#FF9500",
- "#FFCC00",
- "#00CC99",
- "#3300FF",
- "#7000FF",
- "#CC66CC",
- "#FF3366",
-]
-
-export const CONDITION_LABELS: {
- [key: string]: conditionLabelType
-} = {
- manual: "manual",
- forward: "forward",
- backward: "backward",
- repeat: "repeat",
- fallback: "fallback",
- start: "start",
- previous: "previous",
-}
diff --git a/frontend/src/consts.tsx b/frontend/src/consts.tsx
new file mode 100644
index 00000000..0be0e0f0
--- /dev/null
+++ b/frontend/src/consts.tsx
@@ -0,0 +1,199 @@
+import { Code2, Text } from "lucide-react"
+import ButtonConditionIcon from "./icons/nodes/conditions/ButtonConditionIcon"
+import CodeConditionIcon from "./icons/nodes/conditions/CodeConditionIcon"
+import CustomConditionIcon from "./icons/nodes/conditions/CustomConditionIcon"
+import LLMConditionIcon from "./icons/nodes/conditions/LLMConditionIcon"
+import SlotsConditionIcon from "./icons/nodes/conditions/SlotsConditionIcon"
+import { conditionLabelType } from "./types/ConditionTypes"
+
+export const NODE_TYPES = {
+ default_node: "default_node",
+ // start_node: "start_node",
+ // fallback_node: "fallback_node",
+}
+
+export const NODE_NAMES = [
+ "Beginning of conversation",
+ "Weather discussion",
+ "Question about work",
+ "Talking about hobbies",
+ "Family talk",
+ "Weekend plans",
+ "Movie discussion",
+ "Exchanging opinions on books",
+ "Travel talk",
+ "Sports discussion",
+ "Talking about friends",
+ "News discussion",
+ "Music talk",
+ "Food discussion",
+ "Vacation plans",
+ "Pets discussion",
+ "Health talk",
+ "Technology discussion",
+ "Childhood story",
+ "Art talk",
+ "Future plans",
+ "Politics discussion",
+ "Culture talk",
+ "Education story",
+ "Social problems discussion",
+ "Evening plans",
+ "Relationship talk",
+ "Science talk",
+ "Interests story",
+ "Economy discussion",
+ "Year plans",
+ "History talk",
+ "Environmental talk",
+ "Work experience story",
+ "Philosophy talk",
+ "Month plans",
+ "Rest talk",
+ "Psychology discussion",
+ "Study story",
+ "Transportation discussion",
+ "Day plans",
+ "Society talk",
+ "Medicine discussion",
+ "Leisure story",
+ "Fashion discussion",
+ "Minute plans",
+ "Technology talk",
+ "Culinary discussion",
+ "Friends story",
+ "Music discussion",
+ "Second plans",
+ "Education talk",
+ "Literature discussion",
+ "Ideas story",
+ "Entertainment discussion",
+ "Hour plans",
+ "Economy talk",
+ "Science discussion",
+ "Experience story",
+ "Political discussion",
+ "Now plans",
+ "Sport talk",
+ "Movie discussion",
+ "Family story",
+ "Travel discussion",
+ "Second plans",
+ "Economic talk",
+ "Art discussion",
+ "Interest story",
+ "Finance discussion",
+ "Morning plans",
+ "Society talk",
+ "Medical discussion",
+ "Study story",
+ "Transportation discussion",
+ "Evening plans",
+ "Fashion talk",
+ "Psychology discussion",
+ "Leisure story",
+ "Entertainment discussion",
+ "Night plans",
+ "Literature talk",
+ "Culinary discussion",
+ "Technology story",
+ "Music discussion",
+ "Next year plans",
+ "Movie talk",
+ "Travel discussion",
+ "Family story",
+ "Sports talk",
+ "Plans for the next year",
+ "Book talk",
+ "Cooking discussion",
+ "Technology story",
+ "Music talk",
+ "Next year plans"
+]
+
+export const START_FALLBACK_NODE_FLAGS = ["start", "fallback"]
+export const START_NODE_FLAGS = ["start"]
+export const FALLBACK_NODE_FLAGS = ["fallback"]
+
+export const NODES = {
+ default_node: {
+ name: "Default Node",
+ type: "default_node",
+ dragHandle: '.custom-drag-handle',
+ conditions: [],
+ global_conditions: [],
+ local_conditions: [],
+ response: {
+ name: "default_response",
+ type: "text",
+ data: [{ text: "I am a bot and here is my quote ", priority: 1 }],
+ },
+ },
+ // start_node: {
+ // name: "Start Node",
+ // type: "start_node",
+ // conditions: [],
+ // global_conditions: [],
+ // local_conditions: [],
+ // response: "Start response",
+ // },
+ // fallback_node: {
+ // name: "Fallback Node",
+ // type: "fallback_node",
+ // conditions: [],
+ // global_conditions: [],
+ // local_conditions: [],
+ // response: "Fallback response",
+ // },
+ link_node: {
+ name: "Link",
+ type: "link_node",
+ dragHandle: '',
+ conditions: [],
+ global_conditions: [],
+ local_conditions: [],
+ response: {
+ name: "Link response",
+ type: "text",
+ data: [{ text: "Link response", priority: 1 }],
+ }
+ },
+}
+
+export const FLOW_COLORS = [
+ "#FF3333",
+ "#FF9500",
+ "#FFCC00",
+ "#00CC99",
+ "#3300FF",
+ "#7000FF",
+ "#CC66CC",
+ "#FF3366",
+]
+
+export const CONDITION_LABELS: {
+ [key: string]: conditionLabelType
+} = {
+ manual: "manual",
+ forward: "forward",
+ backward: "backward",
+ repeat: "repeat",
+ fallback: "fallback",
+ start: "start",
+ previous: "previous",
+}
+
+export const conditionTypeIcons = {
+ python:
,
+ custom:
,
+ slot:
,
+ button:
,
+ llm:
,
+}
+
+export const responseTypeIcons = {
+ python:
,
+ text:
,
+ llm:
+}
+
diff --git a/frontend/src/contexts/buildContext.tsx b/frontend/src/contexts/buildContext.tsx
index 5f098368..a83433a8 100644
--- a/frontend/src/contexts/buildContext.tsx
+++ b/frontend/src/contexts/buildContext.tsx
@@ -1,6 +1,5 @@
/* eslint-disable react-refresh/only-export-components */
-import { createContext, useEffect, useState } from "react"
-import toast from "react-hot-toast"
+import { createContext, useContext, useEffect, useState } from "react"
import { useSearchParams } from "react-router-dom"
import {
buildApiStatusType,
@@ -12,6 +11,7 @@ import {
get_builds,
localBuildType,
} from "../api/bot"
+import { notificationsContext } from "./notificationsContext"
type BuildContextType = {
build: boolean
@@ -53,6 +53,7 @@ export const BuildProvider = ({ children }: { children: React.ReactNode }) => {
const [searchParams, setSearchParams] = useSearchParams()
const [logsPage, setLogsPage] = useState(searchParams.get("logs_page") === "opened")
const [builds, setBuilds] = useState
([])
+ const { notification: n } = useContext(notificationsContext)
const setBuildsHandler = (builds: buildMinifyApiType[]) => {
setBuilds(() =>
@@ -93,7 +94,11 @@ export const BuildProvider = ({ children }: { children: React.ReactNode }) => {
if (timer > 15) {
setBuild(() => false)
setBuildStatus("failed")
- toast.error("Build timeout!")
+ n.add({
+ title: "Build timeout error!",
+ message: "",
+ type: "error",
+ })
await build_stop(start_res.build_id)
return (flag = false)
}
@@ -113,11 +118,19 @@ export const BuildProvider = ({ children }: { children: React.ReactNode }) => {
if (status === "completed") {
setBuildStatus("completed")
setBuild(() => true)
- toast.success("Build successfully!")
+ n.add({
+ title: "Build successfully!",
+ message: "",
+ type: "success",
+ })
} else if (status === "failed") {
setBuildStatus("failed")
setBuild(() => false)
- toast.error("Build failed!")
+ n.add({
+ title: "Build failed!",
+ message: "Unknown build error. Please check your script.",
+ type: "error",
+ })
}
}
await new Promise((resolve) => setTimeout(resolve, 1000))
diff --git a/frontend/src/contexts/chatContext.tsx b/frontend/src/contexts/chatContext.tsx
index 39212789..146443a2 100644
--- a/frontend/src/contexts/chatContext.tsx
+++ b/frontend/src/contexts/chatContext.tsx
@@ -1,5 +1,6 @@
import { createContext, useState } from "react"
import { useSearchParams } from "react-router-dom"
+import useLocalStorage from "../hooks/useLocalStorage"
export type messageType = {
message: string
@@ -21,7 +22,7 @@ export const ChatProvider = ({ children }: { children: React.ReactNode }) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [searchParams, setSearchParams] = useSearchParams()
const [chat, setChat] = useState(searchParams.get('chat') === 'opened')
- const [messages, setMessages] = useState([])
+ const [messages, setMessages] = useLocalStorage("chat_messages", [])
return (
{
const [tab, setTab] = useState(initialValue.tab)
const { flowId } = useParams()
const [flows, setFlows] = useState([])
+ const { notification: n } = useContext(notificationsContext)
useEffect(() => {
setTab(flowId || "")
@@ -105,7 +106,7 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
}
}
- useEffect(() => {
+ useEffect(() => {
getFlows()
}, [])
@@ -118,13 +119,13 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
setTimeout(async () => {
console.log("quiet save flows")
await saveFlows(flows)
- toast.success("DEBUG: Flows saved")
+ n.add({ message: "Flows saved", title: "DEBUG", type: "debug" })
}, 100)
}
- const getLocaleFlows = () => {
+ const getLocaleFlows = useCallback(() => {
return flows
- }
+ }, [flows])
const deleteFlow = useCallback(
(flow: FlowType) => {
@@ -144,13 +145,14 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
[flows]
)
- const deleteNode = (id: string) => {
+ const deleteNode = useCallback((id: string) => {
const flow = flows.find((flow) => flow.data.nodes.some((node) => node.id === id))
if (!flow) return -1
const deleted_node: NodeType = flow.data.nodes.find((node) => node.id === id) as NodeType
- if (deleted_node?.data.flags?.includes("start")) return toast.error("Can't delete start node")
- if (deleted_node?.id?.includes("LOCAL")) return toast.error("Can't delete local node")
- if (deleted_node?.id?.includes("GLOBAL")) return toast.error("Can't delete global node")
+ if (deleted_node?.data.flags?.includes("start"))
+ return n.add({ title: "Warning!", message: "Can't delete start node", type: "warning" })
+ if (deleted_node?.id?.includes("LOCAL")) return n.add({ title: "Warning!", message: "Can't delete local node", type: "warning" })
+ if (deleted_node?.id?.includes("GLOBAL")) return n.add({ title: "Warning!", message: "Can't delete global node", type: "warning" })
if (deleted_node?.data.flags?.includes("fallback")) {
console.log(
flow.data.nodes
@@ -170,9 +172,9 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
)
saveFlows(newFlows)
setFlows(newFlows)
- }
+ }, [flowId, flows, n])
- const deleteEdge = (id: string) => {
+ const deleteEdge = useCallback((id: string) => {
const flow = flows.find((flow) => flow.data.edges.some((edge) => edge.id === id))
if (!flow) return -1
const newEdges = flow.data.edges.filter((edge) => edge.id !== id)
@@ -182,15 +184,15 @@ export const FlowProvider = ({ children }: { children: React.ReactNode }) => {
)
)
setFlows((flows) => flows.map((flow) => ({ ...flow, data: { ...flow.data, edges: newEdges } })))
- }
+ }, [flowId, flows])
- const deleteObject = (id: string) => {
+ const deleteObject = useCallback((id: string) => {
const flow_node = flows.find((flow) => flow.data.nodes.some((node) => node.id === id))
const flow_edge = flows.find((flow) => flow.data.edges.some((edge) => edge.id === id))
if (!flow_node && !flow_edge) return -1
if (flow_node) deleteNode(id)
if (flow_edge) deleteEdge(id)
- }
+ }, [deleteEdge, deleteNode, flows])
return (
-
-
-
-
-
-
-
- {children}
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+
+
+
+
)
}
diff --git a/frontend/src/contexts/notificationsContext.tsx b/frontend/src/contexts/notificationsContext.tsx
new file mode 100644
index 00000000..f02b9964
--- /dev/null
+++ b/frontend/src/contexts/notificationsContext.tsx
@@ -0,0 +1,166 @@
+import classNames from "classnames"
+import { AlertOctagon, AlertTriangle, Bug, CheckCircle2, Info } from "lucide-react"
+import { createContext } from "react"
+import toast from "react-hot-toast"
+import useLocalStorage from "../hooks/useLocalStorage"
+
+export type notificationTypeType = "success" | "warning" | "error" | "info" | "debug"
+
+export type notificationType = {
+ title: string
+ message: string
+ type: notificationTypeType
+ duration: number
+ timestamp: number
+ stack: number
+}
+
+export type createNotificationType = {
+ title: string
+ message: string
+ type?: "success" | "warning" | "error" | "info" | "debug"
+ duration?: number
+ timestamp?: number
+ stack?: number
+}
+
+type notificationsContextType = {
+ notifications: notificationType[]
+ notification: {
+ add: (notification: createNotificationType) => void
+ delete: (timestamp: number) => void
+ clear: () => void
+ set: (notifications: notificationType[]) => void
+ }
+}
+
+export const notificationsContext = createContext({
+ notifications: [],
+ notification: {
+ add: () => {},
+ delete: () => {},
+ clear: () => {},
+ set: () => {},
+ },
+})
+
+const NotificationsProvider = ({ children }: { children: React.ReactNode }) => {
+ const [notifications, setNotifications] = useLocalStorage("notifications", [])
+
+ const notificationTypeColor = (type: string) => {
+ switch (type) {
+ case "success":
+ return "bg-[#ebf9f5] border-green-500"
+ case "warning":
+ return "bg-[#fff5ea] border-yellow-500"
+ case "error":
+ return "bg-[#ffebeb] border-red-500"
+ case "info":
+ return "bg-[#ebf4fa] border-blue-500"
+ case "debug":
+ return "bg-[#f5f5f5] border-neutral-500"
+ }
+ }
+
+ const notificationHeaderColor = (type: string) => {
+ switch (type) {
+ case "success":
+ return "text-black"
+ case "warning":
+ return "text-black"
+ case "error":
+ return "text-[#B20000]"
+ case "info":
+ return "text-black"
+ case "debug":
+ return "text-neutral-500"
+ }
+ }
+ const notificationTypeIcon = (type: string) => {
+ switch (type) {
+ case "success":
+ return
+ case "warning":
+ return
+ case "error":
+ return
+ case "info":
+ return
+ case "debug":
+ return
+ }
+ }
+
+ const addNotification = ({
+ message,
+ title,
+ type = "info",
+ duration = 5000,
+ timestamp = Date.now(),
+ stack = 1,
+ }: createNotificationType) => {
+ const color = notificationTypeColor(type)
+ const notification = { title, message, type, timestamp, stack, duration }
+ setNotifications((prevNotifications) => [...prevNotifications, notification])
+ toast.custom(
+ (t) => (
+
+
+
+ {notificationTypeIcon(notification.type)}
+
+ {notification.title}
+
+
+ {notification.message && (
+
{notification.message}
+ )}
+
+
+ ),
+ {
+ id: message,
+ }
+ )
+ }
+
+ const deleteNotification = (timestamp: number) => {
+
+ setNotifications((prevNotifications) =>
+ prevNotifications.filter((notification) => notification.timestamp !== timestamp)
+ )
+ }
+
+ const clearNotifications = () => {
+ setNotifications([])
+ }
+
+ const notification = {
+ add: addNotification,
+ delete: deleteNotification,
+ clear: clearNotifications,
+ set: setNotifications,
+ }
+
+ return (
+
+ {children}
+
+ )
+}
+
+export default NotificationsProvider
diff --git a/frontend/src/contexts/runContext.tsx b/frontend/src/contexts/runContext.tsx
index 7d787035..fb49a0c0 100644
--- a/frontend/src/contexts/runContext.tsx
+++ b/frontend/src/contexts/runContext.tsx
@@ -1,5 +1,4 @@
import { createContext, useContext, useEffect, useState } from "react"
-import toast from "react-hot-toast"
import {
buildApiStatusType,
buildPresetType,
@@ -12,6 +11,7 @@ import {
run_stop,
} from "../api/bot"
import { buildContext } from "./buildContext"
+import { notificationsContext } from "./notificationsContext"
export type runApiType = {
id: number
@@ -58,6 +58,7 @@ export const RunProvider = ({ children }: { children: React.ReactNode }) => {
const [runStatus, setRunStatus] = useState("stopped")
const [runs, setRuns] = useState([])
const { setBuildsHandler, builds: context_builds } = useContext(buildContext)
+ const { notification: n } = useContext(notificationsContext)
const setRunsHandler = (runs: runMinifyApiType[]) => {
setRuns(runs.map((run) => ({ ...run, type: "run" })))
@@ -70,7 +71,7 @@ export const RunProvider = ({ children }: { children: React.ReactNode }) => {
return { ...run, type: "run" }
})
setRuns(_runs)
- if (_runs[_runs.length - 1].status === "running") {
+ if (_runs[_runs.length - 1].status === "alive") {
setRun(_runs[_runs.length - 1])
setRunStatus("alive")
}
@@ -108,7 +109,11 @@ export const RunProvider = ({ children }: { children: React.ReactNode }) => {
if (timer > 9999) {
setRunPending(() => false)
setRunStatus("failed")
- toast.error("Run timeout error!")
+ n.add({
+ title: "Run timeout error!",
+ message: "",
+ type: "error",
+ })
return (flag = false)
}
const { status } = await run_status(started_run.id)
@@ -117,11 +122,15 @@ export const RunProvider = ({ children }: { children: React.ReactNode }) => {
setRunPending(false)
setRunStatus("alive")
}
- if (status === 'failed') {
+ if (status === "failed") {
flag = false
setRunPending(false)
setRunStatus("failed")
- toast.error("Run failed!")
+ n.add({
+ message: "Unknown run error. Please check your script.",
+ title: "Run failed!",
+ type: "error",
+ })
}
await new Promise((resolve) => setTimeout(resolve, 500))
}
@@ -146,7 +155,11 @@ export const RunProvider = ({ children }: { children: React.ReactNode }) => {
setRunsHandler(runs)
setRunStatus("stopped")
setRunPending(() => false)
- toast.success("Run stopped!")
+ n.add({
+ message: "",
+ title: "Run stopped!",
+ type: "info",
+ })
}, 500)
}
} catch (error) {
diff --git a/frontend/src/contexts/themeContext.tsx b/frontend/src/contexts/themeContext.tsx
index 88acfc2e..8a1ef4f0 100644
--- a/frontend/src/contexts/themeContext.tsx
+++ b/frontend/src/contexts/themeContext.tsx
@@ -21,8 +21,8 @@ export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
useEffect(() => {
const _theme = localStorage.getItem("theme") as themeType
- setTheme(_theme)
- document.body.classList.add(_theme)
+ setTheme(_theme ?? "dark")
+ document.body.classList.add(_theme ?? "dark")
}, [])
const toggleTheme = useCallback(() => {
diff --git a/frontend/src/contexts/workspaceContext.tsx b/frontend/src/contexts/workspaceContext.tsx
index 1661f0c3..41381b6e 100644
--- a/frontend/src/contexts/workspaceContext.tsx
+++ b/frontend/src/contexts/workspaceContext.tsx
@@ -1,10 +1,11 @@
/* eslint-disable react-refresh/only-export-components */
-import { createContext, useContext, useEffect, useState } from "react"
+import { createContext, useCallback, useContext, useEffect, useState } from "react"
import { useSearchParams } from "react-router-dom"
import { Node } from "reactflow"
import { FlowType } from "../types/FlowTypes"
import { NodeDataType } from "../types/NodeTypes"
import { flowContext } from "./flowContext"
+import { notificationsContext } from "./notificationsContext"
type WorkspaceContextType = {
workspaceMode: boolean
@@ -60,11 +61,12 @@ export const WorkspaceProvider = ({ children }: { children: React.ReactNode }) =
const [nodesLayoutMode, setNodesLayoutMode] = useState(false)
const [managerMode, setManagerMode] = useState(false)
const [searchParams, setSearchParams] = useSearchParams()
- const [settingsPage, setSettingsPage] = useState(searchParams.get('settings') === 'opened')
+ const [settingsPage, setSettingsPage] = useState(searchParams.get("settings") === "opened")
const [selectedNode, setSelectedNode] = useState("")
const { updateFlow, flows, tab, quietSaveFlows, setFlows } = useContext(flowContext)
const [mouseOnPane, setMouseOnPane] = useState(true)
const [modalsOpened, setModalsOpened] = useState(0)
+ const { notification: n } = useContext(notificationsContext)
useEffect(() => {
console.log(modalsOpened)
@@ -80,24 +82,39 @@ export const WorkspaceProvider = ({ children }: { children: React.ReactNode }) =
setModalsOpened(0)
}
}, [modalsOpened])
-
+
useEffect(() => console.log(mouseOnPane), [mouseOnPane])
const flow = flows.find((flow) => flow.name === tab)
- const toggleWorkspaceMode = () => {
+ const toggleWorkspaceMode = useCallback(() => {
setWorkspaceMode(() => !workspaceMode)
- }
+ n.add({
+ message: `Workspace mode is now ${workspaceMode ? "fixed" : "free"}.`,
+ title: "Workspace mode changed!",
+ type: "info",
+ })
+ }, [n, workspaceMode])
- const toggleNodesLayoutMode = () => {
+ const toggleNodesLayoutMode = useCallback(() => {
setNodesLayoutMode(() => !nodesLayoutMode)
- }
+ n.add({
+ message: `Nodes layout mode is now ${!nodesLayoutMode ? "on" : "off"}.`,
+ title: "Layout mode changed!",
+ type: "info",
+ })
+ }, [n, nodesLayoutMode])
- const toggleManagerMode = () => {
+ const toggleManagerMode = useCallback(() => {
setManagerMode(() => !managerMode)
- }
+ n.add({
+ message: `Manager mode is now ${!managerMode ? "on" : "off"}.`,
+ title: "Mode changed!",
+ type: "info",
+ })
+ }, [managerMode, n])
- const handleNodeFlags = (e: React.MouseEvent) => {
+ const handleNodeFlags = useCallback((e: React.MouseEvent) => {
const nodes = flows.flatMap((flow) => flow.data.nodes)
console.log(nodes)
const new_nds = nodes.map((nd: Node) => {
@@ -127,23 +144,22 @@ export const WorkspaceProvider = ({ children }: { children: React.ReactNode }) =
},
}
})
- setFlows(new_flows)
-
- if (flow) {
- updateFlow(flow)
- }
+ setFlows(() => new_flows)
+ // if (flow) {
+ // updateFlow(flow)
+ // }
quietSaveFlows()
- }
+ }, [flows, quietSaveFlows, selectedNode, setFlows])
- const onModalOpen = (onOpen: () => void) => {
+ const onModalOpen = useCallback((onOpen: () => void) => {
setMouseOnPane(false)
onOpen()
- }
+ }, [])
- const onModalClose = (onClose: () => void) => {
+ const onModalClose = useCallback((onClose: () => void) => {
setMouseOnPane(true)
onClose()
- }
+ }, [])
return (
{
return (
)
diff --git a/frontend/src/icons/nodes/conditions/ButtonConditionIcon.tsx b/frontend/src/icons/nodes/conditions/ButtonConditionIcon.tsx
new file mode 100644
index 00000000..5c29e853
--- /dev/null
+++ b/frontend/src/icons/nodes/conditions/ButtonConditionIcon.tsx
@@ -0,0 +1,23 @@
+import React from "react"
+
+const ButtonConditionIcon = ({className, stroke="var(--foreground)"}: React.SVGAttributes) => {
+ return (
+
+ )
+}
+
+export default ButtonConditionIcon
diff --git a/frontend/src/icons/nodes/conditions/CodeConditionIcon.tsx b/frontend/src/icons/nodes/conditions/CodeConditionIcon.tsx
new file mode 100644
index 00000000..933c473a
--- /dev/null
+++ b/frontend/src/icons/nodes/conditions/CodeConditionIcon.tsx
@@ -0,0 +1,37 @@
+import React from "react"
+
+const CodeConditionIcon = ({className, fill="var(--foreground)"}: React.SVGAttributes) => {
+ return (
+
+ )
+}
+
+export default CodeConditionIcon
diff --git a/frontend/src/icons/nodes/conditions/CustomConditionIcon.tsx b/frontend/src/icons/nodes/conditions/CustomConditionIcon.tsx
new file mode 100644
index 00000000..e3c7c005
--- /dev/null
+++ b/frontend/src/icons/nodes/conditions/CustomConditionIcon.tsx
@@ -0,0 +1,24 @@
+import React from "react"
+
+const CustomConditionIcon = ({ className, fill, stroke="var(--foreground)" }: React.SVGAttributes) => {
+ return (
+
+ )
+}
+
+export default CustomConditionIcon
diff --git a/frontend/src/icons/nodes/conditions/LLMConditionIcon.tsx b/frontend/src/icons/nodes/conditions/LLMConditionIcon.tsx
new file mode 100644
index 00000000..7b981cc6
--- /dev/null
+++ b/frontend/src/icons/nodes/conditions/LLMConditionIcon.tsx
@@ -0,0 +1,23 @@
+import React from "react"
+
+const LLMConditionIcon = ({className, fill="var(--foreground)"}: React.SVGAttributes) => {
+ return (
+
+ )
+}
+
+export default LLMConditionIcon
diff --git a/frontend/src/icons/nodes/conditions/SlotsConditionIcon.tsx b/frontend/src/icons/nodes/conditions/SlotsConditionIcon.tsx
new file mode 100644
index 00000000..e15bc4ee
--- /dev/null
+++ b/frontend/src/icons/nodes/conditions/SlotsConditionIcon.tsx
@@ -0,0 +1,26 @@
+import React from "react"
+
+const SlotsConditionIcon = ({
+ className,
+ stroke = "black",
+}: React.SVGAttributes) => {
+ return (
+
+ )
+}
+
+export default SlotsConditionIcon
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 16066da0..86430117 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -3,14 +3,14 @@
@tailwind utilities;
@font-face {
- font-family: 'Inter';
+ font-family: "Inter";
src: url(../public/Inter-VariableFont.ttf);
}
@layer base {
:root {
font-family:
- 'Inter',
+ "Inter",
system-ui,
-apple-system,
BlinkMacSystemFont,
@@ -34,13 +34,13 @@
--background: #fff;
--foreground: #191919;
- --bg-secondary: #f8fafc;
+ --bg-secondary: #F9FAFC;
--fg-secondary: #333;
--chat-background: #ebf5fa;
--node: #fff;
- --node-header: #f8fafc;
+ --node-header: #F9FAFC;
--border: #e4e8ee;
--border-node-selected: #94a3b8;
@@ -86,8 +86,20 @@
--status-green: #00cc9a;
--status-blue: #2563eb;
- .dark {
+ --condition-test-success: #00cc9933;
+ --condition-test-error: #ff333333;
+
+ --node-start-label-bg: #e5faf5;
+ --node-start-label-fg: #00cc99;
+
+ --node-fallback-label-bg: #fde9e9;
+ --node-fallback-label-fg: #ff3333;
+
+ --logo-red: #E86C4A;
+ --logo-yellow: #FFC247;
+ --logo-blue: #00A3FF;
+ .dark {
color: #ebebeb;
--background: #212121;
@@ -128,6 +140,20 @@
--info-background: #172554;
--condition-default: rgba(51, 153, 204, 1);
+
+ --node-start-label-bg: #00cc99;
+ --node-start-label-fg: #e5faf5;
+
+ --node-fallback-label-bg: #ff3333;
+ --node-fallback-label-fg: #fde9e9;
+
+ /* --condition-test-success: #00cc99;
+ --condition-test-error: #ff3333; */
+
+ --logo-red: #E55934;
+ --logo-yellow: #FFBB33;
+ --logo-blue: #018BD9;
+
}
}
}
@@ -138,7 +164,7 @@
margin: 0;
color: var(--text);
font-family:
- 'Inter',
+ "Inter",
system-ui,
-apple-system,
BlinkMacSystemFont,
@@ -188,6 +214,16 @@ body {
@apply border-node-selected;
}
+.border-error {
+ /* --tw-border-opacity: 1;
+ border-color: rgb(239 68 68 / var(--tw-border-opacity)) !important; */
+ @apply border-red-500
+}
+
+.selected .border-error {
+ @apply border-red-800;
+}
+
.header-service-btn {
@apply w-[34px] h-[34px] min-w-[34px] min-h-[34px] bg-transparent border border-transparent hover:bg-header-btn-hover hover:border-border rounded;
}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 6f6eb50d..6c63518f 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,5 +1,10 @@
+import { StrictMode } from "react"
import ReactDOM from "react-dom/client"
import App from "./App.tsx"
import "./index.css"
-ReactDOM.createRoot(document.getElementById("root")!).render()
+ReactDOM.createRoot(document.getElementById("root")!).render(
+
+
+
+)
diff --git a/frontend/src/modals/ConditionModal/ConditionModal.tsx b/frontend/src/modals/ConditionModal/ConditionModal.tsx
index e364d286..649e6204 100644
--- a/frontend/src/modals/ConditionModal/ConditionModal.tsx
+++ b/frontend/src/modals/ConditionModal/ConditionModal.tsx
@@ -12,12 +12,12 @@ import {
import classNames from "classnames"
import { HelpCircle, TrashIcon } from "lucide-react"
import { useContext, useEffect, useMemo, useState } from "react"
-import toast from "react-hot-toast"
import { useParams } from "react-router-dom"
import { useReactFlow } from "reactflow"
import { lint_service } from "../../api/services"
import ModalComponent from "../../components/ModalComponent"
import { flowContext } from "../../contexts/flowContext"
+import { notificationsContext } from "../../contexts/notificationsContext"
import { conditionType, conditionTypeType } from "../../types/ConditionTypes"
import { NodeDataType, NodeType } from "../../types/NodeTypes"
import { generateNewConditionBase } from "../../utils"
@@ -62,6 +62,7 @@ const ConditionModal = ({
}
const { getNode, setNodes, getNodes } = useReactFlow()
+ const { notification: n } = useContext(notificationsContext)
const { updateFlow, flows, quietSaveFlows } = useContext(flowContext)
const { flowId } = useParams()
@@ -284,7 +285,11 @@ const ConditionModal = ({
)
} else {
if (!validate_name.status) {
- toast.error(`Condition name is not valid: \n ${validate_name.reason}`)
+ n.add({
+ title: "Saving error!",
+ message: `Condition name is not valid: \n ${validate_name.reason}`,
+ type: "error",
+ })
}
}
}
@@ -344,8 +349,9 @@ const ConditionModal = ({
)}
-
+
setCurrentCondition({ ...currentCondition, name: e.target.value })}
/>
+ setCurrentCondition({ ...currentCondition, data: {
+ ...currentCondition.data,
+ priority: parseInt(e.target.value)
+ } })}
+ />
{bodyItems[selected]}
@@ -366,7 +384,7 @@ const ConditionModal = ({
{lintStatus?.status == "ok" ? "Condition test passed!" : lintStatus?.message}
diff --git a/frontend/src/modals/FlowModal/CreateFlowModal.tsx b/frontend/src/modals/FlowModal/CreateFlowModal.tsx
index 199753c3..d167bd76 100644
--- a/frontend/src/modals/FlowModal/CreateFlowModal.tsx
+++ b/frontend/src/modals/FlowModal/CreateFlowModal.tsx
@@ -7,14 +7,14 @@ import {
ModalFooter,
ModalHeader,
Select,
- SelectItem
+ SelectItem,
} from "@nextui-org/react"
import { HelpCircle } from "lucide-react"
import { useContext, useState } from "react"
-import toast from "react-hot-toast"
import ModalComponent from "../../components/ModalComponent"
import { FLOW_COLORS } from "../../consts"
import { flowContext } from "../../contexts/flowContext"
+import { notificationsContext } from "../../contexts/notificationsContext"
import { ModalType } from "../../types/ModalTypes"
import { generateNewFlow, validateFlowName } from "../../utils"
@@ -29,6 +29,7 @@ export type CreateFlowType = {
const CreateFlowModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProps) => {
const { flows, setFlows, saveFlows } = useContext(flowContext)
+ const { notification: n } = useContext(notificationsContext)
const [flow, setFlow] = useState
({
name: "",
description: "",
@@ -45,7 +46,14 @@ const CreateFlowModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProps
}
const onFlowSave = () => {
- if (validateFlowName(flow.name, flows) && flow.color && flow.subflow) {
+ if (!validateFlowName(flow.name, flows)) {
+ return n.add({
+ title: "Warning!",
+ message: "Flow name is not valid.",
+ type: "warning",
+ })
+ }
+ if (flow.color && flow.subflow) {
const newFlow = generateNewFlow(flow)
setFlows([...flows, newFlow])
saveFlows([...flows, newFlow])
@@ -58,7 +66,11 @@ const CreateFlowModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProps
setIsSubFlow(false)
onClose()
} else {
- toast.error("Please fill all the fields correctly!")
+ n.add({
+ title: "Creating error!",
+ message: "Please fill all the fields correctly.",
+ type: "error",
+ })
}
}
@@ -81,6 +93,7 @@ const CreateFlowModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProps
name='name'
onChange={onFlowChange}
value={flow.name}
+ min={2}
/>
{
const { flows, setFlows, saveFlows } = useContext(flowContext)
+ const { notification: n } = useContext(notificationsContext)
const [newFlows, setNewFlows] = useState([...flows] ?? [])
const { flowId } = useParams()
const [flow, setFlow] = useState(
newFlows.find((_flow) => _flow.name === flowId) ?? [][0]
)
+ const [newFlow, setNewFlow] = useState(flow)
+
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [isSubFlow, setIsSubFlow] = useState(false)
const [isGlobal, setIsGlobal] = useState(false)
@@ -45,17 +49,23 @@ const ManageFlowsModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProp
useEffect(() => {
setFlow(() => newFlows.find((_flow) => _flow.name === flowId) ?? [][0])
+ setNewFlow(() => newFlows.find((_flow) => _flow.name === flowId) ?? [][0])
+
if (flowId === "Global") {
setIsGlobal(true)
}
}, [flowId, newFlows, isOpen])
const onFlowSelect = (e: React.MouseEvent) => {
- setFlow(newFlows.find((_flow) => _flow.name === e.currentTarget.name) ?? [][0])
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ setFlow(() => newFlows.find((_flow) => _flow.name === e.target.name) ?? [][0])
}
+
useEffect(() => {
if (flow) {
+ setNewFlow(flow)
if (flow.name === "Global") {
setIsGlobal(true)
} else {
@@ -65,20 +75,34 @@ const ManageFlowsModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProp
}, [flow])
const onFlowChange = (e: React.ChangeEvent) => {
- setFlow({
- ...flow,
+ setNewFlow({
+ ...newFlow,
[e.target.name]: e.target.value,
})
}
const onFlowSave = () => {
- if (validateFlowName(flow.name, newFlows) && flow.color && flow.subflow) {
- setFlows([...newFlows.map((_flow) => (_flow.id === flow.id ? flow : _flow))])
- saveFlows([...newFlows.map((_flow) => (_flow.id === flow.id ? flow : _flow))])
+ if (!validateFlowName(newFlow.name, newFlows) || newFlow.name === flow.name) {
+ return n.add({
+ title: "Warning!",
+ message: "Flow name is not valid.",
+ type: "warning",
+ })
+ }
+ if (
+ newFlow.color &&
+ newFlow.subflow
+ ) {
+ setFlows([...newFlows.map((_flow) => (_flow.id === flow.id ? newFlow : _flow))])
+ saveFlows([...newFlows.map((_flow) => (_flow.id === flow.id ? newFlow : _flow))])
setIsSubFlow(false)
onClose()
} else {
- toast.error("Please fill all the fields correct!")
+ n.add({
+ title: "Warning!",
+ message: "Please fill all the fields correctly.",
+ type: "warning",
+ })
}
}
@@ -118,7 +142,8 @@ const ManageFlowsModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProp
placeholder="Enter flow's name here"
name='name'
onChange={onFlowChange}
- value={flow.name}
+ value={newFlow.name}
+ min={2}
/>
@@ -137,11 +162,11 @@ const ManageFlowsModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProp
))}
@@ -174,12 +199,12 @@ const ManageFlowsModal = ({ isOpen, onClose, size = "3xl" }: CreateFlowModalProp
name='subflow'
onChange={(e) => {
if (e.target.value !== "") {
- setFlow({ ...flow, subflow: e.target.value })
+ setNewFlow({ ...newFlow, subflow: e.target.value })
} else {
- setFlow({ ...flow, subflow: "Global" })
+ setNewFlow({ ...newFlow, subflow: "Global" })
}
}}
- selectedKeys={flow.subflow ? [flow.subflow] : []}>
+ selectedKeys={newFlow.subflow ? [newFlow.subflow] : []}>
{(flow) =>
{flow.name}}
diff --git a/frontend/src/modals/NodeModal/NodeModal.tsx b/frontend/src/modals/NodeModal/NodeModal.tsx
index f4721ddc..9149feed 100644
--- a/frontend/src/modals/NodeModal/NodeModal.tsx
+++ b/frontend/src/modals/NodeModal/NodeModal.tsx
@@ -6,16 +6,14 @@ import {
ModalFooter,
ModalHeader,
ModalProps,
- useDisclosure,
} from "@nextui-org/react"
import { HelpCircle, TrashIcon } from "lucide-react"
-import React, { useCallback, useContext, useEffect, useState } from "react"
+import React, { useCallback, useContext, useEffect } from "react"
import { useReactFlow } from "reactflow"
import ModalComponent from "../../components/ModalComponent"
import { flowContext } from "../../contexts/flowContext"
import EditPenIcon from "../../icons/EditPenIcon"
import { NodeDataType } from "../../types/NodeTypes"
-import ResponseModal from "../ResponseModal/ResponseModal"
import ConditionRow from "./components/ConditionRow"
type NodeModalProps = {
@@ -23,18 +21,22 @@ type NodeModalProps = {
size?: ModalProps["size"]
isOpen: boolean
onClose: () => void
+ onResponseModalOpen: () => void
+ nodeDataState: NodeDataType
+ setNodeDataState: React.Dispatch>
}
-const NodeModal = ({ data, isOpen, onClose, size = "3xl" }: NodeModalProps) => {
+const NodeModal = ({
+ data,
+ isOpen,
+ onClose,
+ size = "3xl",
+ onResponseModalOpen,
+ nodeDataState,
+ setNodeDataState,
+}: NodeModalProps) => {
const { getNodes, setNodes } = useReactFlow()
const { quietSaveFlows } = useContext(flowContext)
- const [nodeDataState, setNodeDataState] = useState(data)
-
- const {
- isOpen: isResponseModalOpen,
- onOpen: onResponseModalOpen,
- onClose: onResponseModalClose,
- } = useDisclosure()
useEffect(() => {
setNodeDataState(getNodes().find((node) => node.data.id === data.id)?.data ?? data)
@@ -49,7 +51,14 @@ const NodeModal = ({ data, isOpen, onClose, size = "3xl" }: NodeModalProps) => {
)
const setTextResponseValue = (e: React.ChangeEvent) => {
- setNodeDataState({ ...nodeDataState, response: { ...nodeDataState.response!, type: 'text', data: [{ text: e.target.value, priority: 1 }] } })
+ setNodeDataState({
+ ...nodeDataState,
+ response: {
+ ...nodeDataState.response!,
+ type: "text",
+ data: [{ text: e.target.value, priority: 1 }],
+ },
+ })
}
const onNodeSave = () => {
@@ -85,7 +94,6 @@ const NodeModal = ({ data, isOpen, onClose, size = "3xl" }: NodeModalProps) => {
}
}
-
return (
<>
{
>
)
diff --git a/frontend/src/modals/ResponseModal/ResponseModal.tsx b/frontend/src/modals/ResponseModal/ResponseModal.tsx
index d4dff5a1..3e55efc7 100644
--- a/frontend/src/modals/ResponseModal/ResponseModal.tsx
+++ b/frontend/src/modals/ResponseModal/ResponseModal.tsx
@@ -10,11 +10,11 @@ import {
Tabs,
} from "@nextui-org/react"
import { useContext, useMemo, useState } from "react"
-import toast from "react-hot-toast"
import { useParams } from "react-router-dom"
import { useReactFlow } from "reactflow"
import ModalComponent from "../../components/ModalComponent"
import { flowContext } from "../../contexts/flowContext"
+import { notificationsContext } from "../../contexts/notificationsContext"
import { NodeDataType } from "../../types/NodeTypes"
import { responseType, responseTypeType } from "../../types/ResponseTypes"
import PythonResponse from "./components/PythonResponse"
@@ -49,6 +49,7 @@ const ResponseModal = ({
setCurrentResponse({ ...currentResponse, type: key })
setSelected(key)
}
+ const { notification: n } = useContext(notificationsContext)
const tabItems: {
title: ResponseModalTab
@@ -94,10 +95,18 @@ const ResponseModal = ({
const saveResponse = () => {
if (!currentResponse.name) {
- return toast.error("Response name is required!")
+ return n.add({
+ title: "Saving error!",
+ message: "Response name is required!",
+ type: "error",
+ })
}
if (flows.some((flow) => flow.data.nodes.some((node) => (node.data.response.name === currentResponse.name && node.id !== data.id)))) {
- return toast.error("Response name must be unique!")
+ return n.add({
+ title: "Saving error!",
+ message: "Response name must be unique!",
+ type: "error",
+ })
} else {
const nodes = getNodes()
const node = getNode(data.id)
@@ -112,7 +121,10 @@ const ResponseModal = ({
}
const new_nodes = nodes.map((node) => (node.id === data.id ? new_node : node))
setNodes(() => new_nodes)
- setData(new_node.data)
+ setData({
+ ...data,
+ response: new_node.data.response
+ })
// currentFlow.data.nodes = nodes.map((node) => (node.id === data.id ? new_node : node))
// updateFlow(currentFlow)
quietSaveFlows()
diff --git a/frontend/src/pages/Flow.tsx b/frontend/src/pages/Flow.tsx
index 4f9fa367..ebfb7367 100644
--- a/frontend/src/pages/Flow.tsx
+++ b/frontend/src/pages/Flow.tsx
@@ -1,9 +1,8 @@
-import React, { useCallback, useContext, useEffect, useRef, useState } from "react"
+import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"
import ReactFlow, {
Background,
BackgroundVariant,
Connection,
- Controls,
Edge,
HandleType,
Node,
@@ -17,25 +16,27 @@ import ReactFlow, {
} from "reactflow"
import { a, useTransition } from "@react-spring/web"
-import toast from "react-hot-toast"
import { useParams } from "react-router-dom"
import "reactflow/dist/style.css"
import { v4 } from "uuid"
import Chat from "../components/chat/Chat"
+import CustomEdge from "../components/edges/ButtonEdge/ButtonEdge"
import FootBar from "../components/footbar/FootBar"
import DefaultNode from "../components/nodes/DefaultNode"
import FallbackNode from "../components/nodes/FallbackNode"
import LinkNode from "../components/nodes/LinkNode"
import StartNode from "../components/nodes/StartNode"
import SideBar from "../components/sidebar/SideBar"
-import { NODES } from "../consts"
+import { NODES, NODE_NAMES } from "../consts"
import { flowContext } from "../contexts/flowContext"
+import { notificationsContext } from "../contexts/notificationsContext"
import { undoRedoContext } from "../contexts/undoRedoContext"
import { workspaceContext } from "../contexts/workspaceContext"
import "../index.css"
import { FlowType } from "../types/FlowTypes"
import { NodeDataType, NodeType, NodesTypes } from "../types/NodeTypes"
import { responseType } from "../types/ResponseTypes"
+import Fallback from "./Fallback"
import Logs from "./Logs"
import NodesLayout from "./NodesLayout"
import Settings from "./Settings"
@@ -47,6 +48,12 @@ const nodeTypes = {
link_node: LinkNode,
}
+const edgeTypes = {
+ default: CustomEdge,
+}
+
+const untrackedFields = ["position", "positionAbsolute", "targetPosition", "sourcePosition"]
+
// export const addNodeToGraph = (node: NodeType, graph: FlowType[]) => {}
export default function Flow() {
@@ -70,10 +77,12 @@ export default function Flow() {
const [nodes, setNodes, onNodesChange] = useNodesState(flow?.data.nodes || [])
const [edges, setEdges, onEdgesChange] = useEdgesState(flow?.data.edges || [])
+
const [reactFlowInstance, setReactFlowInstance] = useState