diff --git a/frontend/src/index.css b/frontend/src/index.css
index 4a20843d503..82454fc5a01 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -41,6 +41,8 @@
--theme-button-primary: #46c8ff;
--theme-button-primary-hover: #434343;
+ --theme-button-cta: #7cd4fd;
+
--theme-file-row-even: #0e0f0f;
--theme-file-row-odd: #1b1b1e;
--theme-file-row-selected-even: rgba(14, 165, 233, 0.2);
@@ -92,6 +94,8 @@
--theme-button-primary: #0ba5ec;
--theme-button-primary-hover: #dedede;
+ --theme-button-cta: #7cd4fd;
+
--theme-file-row-even: #f5f5f5;
--theme-file-row-odd: #e9e9e9;
--theme-file-row-selected-even: #0ba5ec;
@@ -664,6 +668,11 @@ dialog::backdrop {
padding: 14px 15px;
}
+.markdown > * a {
+ color: var(--theme-button-cta);
+ text-decoration: underline;
+}
+
@media (max-width: 600px) {
.markdown table th,
.markdown table td {
diff --git a/frontend/src/media/logo/anything-llm-infinity.png b/frontend/src/media/logo/anything-llm-infinity.png
new file mode 100644
index 00000000000..9970b926933
Binary files /dev/null and b/frontend/src/media/logo/anything-llm-infinity.png differ
diff --git a/frontend/src/models/agentFlows.js b/frontend/src/models/agentFlows.js
new file mode 100644
index 00000000000..fcb82c597ac
--- /dev/null
+++ b/frontend/src/models/agentFlows.js
@@ -0,0 +1,149 @@
+import { API_BASE } from "@/utils/constants";
+import { baseHeaders } from "@/utils/request";
+
+const AgentFlows = {
+ /**
+ * Save a flow configuration
+ * @param {string} name - Display name of the flow
+ * @param {object} config - The configuration object for the flow
+ * @param {string} [uuid] - Optional UUID for updating existing flow
+ * @returns {Promise<{success: boolean, error: string | null, flow: {name: string, config: object, uuid: string} | null}>}
+ */
+ saveFlow: async (name, config, uuid = null) => {
+ return await fetch(`${API_BASE}/agent-flows/save`, {
+ method: "POST",
+ headers: {
+ ...baseHeaders(),
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ name, config, uuid }),
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error(response.error || "Failed to save flow");
+ return res;
+ })
+ .then((res) => res.json())
+ .catch((e) => ({
+ success: false,
+ error: e.message,
+ flow: null,
+ }));
+ },
+
+ /**
+ * List all available flows in the system
+ * @returns {Promise<{success: boolean, error: string | null, flows: Array<{name: string, uuid: string, description: string, steps: Array}>}>}
+ */
+ listFlows: async () => {
+ return await fetch(`${API_BASE}/agent-flows/list`, {
+ method: "GET",
+ headers: baseHeaders(),
+ })
+ .then((res) => res.json())
+ .catch((e) => ({
+ success: false,
+ error: e.message,
+ flows: [],
+ }));
+ },
+
+ /**
+ * Get a specific flow by UUID
+ * @param {string} uuid - The UUID of the flow to retrieve
+ * @returns {Promise<{success: boolean, error: string | null, flow: {name: string, config: object, uuid: string} | null}>}
+ */
+ getFlow: async (uuid) => {
+ return await fetch(`${API_BASE}/agent-flows/${uuid}`, {
+ method: "GET",
+ headers: baseHeaders(),
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error(response.error || "Failed to get flow");
+ return res;
+ })
+ .then((res) => res.json())
+ .catch((e) => ({
+ success: false,
+ error: e.message,
+ flow: null,
+ }));
+ },
+
+ /**
+ * Execute a specific flow
+ * @param {string} uuid - The UUID of the flow to run
+ * @param {object} variables - Optional variables to pass to the flow
+ * @returns {Promise<{success: boolean, error: string | null, results: object | null}>}
+ */
+ // runFlow: async (uuid, variables = {}) => {
+ // return await fetch(`${API_BASE}/agent-flows/${uuid}/run`, {
+ // method: "POST",
+ // headers: {
+ // ...baseHeaders(),
+ // "Content-Type": "application/json",
+ // },
+ // body: JSON.stringify({ variables }),
+ // })
+ // .then((res) => {
+ // if (!res.ok) throw new Error(response.error || "Failed to run flow");
+ // return res;
+ // })
+ // .then((res) => res.json())
+ // .catch((e) => ({
+ // success: false,
+ // error: e.message,
+ // results: null,
+ // }));
+ // },
+
+ /**
+ * Delete a specific flow
+ * @param {string} uuid - The UUID of the flow to delete
+ * @returns {Promise<{success: boolean, error: string | null}>}
+ */
+ deleteFlow: async (uuid) => {
+ return await fetch(`${API_BASE}/agent-flows/${uuid}`, {
+ method: "DELETE",
+ headers: baseHeaders(),
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error(response.error || "Failed to delete flow");
+ return res;
+ })
+ .then((res) => res.json())
+ .catch((e) => ({
+ success: false,
+ error: e.message,
+ }));
+ },
+
+ /**
+ * Toggle a flow's active status
+ * @param {string} uuid - The UUID of the flow to toggle
+ * @param {boolean} active - The new active status
+ * @returns {Promise<{success: boolean, error: string | null}>}
+ */
+ toggleFlow: async (uuid, active) => {
+ try {
+ const result = await fetch(`${API_BASE}/agent-flows/${uuid}/toggle`, {
+ method: "POST",
+ headers: {
+ ...baseHeaders(),
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ active }),
+ })
+ .then((res) => {
+ if (!res.ok) throw new Error(res.error || "Failed to toggle flow");
+ return res;
+ })
+ .then((res) => res.json());
+ return { success: true, flow: result.flow };
+ } catch (error) {
+ console.error("Failed to toggle flow:", error);
+ return { success: false, error: error.message };
+ }
+ },
+};
+
+export default AgentFlows;
diff --git a/frontend/src/pages/Admin/AgentBuilder/AddBlockMenu/index.jsx b/frontend/src/pages/Admin/AgentBuilder/AddBlockMenu/index.jsx
new file mode 100644
index 00000000000..777f521c043
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/AddBlockMenu/index.jsx
@@ -0,0 +1,68 @@
+import React, { useRef, useEffect } from "react";
+import { Plus, CaretDown } from "@phosphor-icons/react";
+import { BLOCK_TYPES, BLOCK_INFO } from "../BlockList";
+
+export default function AddBlockMenu({
+ showBlockMenu,
+ setShowBlockMenu,
+ addBlock,
+}) {
+ const menuRef = useRef(null);
+
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setShowBlockMenu(false);
+ }
+ }
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, [setShowBlockMenu]);
+
+ return (
+
+
setShowBlockMenu(!showBlockMenu)}
+ className="transition-all duration-300 w-full p-2.5 bg-theme-action-menu-bg hover:bg-theme-action-menu-item-hover border border-white/10 rounded-lg text-white flex items-center justify-center gap-2 text-sm font-medium"
+ >
+
+ Add Block
+
+
+ {showBlockMenu && (
+
+ {Object.entries(BLOCK_INFO).map(
+ ([type, info]) =>
+ type !== BLOCK_TYPES.START &&
+ type !== BLOCK_TYPES.FINISH &&
+ type !== BLOCK_TYPES.FLOW_INFO && (
+
{
+ addBlock(type);
+ setShowBlockMenu(false);
+ }}
+ className="w-full p-2.5 flex items-center gap-3 hover:bg-theme-action-menu-item-hover text-white transition-colors duration-300 group"
+ >
+
+
+
{info.label}
+
+ {info.description}
+
+
+
+ )
+ )}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/BlockList/index.jsx b/frontend/src/pages/Admin/AgentBuilder/BlockList/index.jsx
new file mode 100644
index 00000000000..c937f109dd8
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/BlockList/index.jsx
@@ -0,0 +1,305 @@
+import React from "react";
+import {
+ X,
+ CaretUp,
+ CaretDown,
+ Globe,
+ Browser,
+ Brain,
+ Flag,
+ Info,
+ BracketsCurly,
+} from "@phosphor-icons/react";
+import { Tooltip } from "react-tooltip";
+import StartNode from "../nodes/StartNode";
+import ApiCallNode from "../nodes/ApiCallNode";
+import WebsiteNode from "../nodes/WebsiteNode";
+import FileNode from "../nodes/FileNode";
+import CodeNode from "../nodes/CodeNode";
+import LLMInstructionNode from "../nodes/LLMInstructionNode";
+import FinishNode from "../nodes/FinishNode";
+import WebScrapingNode from "../nodes/WebScrapingNode";
+import FlowInfoNode from "../nodes/FlowInfoNode";
+
+const BLOCK_TYPES = {
+ FLOW_INFO: "flowInfo",
+ START: "start",
+ API_CALL: "apiCall",
+ // WEBSITE: "website", // Temporarily disabled
+ // FILE: "file", // Temporarily disabled
+ // CODE: "code", // Temporarily disabled
+ LLM_INSTRUCTION: "llmInstruction",
+ WEB_SCRAPING: "webScraping",
+ FINISH: "finish",
+};
+
+const BLOCK_INFO = {
+ [BLOCK_TYPES.FLOW_INFO]: {
+ label: "Flow Infomation",
+ icon:
,
+ description: "Basic flow information",
+ defaultConfig: {
+ name: "",
+ description: "",
+ },
+ getSummary: (config) => config.name || "Untitled Flow",
+ },
+ [BLOCK_TYPES.START]: {
+ label: "Flow Variables",
+ icon:
,
+ description: "Configure agent variables and settings",
+ getSummary: (config) => {
+ const varCount = config.variables?.filter((v) => v.name)?.length || 0;
+ return `${varCount} variable${varCount !== 1 ? "s" : ""} defined`;
+ },
+ },
+ [BLOCK_TYPES.API_CALL]: {
+ label: "API Call",
+ icon:
,
+ description: "Make an HTTP request",
+ defaultConfig: {
+ url: "",
+ method: "GET",
+ headers: [],
+ bodyType: "json",
+ body: "",
+ formData: [],
+ responseVariable: "",
+ },
+ getSummary: (config) =>
+ `${config.method || "GET"} ${config.url || "(no URL)"}`,
+ },
+ // TODO: Implement website, file, and code blocks
+ /* [BLOCK_TYPES.WEBSITE]: {
+ label: "Open Website",
+ icon:
,
+ description: "Navigate to a URL",
+ defaultConfig: {
+ url: "",
+ selector: "",
+ action: "read",
+ value: "",
+ resultVariable: "",
+ },
+ getSummary: (config) =>
+ `${config.action || "read"} from ${config.url || "(no URL)"}`,
+ },
+ [BLOCK_TYPES.FILE]: {
+ label: "Open File",
+ icon:
,
+ description: "Read or write to a file",
+ defaultConfig: {
+ path: "",
+ operation: "read",
+ content: "",
+ resultVariable: "",
+ },
+ getSummary: (config) =>
+ `${config.operation || "read"} ${config.path || "(no path)"}`,
+ },
+ [BLOCK_TYPES.CODE]: {
+ label: "Code Execution",
+ icon:
,
+ description: "Execute code snippets",
+ defaultConfig: {
+ language: "javascript",
+ code: "",
+ resultVariable: "",
+ },
+ getSummary: (config) => `Run ${config.language || "javascript"} code`,
+ },
+ */
+ [BLOCK_TYPES.LLM_INSTRUCTION]: {
+ label: "LLM Instruction",
+ icon:
,
+ description: "Process data using LLM instructions",
+ defaultConfig: {
+ instruction: "",
+ inputVariable: "",
+ resultVariable: "",
+ },
+ getSummary: (config) => config.instruction || "No instruction",
+ },
+ [BLOCK_TYPES.WEB_SCRAPING]: {
+ label: "Web Scraping",
+ icon:
,
+ description: "Scrape content from a webpage",
+ defaultConfig: {
+ url: "",
+ resultVariable: "",
+ },
+ getSummary: (config) => config.url || "No URL specified",
+ },
+ [BLOCK_TYPES.FINISH]: {
+ label: "Flow Complete",
+ icon:
,
+ description: "End of agent flow",
+ getSummary: () => "Flow will end here",
+ defaultConfig: {},
+ renderConfig: () => null,
+ },
+};
+
+export default function BlockList({
+ blocks,
+ updateBlockConfig,
+ removeBlock,
+ toggleBlockExpansion,
+ renderVariableSelect,
+ onDeleteVariable,
+ moveBlock,
+ refs,
+}) {
+ const renderBlockConfig = (block) => {
+ const props = {
+ config: block.config,
+ onConfigChange: (config) => updateBlockConfig(block.id, config),
+ renderVariableSelect,
+ onDeleteVariable,
+ };
+
+ switch (block.type) {
+ case BLOCK_TYPES.FLOW_INFO:
+ return
;
+ case BLOCK_TYPES.START:
+ return
;
+ case BLOCK_TYPES.API_CALL:
+ return
;
+ case BLOCK_TYPES.WEBSITE:
+ return
;
+ case BLOCK_TYPES.FILE:
+ return
;
+ case BLOCK_TYPES.CODE:
+ return
;
+ case BLOCK_TYPES.LLM_INSTRUCTION:
+ return
;
+ case BLOCK_TYPES.WEB_SCRAPING:
+ return
;
+ case BLOCK_TYPES.FINISH:
+ return
;
+ default:
+ return
Configuration options coming soon...
;
+ }
+ };
+
+ return (
+
+ {blocks.map((block, index) => (
+
+
+
toggleBlockExpansion(block.id)}
+ className="w-full p-4 flex items-center justify-between hover:bg-theme-action-menu-item-hover transition-colors duration-300 group cursor-pointer"
+ >
+
+
+ {React.cloneElement(BLOCK_INFO[block.type].icon, {
+ className: "w-4 h-4 text-white",
+ })}
+
+
+
+ {BLOCK_INFO[block.type].label}
+
+ {!block.isExpanded && (
+
+ {BLOCK_INFO[block.type].getSummary(block.config)}
+
+ )}
+
+
+
+ {block.id !== "start" &&
+ block.type !== BLOCK_TYPES.FINISH &&
+ block.type !== BLOCK_TYPES.FLOW_INFO && (
+
+ {index > 1 && (
+ {
+ e.stopPropagation();
+ moveBlock(index, index - 1);
+ }}
+ className="p-1.5 rounded-lg bg-theme-bg-primary border border-white/5 text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300"
+ data-tooltip-id="block-action"
+ data-tooltip-content="Move block up"
+ >
+
+
+ )}
+ {index < blocks.length - 2 && (
+ {
+ e.stopPropagation();
+ moveBlock(index, index + 1);
+ }}
+ className="p-1.5 rounded-lg bg-theme-bg-primary border border-white/5 text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300"
+ data-tooltip-id="block-action"
+ data-tooltip-content="Move block down"
+ >
+
+
+ )}
+ {
+ e.stopPropagation();
+ removeBlock(block.id);
+ }}
+ className="p-1.5 rounded-lg bg-theme-bg-primary border border-white/5 text-red-400 hover:bg-red-500/10 hover:border-red-500/20 transition-colors duration-300"
+ data-tooltip-id="block-action"
+ data-tooltip-content="Delete block"
+ >
+
+
+
+ )}
+
+
+
+
+ {renderBlockConfig(block)}
+
+
+
+ {index < blocks.length - 1 && (
+
+ )}
+
+ ))}
+
+
+ );
+}
+
+export { BLOCK_TYPES, BLOCK_INFO };
diff --git a/frontend/src/pages/Admin/AgentBuilder/HeaderMenu/index.jsx b/frontend/src/pages/Admin/AgentBuilder/HeaderMenu/index.jsx
new file mode 100644
index 00000000000..536d4af70ee
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/HeaderMenu/index.jsx
@@ -0,0 +1,130 @@
+import { CaretDown, CaretUp, Plus } from "@phosphor-icons/react";
+import AnythingInfinityLogo from "@/media/logo/anything-llm-infinity.png";
+import { useState, useRef, useEffect } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import paths from "@/utils/paths";
+import { Link } from "react-router-dom";
+
+export default function HeaderMenu({
+ agentName,
+ availableFlows = [],
+ onNewFlow,
+ onSaveFlow,
+}) {
+ const { flowId = null } = useParams();
+ const [showDropdown, setShowDropdown] = useState(false);
+ const navigate = useNavigate();
+ const dropdownRef = useRef(null);
+ const hasOtherFlows =
+ availableFlows.filter((flow) => flow.uuid !== flowId).length > 0;
+
+ useEffect(() => {
+ function handleClickOutside(event) {
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
+ setShowDropdown(false);
+ }
+ }
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, []);
+
+ return (
+
+
+
+
navigate(paths.settings.agentSkills())}
+ className="border-y-none border-l-none flex items-center gap-x-2 px-4 py-2 border-r border-white/10 hover:bg-theme-action-menu-bg transition-colors duration-300"
+ >
+
+
+ Builder
+
+
+
+
{
+ if (!agentName && !hasOtherFlows) {
+ const agentNameInput = document.getElementById(
+ "agent-flow-name-input"
+ );
+ if (agentNameInput) agentNameInput.focus();
+ return;
+ }
+ setShowDropdown(!showDropdown);
+ }}
+ >
+
+ {agentName || "Untitled Flow"}
+
+ {hasOtherFlows && (
+
+
+
+
+ )}
+
+ {showDropdown && (
+
+ {availableFlows
+ .filter((flow) => flow.uuid !== flowId)
+ .map((flow) => (
+ {
+ navigate(paths.agents.editAgent(flow.uuid));
+ setShowDropdown(false);
+ }}
+ className="border-none w-full text-left px-2 py-1 text-sm text-theme-text-primary hover:bg-theme-action-menu-bg transition-colors duration-300"
+ >
+
+ {flow?.name || "Untitled Flow"}
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ New Flow
+
+
+ Save
+
+
+
+ view documentation →
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/index.jsx b/frontend/src/pages/Admin/AgentBuilder/index.jsx
new file mode 100644
index 00000000000..f658931dad5
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/index.jsx
@@ -0,0 +1,361 @@
+import React, { useState, useEffect, useRef } from "react";
+import { useNavigate, useParams } from "react-router-dom";
+
+import BlockList, { BLOCK_TYPES, BLOCK_INFO } from "./BlockList";
+import AddBlockMenu from "./AddBlockMenu";
+import showToast from "@/utils/toast";
+import AgentFlows from "@/models/agentFlows";
+import { useTheme } from "@/hooks/useTheme";
+import HeaderMenu from "./HeaderMenu";
+import paths from "@/utils/paths";
+
+const DEFAULT_BLOCKS = [
+ {
+ id: "flow_info",
+ type: BLOCK_TYPES.FLOW_INFO,
+ config: {
+ name: "",
+ description: "",
+ },
+ isExpanded: true,
+ },
+ {
+ id: "start",
+ type: BLOCK_TYPES.START,
+ config: {
+ variables: [{ name: "", value: "" }],
+ },
+ isExpanded: true,
+ },
+ {
+ id: "finish",
+ type: BLOCK_TYPES.FINISH,
+ config: {},
+ isExpanded: false,
+ },
+];
+
+export default function AgentBuilder() {
+ const { flowId } = useParams();
+ const { theme } = useTheme();
+ const navigate = useNavigate();
+ const [agentName, setAgentName] = useState("");
+ const [_, setAgentDescription] = useState("");
+ const [currentFlowUuid, setCurrentFlowUuid] = useState(null);
+ const [active, setActive] = useState(true);
+ const [blocks, setBlocks] = useState(DEFAULT_BLOCKS);
+ const [selectedBlock, setSelectedBlock] = useState("start");
+ const [showBlockMenu, setShowBlockMenu] = useState(false);
+ const [showLoadMenu, setShowLoadMenu] = useState(false);
+ const [availableFlows, setAvailableFlows] = useState([]);
+ const [selectedFlowForDetails, setSelectedFlowForDetails] = useState(null);
+ const nameRef = useRef(null);
+ const descriptionRef = useRef(null);
+
+ useEffect(() => {
+ loadAvailableFlows();
+ }, []);
+
+ useEffect(() => {
+ if (flowId) {
+ loadFlow(flowId);
+ }
+ }, [flowId]);
+
+ useEffect(() => {
+ const flowInfoBlock = blocks.find(
+ (block) => block.type === BLOCK_TYPES.FLOW_INFO
+ );
+ setAgentName(flowInfoBlock?.config?.name || "");
+ }, [blocks]);
+
+ const loadAvailableFlows = async () => {
+ try {
+ const { success, error, flows } = await AgentFlows.listFlows();
+ if (!success) throw new Error(error);
+ setAvailableFlows(flows);
+ } catch (error) {
+ console.error(error);
+ showToast("Failed to load available flows", "error", { clear: true });
+ }
+ };
+
+ const loadFlow = async (uuid) => {
+ try {
+ const { success, error, flow } = await AgentFlows.getFlow(uuid);
+ if (!success) throw new Error(error);
+
+ // Convert steps to blocks with IDs, ensuring finish block is at the end
+ const flowBlocks = [
+ {
+ id: "flow_info",
+ type: BLOCK_TYPES.FLOW_INFO,
+ config: {
+ name: flow.config.name,
+ description: flow.config.description,
+ },
+ isExpanded: true,
+ },
+ ...flow.config.steps.map((step, index) => ({
+ id: index === 0 ? "start" : `block_${index}`,
+ type: step.type,
+ config: step.config,
+ isExpanded: true,
+ })),
+ ];
+
+ // Add finish block if not present
+ if (flowBlocks[flowBlocks.length - 1]?.type !== BLOCK_TYPES.FINISH) {
+ flowBlocks.push({
+ id: "finish",
+ type: BLOCK_TYPES.FINISH,
+ config: {},
+ isExpanded: false,
+ });
+ }
+
+ setAgentName(flow.config.name);
+ setAgentDescription(flow.config.description);
+ setActive(flow.config.active ?? true);
+ setCurrentFlowUuid(flow.uuid);
+ setBlocks(flowBlocks);
+ setShowLoadMenu(false);
+ } catch (error) {
+ console.error(error);
+ showToast("Failed to load flow", "error", { clear: true });
+ }
+ };
+
+ const addBlock = (type) => {
+ const newBlock = {
+ id: `block_${blocks.length}`,
+ type,
+ config: { ...BLOCK_INFO[type].defaultConfig },
+ isExpanded: true,
+ };
+ // Insert the new block before the finish block
+ const newBlocks = [...blocks];
+ newBlocks.splice(newBlocks.length - 1, 0, newBlock);
+ setBlocks(newBlocks);
+ setShowBlockMenu(false);
+ };
+
+ const updateBlockConfig = (blockId, config) => {
+ setBlocks(
+ blocks.map((block) =>
+ block.id === blockId
+ ? { ...block, config: { ...block.config, ...config } }
+ : block
+ )
+ );
+ };
+
+ const removeBlock = (blockId) => {
+ if (blockId === "start") return;
+ setBlocks(blocks.filter((block) => block.id !== blockId));
+ if (selectedBlock === blockId) {
+ setSelectedBlock("start");
+ }
+ };
+
+ const saveFlow = async () => {
+ const flowInfoBlock = blocks.find(
+ (block) => block.type === BLOCK_TYPES.FLOW_INFO
+ );
+ const name = flowInfoBlock?.config?.name;
+ const description = flowInfoBlock?.config?.description;
+
+ if (!name?.trim() || !description?.trim()) {
+ // Make sure the flow info block is expanded first
+ if (!flowInfoBlock.isExpanded) {
+ setBlocks(
+ blocks.map((block) =>
+ block.type === BLOCK_TYPES.FLOW_INFO
+ ? { ...block, isExpanded: true }
+ : block
+ )
+ );
+ // Small delay to allow expansion animation to complete
+ await new Promise((resolve) => setTimeout(resolve, 100));
+ }
+
+ if (!name?.trim()) {
+ nameRef.current?.focus();
+ } else if (!description?.trim()) {
+ descriptionRef.current?.focus();
+ }
+ showToast(
+ "Please provide both a name and description for your flow",
+ "error",
+ {
+ clear: true,
+ }
+ );
+ return;
+ }
+
+ const flowConfig = {
+ name,
+ description,
+ active,
+ steps: blocks
+ .filter(
+ (block) =>
+ block.type !== BLOCK_TYPES.FINISH &&
+ block.type !== BLOCK_TYPES.FLOW_INFO
+ )
+ .map((block) => ({
+ type: block.type,
+ config: block.config,
+ })),
+ };
+
+ try {
+ const { success, error, flow } = await AgentFlows.saveFlow(
+ name,
+ flowConfig,
+ currentFlowUuid
+ );
+ if (!success) throw new Error(error);
+
+ setCurrentFlowUuid(flow.uuid);
+ showToast("Agent flow saved successfully!", "success", { clear: true });
+ await loadAvailableFlows();
+ } catch (error) {
+ console.error("Save error details:", error);
+ showToast("Failed to save agent flow", "error", { clear: true });
+ }
+ };
+
+ const toggleBlockExpansion = (blockId) => {
+ setBlocks(
+ blocks.map((block) =>
+ block.id === blockId
+ ? { ...block, isExpanded: !block.isExpanded }
+ : block
+ )
+ );
+ };
+
+ // Get all available variables from the start block
+ const getAvailableVariables = () => {
+ const startBlock = blocks.find((b) => b.type === BLOCK_TYPES.START);
+ return startBlock?.config?.variables?.filter((v) => v.name) || [];
+ };
+
+ const renderVariableSelect = (
+ value,
+ onChange,
+ placeholder = "Select variable"
+ ) => (
+
onChange(e.target.value)}
+ className="w-full border-none bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5"
+ >
+
+ {placeholder}
+
+ {getAvailableVariables().map((v) => (
+
+ {v.name}
+
+ ))}
+
+ );
+
+ const deleteVariable = (variableName) => {
+ // Clean up references in other blocks
+ blocks.forEach((block) => {
+ if (block.type === BLOCK_TYPES.START) return;
+
+ let configUpdated = false;
+ const newConfig = { ...block.config };
+
+ // Check and clean responseVariable/resultVariable
+ if (newConfig.responseVariable === variableName) {
+ newConfig.responseVariable = "";
+ configUpdated = true;
+ }
+ if (newConfig.resultVariable === variableName) {
+ newConfig.resultVariable = "";
+ configUpdated = true;
+ }
+
+ if (configUpdated) {
+ updateBlockConfig(block.id, newConfig);
+ }
+ });
+ };
+
+ // const runFlow = async (uuid) => {
+ // try {
+ // const { success, error, _results } = await AgentFlows.runFlow(uuid);
+ // if (!success) throw new Error(error);
+
+ // showToast("Flow executed successfully!", "success", { clear: true });
+ // } catch (error) {
+ // console.error(error);
+ // showToast("Failed to run agent flow", "error", { clear: true });
+ // }
+ // };
+
+ const clearFlow = () => {
+ if (!!flowId) navigate(paths.agents.builder());
+ setAgentName("");
+ setAgentDescription("");
+ setCurrentFlowUuid(null);
+ setActive(true);
+ setBlocks(DEFAULT_BLOCKS);
+ };
+
+ const moveBlock = (fromIndex, toIndex) => {
+ const newBlocks = [...blocks];
+ const [movedBlock] = newBlocks.splice(fromIndex, 1);
+ newBlocks.splice(toIndex, 0, movedBlock);
+ setBlocks(newBlocks);
+ };
+
+ return (
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/ApiCallNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/ApiCallNode/index.jsx
new file mode 100644
index 00000000000..a1622909d28
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/ApiCallNode/index.jsx
@@ -0,0 +1,293 @@
+import React, { useRef, useState } from "react";
+import { Plus, X, CaretDown } from "@phosphor-icons/react";
+
+export default function ApiCallNode({
+ config,
+ onConfigChange,
+ renderVariableSelect,
+}) {
+ const urlInputRef = useRef(null);
+ const [showVarMenu, setShowVarMenu] = useState(false);
+ const varButtonRef = useRef(null);
+
+ const handleHeaderChange = (index, field, value) => {
+ const newHeaders = [...(config.headers || [])];
+ newHeaders[index] = { ...newHeaders[index], [field]: value };
+ onConfigChange({ headers: newHeaders });
+ };
+
+ const addHeader = () => {
+ const newHeaders = [...(config.headers || []), { key: "", value: "" }];
+ onConfigChange({ headers: newHeaders });
+ };
+
+ const removeHeader = (index) => {
+ const newHeaders = [...(config.headers || [])].filter(
+ (_, i) => i !== index
+ );
+ onConfigChange({ headers: newHeaders });
+ };
+
+ const insertVariableAtCursor = (variableName) => {
+ if (!urlInputRef.current) return;
+
+ const input = urlInputRef.current;
+ const start = input.selectionStart;
+ const end = input.selectionEnd;
+ const currentValue = config.url;
+
+ const newValue =
+ currentValue.substring(0, start) +
+ "${" +
+ variableName +
+ "}" +
+ currentValue.substring(end);
+
+ onConfigChange({ url: newValue });
+ setShowVarMenu(false);
+
+ // Set cursor position after the inserted variable
+ setTimeout(() => {
+ const newPosition = start + variableName.length + 3; // +3 for ${}
+ input.setSelectionRange(newPosition, newPosition);
+ input.focus();
+ }, 0);
+ };
+
+ return (
+
+
+
+ URL
+
+
+
onConfigChange({ url: e.target.value })}
+ className="flex-1 border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5"
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+
setShowVarMenu(!showVarMenu)}
+ className="h-full px-3 rounded-lg border-none bg-theme-settings-input-bg text-theme-text-primary hover:bg-theme-action-menu-item-hover transition-colors duration-300 flex items-center gap-1"
+ title="Insert variable"
+ >
+
+
+
+ {showVarMenu && (
+
+ {renderVariableSelect(
+ "",
+ insertVariableAtCursor,
+ "Select variable to insert",
+ true
+ )}
+
+ )}
+
+
+
+
+
+
+ Method
+
+ onConfigChange({ method: e.target.value })}
+ className="w-full border-none bg-theme-settings-input-bg text-theme-text-primary text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5"
+ >
+ {["GET", "POST", "DELETE"].map((method) => (
+
+ {method}
+
+ ))}
+
+
+
+
+
+ {["POST", "PUT", "PATCH"].includes(config.method) && (
+
+
+ Request Body
+
+
+
onConfigChange({ bodyType: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-theme-text-primary focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none light:bg-theme-settings-input-bg light:border-black/10"
+ >
+
+ JSON
+
+
+ Raw Text
+
+
+ Form Data
+
+
+ {config.bodyType === "json" ? (
+
+
+ )}
+
+
+
+ Store Response In
+
+ {renderVariableSelect(
+ config.responseVariable,
+ (value) => onConfigChange({ responseVariable: value }),
+ "Select or create variable"
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/CodeNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/CodeNode/index.jsx
new file mode 100644
index 00000000000..5e5553239d9
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/CodeNode/index.jsx
@@ -0,0 +1,56 @@
+import React from "react";
+
+export default function CodeNode({
+ config,
+ onConfigChange,
+ renderVariableSelect,
+}) {
+ return (
+
+
+
+ Language
+
+ onConfigChange({ language: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none"
+ >
+
+ JavaScript
+
+
+ Python
+
+
+ Shell
+
+
+
+
+
+ Code
+
+ onConfigChange({ code: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none font-mono"
+ rows={5}
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+
+
+ Store Result In
+
+ {renderVariableSelect(
+ config.resultVariable,
+ (value) => onConfigChange({ resultVariable: value }),
+ "Select or create variable"
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/FileNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/FileNode/index.jsx
new file mode 100644
index 00000000000..ad9e7973b1b
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/FileNode/index.jsx
@@ -0,0 +1,72 @@
+import React from "react";
+
+export default function FileNode({
+ config,
+ onConfigChange,
+ renderVariableSelect,
+}) {
+ return (
+
+
+
+ Operation
+
+ onConfigChange({ operation: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none"
+ >
+
+ Read File
+
+
+ Write File
+
+
+ Append to File
+
+
+
+
+
+ File Path
+
+ onConfigChange({ path: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none"
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+ {config.operation !== "read" && (
+
+
+ Content
+
+ onConfigChange({ content: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none"
+ rows={3}
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+ )}
+
+
+ Store Result In
+
+ {renderVariableSelect(
+ config.resultVariable,
+ (value) => onConfigChange({ resultVariable: value }),
+ "Select or create variable"
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/FinishNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/FinishNode/index.jsx
new file mode 100644
index 00000000000..a76e2e8c2d4
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/FinishNode/index.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+
+export default function FinishNode() {
+ return (
+
+ This is the end of your agent flow. All steps above will be executed in
+ sequence.
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/FlowInfoNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/FlowInfoNode/index.jsx
new file mode 100644
index 00000000000..1b3128d6ea1
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/FlowInfoNode/index.jsx
@@ -0,0 +1,65 @@
+import React, { forwardRef } from "react";
+
+const FlowInfoNode = forwardRef(({ config, onConfigChange }, refs) => {
+ return (
+
+
+
+ Flow Name
+
+
+
+ It is important to give your flow a name that an LLM can easily
+ understand.
+
+
"SendMessageToDiscord", "CheckStockPrice", "CheckWeather"
+
+
+ onConfigChange({
+ ...config,
+ name: e.target.value,
+ })
+ }
+ className="w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5"
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+
+
+
+ Description
+
+
+
+ It is equally important to give your flow a description that an LLM
+ can easily understand. Be sure to include the purpose of the flow,
+ the context it will be used in, and any other relevant information.
+
+
+
+ onConfigChange({
+ ...config,
+ description: e.target.value,
+ })
+ }
+ className="w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5"
+ rows={3}
+ placeholder="Enter flow description"
+ />
+
+
+ );
+});
+
+FlowInfoNode.displayName = "FlowInfoNode";
+export default FlowInfoNode;
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/LLMInstructionNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/LLMInstructionNode/index.jsx
new file mode 100644
index 00000000000..24b7821b5de
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/LLMInstructionNode/index.jsx
@@ -0,0 +1,52 @@
+import React from "react";
+
+export default function LLMInstructionNode({
+ config,
+ onConfigChange,
+ renderVariableSelect,
+}) {
+ return (
+
+
+
+ Input Variable
+
+ {renderVariableSelect(
+ config.inputVariable,
+ (value) => onConfigChange({ ...config, inputVariable: value }),
+ "Select input variable"
+ )}
+
+
+
+
+ Instruction
+
+
+ onConfigChange({
+ ...config,
+ instruction: e.target.value,
+ })
+ }
+ className="w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5"
+ rows={3}
+ placeholder="Enter instructions for the LLM..."
+ />
+
+
+
+
+ Result Variable
+
+ {renderVariableSelect(
+ config.resultVariable,
+ (value) => onConfigChange({ ...config, resultVariable: value }),
+ "Select or create variable",
+ true
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/StartNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/StartNode/index.jsx
new file mode 100644
index 00000000000..0e73189d7c4
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/StartNode/index.jsx
@@ -0,0 +1,72 @@
+import React from "react";
+import { Plus, X } from "@phosphor-icons/react";
+
+export default function StartNode({
+ config,
+ onConfigChange,
+ onDeleteVariable,
+}) {
+ const handleDeleteVariable = (index, variableName) => {
+ // First clean up references, then delete the variable
+ onDeleteVariable(variableName);
+ const newVars = config.variables.filter((_, i) => i !== index);
+ onConfigChange({ variables: newVars });
+ };
+
+ return (
+
+
Variables
+ {config.variables.map((variable, index) => (
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/WebScrapingNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/WebScrapingNode/index.jsx
new file mode 100644
index 00000000000..fda51e34d3a
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/WebScrapingNode/index.jsx
@@ -0,0 +1,41 @@
+import React from "react";
+
+export default function WebScrapingNode({
+ config,
+ onConfigChange,
+ renderVariableSelect,
+}) {
+ return (
+
+
+
+ URL to Scrape
+
+
+ onConfigChange({
+ ...config,
+ url: e.target.value,
+ })
+ }
+ className="w-full border-none bg-theme-settings-input-bg text-theme-text-primary placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none p-2.5"
+ placeholder="https://example.com"
+ />
+
+
+
+
+ Result Variable
+
+ {renderVariableSelect(
+ config.resultVariable,
+ (value) => onConfigChange({ ...config, resultVariable: value }),
+ "Select or create variable",
+ true
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/AgentBuilder/nodes/WebsiteNode/index.jsx b/frontend/src/pages/Admin/AgentBuilder/nodes/WebsiteNode/index.jsx
new file mode 100644
index 00000000000..b3b4658ceba
--- /dev/null
+++ b/frontend/src/pages/Admin/AgentBuilder/nodes/WebsiteNode/index.jsx
@@ -0,0 +1,68 @@
+import React from "react";
+
+export default function WebsiteNode({
+ config,
+ onConfigChange,
+ renderVariableSelect,
+}) {
+ return (
+
+
+ URL
+ onConfigChange({ url: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none"
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+
+
+ Action
+
+ onConfigChange({ action: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none"
+ >
+
+ Read Content
+
+
+ Click Element
+
+
+ Type Text
+
+
+
+
+
+ CSS Selector
+
+ onConfigChange({ selector: e.target.value })}
+ className="w-full p-2.5 text-sm rounded-lg bg-theme-bg-primary border border-white/5 text-white placeholder:text-white/20 focus:border-primary-button focus:ring-1 focus:ring-primary-button outline-none"
+ autoComplete="off"
+ spellCheck={false}
+ />
+
+
+
+ Store Result In
+
+ {renderVariableSelect(
+ config.resultVariable,
+ (value) => onConfigChange({ resultVariable: value }),
+ "Select or create variable"
+ )}
+
+
+ );
+}
diff --git a/frontend/src/pages/Admin/Agents/AgentFlows/FlowPanel.jsx b/frontend/src/pages/Admin/Agents/AgentFlows/FlowPanel.jsx
new file mode 100644
index 00000000000..7cfdda5df5f
--- /dev/null
+++ b/frontend/src/pages/Admin/Agents/AgentFlows/FlowPanel.jsx
@@ -0,0 +1,124 @@
+import React, { useState, useEffect, useRef } from "react";
+import AgentFlows from "@/models/agentFlows";
+import showToast from "@/utils/toast";
+import { FlowArrow, Gear } from "@phosphor-icons/react";
+import { useNavigate } from "react-router-dom";
+import paths from "@/utils/paths";
+
+function ManageFlowMenu({ flow, onDelete }) {
+ const [open, setOpen] = useState(false);
+ const menuRef = useRef(null);
+ const navigate = useNavigate();
+
+ async function deleteFlow() {
+ if (
+ !window.confirm(
+ "Are you sure you want to delete this flow? This action cannot be undone."
+ )
+ )
+ return;
+ const { success, error } = await AgentFlows.deleteFlow(flow.uuid);
+ if (success) {
+ showToast("Flow deleted successfully.", "success");
+ onDelete(flow.uuid);
+ } else {
+ showToast(error || "Failed to delete flow.", "error");
+ }
+ }
+
+ useEffect(() => {
+ const handleClickOutside = (event) => {
+ if (menuRef.current && !menuRef.current.contains(event.target)) {
+ setOpen(false);
+ }
+ };
+
+ document.addEventListener("mousedown", handleClickOutside);
+ return () => {
+ document.removeEventListener("mousedown", handleClickOutside);
+ };
+ }, []);
+
+ return (
+
+
setOpen(!open)}
+ className="p-1.5 rounded-lg text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300"
+ >
+
+
+ {open && (
+
+ navigate(paths.agents.editAgent(flow.uuid))}
+ className="border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left"
+ >
+ Edit Flow
+
+
+ Delete Flow
+
+
+ )}
+
+ );
+}
+
+export default function FlowPanel({ flow, toggleFlow, onDelete }) {
+ const [isActive, setIsActive] = useState(flow.active);
+
+ useEffect(() => {
+ setIsActive(flow.active);
+ }, [flow.uuid, flow.active]);
+
+ const handleToggle = async () => {
+ try {
+ const { success, error } = await AgentFlows.toggleFlow(
+ flow.uuid,
+ !isActive
+ );
+ if (!success) throw new Error(error);
+ setIsActive(!isActive);
+ toggleFlow(flow.uuid);
+ showToast("Flow status updated successfully", "success", { clear: true });
+ } catch (error) {
+ console.error("Failed to toggle flow:", error);
+ showToast("Failed to toggle flow", "error", { clear: true });
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {flow.name}
+
+
+
+
+
+
+
+
+
+ {flow.description || "No description provided"}
+
+
+
+ >
+ );
+}
diff --git a/frontend/src/pages/Admin/Agents/AgentFlows/index.jsx b/frontend/src/pages/Admin/Agents/AgentFlows/index.jsx
new file mode 100644
index 00000000000..57258d577af
--- /dev/null
+++ b/frontend/src/pages/Admin/Agents/AgentFlows/index.jsx
@@ -0,0 +1,57 @@
+import React from "react";
+import { CaretRight } from "@phosphor-icons/react";
+
+export default function AgentFlowsList({
+ flows = [],
+ selectedFlow,
+ handleClick,
+}) {
+ if (flows.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {flows.map((flow, index) => (
+
handleClick?.(flow)}
+ >
+
{flow.name}
+
+
+ {flow.active ? "On" : "Off"}
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/frontend/src/pages/Admin/Agents/Imported/ImportedSkillConfig/index.jsx b/frontend/src/pages/Admin/Agents/Imported/ImportedSkillConfig/index.jsx
index a68ff5b16c1..9ee4ad985b0 100644
--- a/frontend/src/pages/Admin/Agents/Imported/ImportedSkillConfig/index.jsx
+++ b/frontend/src/pages/Admin/Agents/Imported/ImportedSkillConfig/index.jsx
@@ -231,9 +231,9 @@ function ManageSkillMenu({ config, setImportedSkills }) {
setOpen(!open)}
- className={`border-none transition duration-200 hover:rotate-90 outline-none ring-none ${open ? "rotate-90" : ""}`}
+ className="p-1.5 rounded-lg text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300"
>
-
+
{open && (
diff --git a/frontend/src/pages/Admin/Agents/Imported/SkillList/index.jsx b/frontend/src/pages/Admin/Agents/Imported/SkillList/index.jsx
index 0c1a8dce4fa..4544fdbeeeb 100644
--- a/frontend/src/pages/Admin/Agents/Imported/SkillList/index.jsx
+++ b/frontend/src/pages/Admin/Agents/Imported/SkillList/index.jsx
@@ -16,7 +16,7 @@ export default function ImportedSkillList({
AnythingLLM Agent Docs
diff --git a/frontend/src/pages/Admin/Agents/index.jsx b/frontend/src/pages/Admin/Agents/index.jsx
index 605c89348a8..dada8c38892 100644
--- a/frontend/src/pages/Admin/Agents/index.jsx
+++ b/frontend/src/pages/Admin/Agents/index.jsx
@@ -4,7 +4,15 @@ import { isMobile } from "react-device-detect";
import Admin from "@/models/admin";
import System from "@/models/system";
import showToast from "@/utils/toast";
-import { CaretLeft, CaretRight, Plug, Robot } from "@phosphor-icons/react";
+import {
+ CaretLeft,
+ CaretRight,
+ Plug,
+ Robot,
+ Hammer,
+ FlowArrow,
+ PlusCircle,
+} from "@phosphor-icons/react";
import ContextualSaveBar from "@/components/ContextualSaveBar";
import { castToType } from "@/utils/types";
import { FullScreenLoader } from "@/components/Preloader";
@@ -13,6 +21,11 @@ import { DefaultBadge } from "./Badges/default";
import ImportedSkillList from "./Imported/SkillList";
import ImportedSkillConfig from "./Imported/ImportedSkillConfig";
import { Tooltip } from "react-tooltip";
+import AgentFlowsList from "./AgentFlows";
+import FlowPanel from "./AgentFlows/FlowPanel";
+import { Link } from "react-router-dom";
+import paths from "@/utils/paths";
+import AgentFlows from "@/models/agentFlows";
export default function AdminAgents() {
const formEl = useRef(null);
@@ -26,6 +39,10 @@ export default function AdminAgents() {
const [importedSkills, setImportedSkills] = useState([]);
const [disabledAgentSkills, setDisabledAgentSkills] = useState([]);
+ const [agentFlows, setAgentFlows] = useState([]);
+ const [selectedFlow, setSelectedFlow] = useState(null);
+ const [activeFlowIds, setActiveFlowIds] = useState([]);
+
// Alert user if they try to leave the page with unsaved changes
useEffect(() => {
const handleBeforeUnload = (event) => {
@@ -47,13 +64,17 @@ export default function AdminAgents() {
"disabled_agent_skills",
"default_agent_skills",
"imported_agent_skills",
+ "active_agent_flows",
]);
+ const { flows = [] } = await AgentFlows.listFlows();
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
setDisabledAgentSkills(
_preferences.settings?.disabled_agent_skills ?? []
);
setImportedSkills(_preferences.settings?.imported_agent_skills ?? []);
+ setActiveFlowIds(_preferences.settings?.active_agent_flows ?? []);
+ setAgentFlows(flows);
setLoading(false);
}
fetchSettings();
@@ -79,6 +100,15 @@ export default function AdminAgents() {
});
};
+ const toggleFlow = (flowId) => {
+ setActiveFlowIds((prev) => {
+ const updatedFlows = prev.includes(flowId)
+ ? prev.filter((id) => id !== flowId)
+ : [...prev, flowId];
+ return updatedFlows;
+ });
+ };
+
const handleSubmit = async (e) => {
e.preventDefault();
const data = {
@@ -129,10 +159,30 @@ export default function AdminAgents() {
setHasChanges(false);
};
- const SelectedSkillComponent = selectedSkill.imported
- ? ImportedSkillConfig
- : configurableSkills[selectedSkill]?.component ||
- defaultSkills[selectedSkill]?.component;
+ const SelectedSkillComponent = selectedFlow
+ ? FlowPanel
+ : selectedSkill?.imported
+ ? ImportedSkillConfig
+ : configurableSkills[selectedSkill]?.component ||
+ defaultSkills[selectedSkill]?.component;
+
+ // Update the click handlers to clear the other selection
+ const handleSkillClick = (skill) => {
+ setSelectedFlow(null);
+ setSelectedSkill(skill);
+ if (isMobile) setShowSkillModal(true);
+ };
+
+ const handleFlowClick = (flow) => {
+ setSelectedSkill(null);
+ setSelectedFlow(flow);
+ };
+
+ const handleFlowDelete = (flowId) => {
+ setSelectedFlow(null);
+ setActiveFlowIds((prev) => prev.filter((id) => id !== flowId));
+ setAgentFlows((prev) => prev.filter((flow) => flow.uuid !== flowId));
+ };
if (loading) {
return (
@@ -154,7 +204,7 @@ export default function AdminAgents() {
>
setHasChanges(true)}
+ onChange={() => !selectedFlow && setHasChanges(true)}
ref={formEl}
className="flex flex-col w-full p-4 mt-10"
>
@@ -180,6 +230,7 @@ export default function AdminAgents() {
skills={defaultSkills}
selectedSkill={selectedSkill}
handleClick={(skill) => {
+ setSelectedFlow(null);
setSelectedSkill(skill);
setShowSkillModal(true);
}}
@@ -192,6 +243,7 @@ export default function AdminAgents() {
skills={configurableSkills}
selectedSkill={selectedSkill}
handleClick={(skill) => {
+ setSelectedFlow(null);
setSelectedSkill(skill);
setShowSkillModal(true);
}}
@@ -205,7 +257,23 @@ export default function AdminAgents() {
+
+
+
+
@@ -232,7 +300,14 @@ export default function AdminAgents() {
{SelectedSkillComponent ? (
<>
- {selectedSkill.imported ? (
+ {selectedFlow ? (
+
+ ) : selectedSkill.imported ? (
- Select an agent skill
+
+ Select an agent skill or flow
+
)}