diff --git a/frontend/app/[locale]/agents/AgentConfiguration.tsx b/frontend/app/[locale]/agents/AgentConfiguration.tsx deleted file mode 100644 index b23f2ccae..000000000 --- a/frontend/app/[locale]/agents/AgentConfiguration.tsx +++ /dev/null @@ -1,653 +0,0 @@ -"use client"; - -import { - useState, - useEffect, - useRef, - forwardRef, - useImperativeHandle, - useCallback, -} from "react"; -import { useTranslation } from "react-i18next"; -import { Drawer, App } from "antd"; -import { - AGENT_SETUP_LAYOUT_DEFAULT, - GENERATE_PROMPT_STREAM_TYPES, -} from "@/const/agentConfig"; -import { SETUP_PAGE_CONTAINER, STANDARD_CARD } from "@/const/layoutConstants"; -import { ModelOption } from "@/types/modelConfig"; -import { - LayoutConfig, - AgentConfigDataResponse, - AgentConfigCustomEvent, - AgentRefreshEvent, -} from "@/types/agentConfig"; -import { - fetchTools, - fetchAgentList, - exportAgent, - deleteAgent, -} from "@/services/agentConfigService"; -import { generatePromptStream } from "@/services/promptService"; -import { updateToolList } from "@/services/mcpService"; -import log from "@/lib/logger"; -import { configStore } from "@/lib/config"; - -import AgentSetupOrchestrator from "./components/AgentSetupOrchestrator"; -import DebugConfig from "./components/DebugConfig"; - -import "../i18n"; - -// Layout Height Constant Configuration -const LAYOUT_CONFIG: LayoutConfig = AGENT_SETUP_LAYOUT_DEFAULT; - -/** - * Agent configuration main component - * Provides a full-width interface for agent business logic configuration - * Follows SETUP_PAGE_CONTAINER layout standards for consistent height and spacing - */ -export type AgentConfigHandle = { - hasUnsavedChanges: () => boolean; - saveAllChanges: () => Promise; - reloadCurrentAgentData: () => Promise; -}; - -interface AgentConfigProps { - canAccessProtectedData: boolean; -} - -export default forwardRef(function AgentConfig( - { canAccessProtectedData }, - ref -) { - const { t } = useTranslation("common"); - const { message } = App.useApp(); - const [businessLogic, setBusinessLogic] = useState(""); - const [selectedTools, setSelectedTools] = useState([]); - const [isDebugDrawerOpen, setIsDebugDrawerOpen] = useState(false); - const [isCreatingNewAgent, setIsCreatingNewAgent] = useState(false); - const [mainAgentModel, setMainAgentModel] = useState(null); - const [mainAgentModelId, setMainAgentModelId] = useState(null); - const [mainAgentMaxStep, setMainAgentMaxStep] = useState(5); - const [businessLogicModel, setBusinessLogicModel] = useState( - null - ); - const [businessLogicModelId, setBusinessLogicModelId] = useState< - number | null - >(null); - const [tools, setTools] = useState([]); - const [mainAgentId, setMainAgentId] = useState(null); - const [subAgentList, setSubAgentList] = useState([]); - const [loadingAgents, setLoadingAgents] = useState(false); - - const [enabledAgentIds, setEnabledAgentIds] = useState([]); - - const [isEditingAgent, setIsEditingAgent] = useState(false); - const [editingAgent, setEditingAgent] = useState(null); - - // Add state for three segmented content sections - const [dutyContent, setDutyContent] = useState(""); - const [constraintContent, setConstraintContent] = useState(""); - const [fewShotsContent, setFewShotsContent] = useState(""); - - // Add state for agent name and description - const [agentName, setAgentName] = useState(""); - const [agentDescription, setAgentDescription] = useState(""); - const [agentDisplayName, setAgentDisplayName] = useState(""); - const [agentAuthor, setAgentAuthor] = useState(""); - - // Add state for business logic and action buttons - const [isGeneratingAgent, setIsGeneratingAgent] = useState(false); - const [isEmbeddingConfigured, setIsEmbeddingConfigured] = useState(false); - - // Error state for business logic input - const [businessLogicError, setBusinessLogicError] = useState(false); - - // Only auto scan once flag - const hasAutoScanned = useRef(false); - const unsavedRef = useRef(false); - const saveHandlerRef = useRef Promise)>(null); - const reloadHandlerRef = useRef Promise)>(null); - - useImperativeHandle(ref, () => ({ - hasUnsavedChanges: () => unsavedRef.current, - saveAllChanges: async () => { - if (saveHandlerRef.current) { - await saveHandlerRef.current(); - } - }, - reloadCurrentAgentData: async () => { - if (reloadHandlerRef.current) { - await reloadHandlerRef.current(); - } - }, - })); - - // Handle generate agent - const handleGenerateAgent = async (selectedModel?: ModelOption) => { - if (!businessLogic || businessLogic.trim() === "") { - setBusinessLogicError(true); - message.warning( - t("businessLogic.config.error.businessDescriptionRequired") - ); - // Scroll to business logic input after a short delay to ensure it's visible - setTimeout(() => { - const businessLogicInput = document.querySelector( - "[data-business-logic-input]" - ); - if (businessLogicInput) { - businessLogicInput.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - } - }, 100); - return; - } - // Clear error when validation passes - setBusinessLogicError(false); - - // In create mode, agent_id should be 0 (backend will handle this case) - // In edit mode, use the current agent id - const currentAgentId = getCurrentAgentId(); - const agentIdToUse = isCreatingNewAgent ? 0 : currentAgentId || 0; - - if (!isCreatingNewAgent && !currentAgentId) { - message.error(t("businessLogic.config.error.noAgentId")); - return; - } - - setIsGeneratingAgent(true); - try { - const currentAgentName = agentName; - const currentAgentDisplayName = agentDisplayName; - - // Extract tool IDs from selected tools (convert string IDs to numbers) - // Always pass tool_ids array (empty array means no tools selected, undefined means use database) - // In edit mode, we want to use current selection, so pass the array even if empty - const toolIds = selectedTools.map((tool) => Number(tool.id)); - - // Get sub-agent IDs from enabledAgentIds - // Always pass sub_agent_ids array (empty array means no sub-agents selected, undefined means use database) - // In edit mode, we want to use current selection, so pass the array even if empty - const subAgentIds = [...enabledAgentIds]; - - // Call backend API to generate agent prompt - // Pass tool_ids and sub_agent_ids to use frontend selection instead of database query - await generatePromptStream( - { - agent_id: agentIdToUse, - task_description: businessLogic, - model_id: selectedModel?.id?.toString() || "", - tool_ids: toolIds, - sub_agent_ids: subAgentIds, - }, - (data) => { - // Process streaming response data - switch (data.type) { - case GENERATE_PROMPT_STREAM_TYPES.DUTY: - setDutyContent(data.content); - break; - case GENERATE_PROMPT_STREAM_TYPES.CONSTRAINT: - setConstraintContent(data.content); - break; - case GENERATE_PROMPT_STREAM_TYPES.FEW_SHOTS: - setFewShotsContent(data.content); - break; - case GENERATE_PROMPT_STREAM_TYPES.AGENT_VAR_NAME: - // Only update if current agent name is empty - if (!currentAgentName || currentAgentName.trim() === "") { - setAgentName(data.content); - } - break; - case GENERATE_PROMPT_STREAM_TYPES.AGENT_DESCRIPTION: - setAgentDescription(data.content); - break; - case GENERATE_PROMPT_STREAM_TYPES.AGENT_DISPLAY_NAME: - // Only update if current agent display name is empty - if ( - !currentAgentDisplayName || - currentAgentDisplayName.trim() === "" - ) { - setAgentDisplayName(data.content); - } - break; - } - }, - (error) => { - log.error("Generate prompt stream error:", error); - message.error(t("businessLogic.config.message.generateError")); - }, - () => { - message.success(t("businessLogic.config.message.generateSuccess")); - } - ); - } catch (error) { - log.error("Generate agent error:", error); - message.error(t("businessLogic.config.message.generateError")); - } finally { - setIsGeneratingAgent(false); - } - }; - - // Handle export agent - const handleExportAgent = async () => { - if (!editingAgent) { - message.warning(t("agent.error.noAgentSelected")); - return; - } - - try { - const result = await exportAgent(Number(editingAgent.id)); - if (result.success) { - // Handle backend returned string or object - let exportData = result.data; - if (typeof exportData === "string") { - try { - exportData = JSON.parse(exportData); - } catch (e) { - // If parsing fails, it means it's already a string, export directly - } - } - const blob = new Blob([JSON.stringify(exportData, null, 2)], { - type: "application/json", - }); - - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `${editingAgent.name}_config.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - message.success(t("businessLogic.config.message.agentExportSuccess")); - } else { - message.error( - result.message || t("businessLogic.config.error.agentExportFailed") - ); - } - } catch (error) { - log.error(t("agentConfig.agents.exportFailed"), error); - message.error(t("businessLogic.config.error.agentExportFailed")); - } - }; - - // Handle delete agent - const handleDeleteAgent = async () => { - if (!editingAgent) { - message.warning(t("agent.error.noAgentSelected")); - return; - } - - try { - const result = await deleteAgent(Number(editingAgent.id)); - if (result.success) { - message.success( - t("businessLogic.config.message.agentDeleteSuccess", { - name: editingAgent.name, - }) - ); - // Reset editing state - setIsEditingAgent(false); - setEditingAgent(null); - setBusinessLogic(""); - setDutyContent(""); - setConstraintContent(""); - setFewShotsContent(""); - setAgentName(""); - setAgentDescription(""); - // Notify AgentManagementConfig to refresh agent list - window.dispatchEvent( - new CustomEvent("refreshAgentList") as AgentRefreshEvent - ); - } else { - message.error( - result.message || t("businessLogic.config.message.agentDeleteFailed") - ); - } - } catch (error) { - log.error(t("agentConfig.agents.deleteFailed"), error); - message.error(t("businessLogic.config.message.agentDeleteFailed")); - } - }; - - // Load tools when page is loaded - useEffect(() => { - if (!canAccessProtectedData) { - return; - } - // Check embedding configuration once when entering the page - try { - const modelConfig = configStore.getModelConfig(); - setIsEmbeddingConfigured(!!modelConfig?.embedding?.modelName); - } catch (e) { - setIsEmbeddingConfigured(false); - } - - const loadTools = async () => { - try { - const result = await fetchTools(); - if (result.success) { - setTools(result.data); - // If the tool list is empty and auto scan hasn't been triggered, trigger scan once - if (result.data.length === 0 && !hasAutoScanned.current) { - hasAutoScanned.current = true; - // Mark as auto scanned - const scanResult = await updateToolList(); - if (!scanResult.success) { - message.error(t("toolManagement.message.refreshFailed")); - return; - } - message.success(t("toolManagement.message.refreshSuccess")); - // After scan, fetch the tool list again - const reFetch = await fetchTools(); - if (reFetch.success) { - setTools(reFetch.data); - } - } - } else { - message.error(result.message); - } - } catch (error) { - log.error(t("agent.error.loadTools"), error); - message.error(t("agent.error.loadToolsRetry")); - } - }; - - loadTools(); - }, [canAccessProtectedData, t]); - - // Get agent list - const fetchAgents = async () => { - setLoadingAgents(true); - try { - const result = await fetchAgentList(); - if (result.success) { - // fetchAgentList now returns AgentBasicInfo[], so we just set the subAgentList - setSubAgentList(result.data); - // Clear other states since we don't have detailed info yet - setMainAgentId(null); - // No longer manually clear enabledAgentIds, completely rely on backend returned sub_agent_id_list - setMainAgentModel(null); - setMainAgentMaxStep(5); - setBusinessLogic(""); - setDutyContent(""); - setConstraintContent(""); - setFewShotsContent(""); - setBusinessLogicError(false); - // Clear agent name and description only when not in editing mode - if (!isEditingAgent) { - setAgentName(""); - setAgentDescription(""); - setAgentDisplayName(""); - } - } else { - message.error(result.message || t("agent.error.fetchAgentList")); - } - } catch (error) { - log.error(t("agent.error.fetchAgentList"), error); - message.error(t("agent.error.fetchAgentListRetry")); - } finally { - setLoadingAgents(false); - } - }; - - // Get agent list when component is loaded - useEffect(() => { - if (!canAccessProtectedData) { - return; - } - fetchAgents(); - }, [canAccessProtectedData]); - - // Use refs to store latest values to avoid recreating event listener - const businessLogicRef = useRef(businessLogic); - const dutyContentRef = useRef(dutyContent); - const constraintContentRef = useRef(constraintContent); - const fewShotsContentRef = useRef(fewShotsContent); - - // Update refs when values change - useEffect(() => { - businessLogicRef.current = businessLogic; - }, [businessLogic]); - useEffect(() => { - dutyContentRef.current = dutyContent; - }, [dutyContent]); - useEffect(() => { - constraintContentRef.current = constraintContent; - }, [constraintContent]); - useEffect(() => { - fewShotsContentRef.current = fewShotsContent; - }, [fewShotsContent]); - - // Memoize event handler to avoid recreating listener - const handleGetAgentConfigData = useCallback(() => { - // Check if there is system prompt content - let hasSystemPrompt = false; - - // If any of the segmented prompts has content, consider it as having system prompt - if (dutyContentRef.current && dutyContentRef.current.trim() !== "") { - hasSystemPrompt = true; - } else if ( - constraintContentRef.current && - constraintContentRef.current.trim() !== "" - ) { - hasSystemPrompt = true; - } else if ( - fewShotsContentRef.current && - fewShotsContentRef.current.trim() !== "" - ) { - hasSystemPrompt = true; - } - - // Send the current configuration data to the main page - const eventData: AgentConfigDataResponse = { - businessLogic: businessLogicRef.current, - systemPrompt: hasSystemPrompt ? "has_content" : "", - }; - - window.dispatchEvent( - new CustomEvent("agentConfigDataResponse", { - detail: eventData, - }) as AgentConfigCustomEvent - ); - }, []); // Empty deps - handler uses refs for latest values - - // Add event listener to respond to the data request from the main page - useEffect(() => { - window.addEventListener("getAgentConfigData", handleGetAgentConfigData); - - return () => { - window.removeEventListener( - "getAgentConfigData", - handleGetAgentConfigData - ); - }; - }, [handleGetAgentConfigData]); - - const handleEditingStateChange = (isEditing: boolean, agent: any) => { - setIsEditingAgent(isEditing); - setEditingAgent(agent); - - // When starting to edit agent, set agent name and description to the right-side name description box - if (isEditing && agent) { - setAgentName(agent.name || ""); - setAgentDescription(agent.description || ""); - setAgentAuthor(agent.author || ""); - setBusinessLogicError(false); - } else if (!isEditing) { - // When stopping editing, clear name description box - setAgentName(""); - setAgentDescription(""); - setAgentDisplayName(""); - setAgentAuthor(""); - setBusinessLogicError(false); - } - }; - - const getCurrentAgentId = () => { - // In edit mode, always use the currently editing agent's id - if (isEditingAgent && editingAgent) { - return parseInt(editingAgent.id); - } - // In create mode, the agent has not been persisted yet, so there should be no agent_id - if (isCreatingNewAgent) { - return undefined; - } - // Fallback to mainAgentId when not creating and not explicitly editing - return mainAgentId ? parseInt(mainAgentId) : undefined; - }; - - // Handle exit creation mode - should clear cache - const handleExitCreation = () => { - setIsCreatingNewAgent(false); - setBusinessLogic(""); - setDutyContent(""); - setConstraintContent(""); - setFewShotsContent(""); - setAgentName(""); - setAgentDescription(""); - setAgentAuthor(""); - setBusinessLogicError(false); - }; - - // Refresh tool list - const handleToolsRefresh = async (showSuccessMessage = true) => { - try { - const result = await fetchTools(); - if (result.success) { - setTools(result.data); - // Only show success message if explicitly requested (e.g., manual refresh) - // Don't show message when auto-refreshing after MCP tool add/delete - if (showSuccessMessage) { - message.success(t("agentConfig.tools.refreshSuccess")); - } - return result.data; // Return the updated tools list - } else { - message.error(t("agentConfig.tools.refreshFailed")); - return null; - } - } catch (error) { - log.error(t("agentConfig.tools.refreshFailedDebug"), error); - message.error(t("agentConfig.tools.refreshFailed")); - return null; - } - }; - - return ( - -
-
-
- { - setBusinessLogic(value); - // Clear error when user starts typing - if (businessLogicError && value.trim() !== "") { - setBusinessLogicError(false); - } - }} - businessLogicError={businessLogicError} - selectedTools={selectedTools} - setSelectedTools={setSelectedTools} - isCreatingNewAgent={isCreatingNewAgent} - setIsCreatingNewAgent={setIsCreatingNewAgent} - mainAgentModel={mainAgentModel} - setMainAgentModel={setMainAgentModel} - mainAgentModelId={mainAgentModelId} - setMainAgentModelId={setMainAgentModelId} - mainAgentMaxStep={mainAgentMaxStep} - setMainAgentMaxStep={setMainAgentMaxStep} - businessLogicModel={businessLogicModel} - setBusinessLogicModel={setBusinessLogicModel} - businessLogicModelId={businessLogicModelId} - setBusinessLogicModelId={setBusinessLogicModelId} - tools={tools} - subAgentList={subAgentList} - loadingAgents={loadingAgents} - mainAgentId={mainAgentId} - setMainAgentId={setMainAgentId} - setSubAgentList={setSubAgentList} - enabledAgentIds={enabledAgentIds} - setEnabledAgentIds={setEnabledAgentIds} - onEditingStateChange={handleEditingStateChange} - onToolsRefresh={handleToolsRefresh} - dutyContent={dutyContent} - setDutyContent={setDutyContent} - constraintContent={constraintContent} - setConstraintContent={setConstraintContent} - fewShotsContent={fewShotsContent} - setFewShotsContent={setFewShotsContent} - agentName={agentName} - setAgentName={setAgentName} - agentDescription={agentDescription} - setAgentDescription={setAgentDescription} - agentDisplayName={agentDisplayName} - setAgentDisplayName={setAgentDisplayName} - agentAuthor={agentAuthor} - setAgentAuthor={setAgentAuthor} - isGeneratingAgent={isGeneratingAgent} - // SystemPromptDisplay related props - onDebug={() => { - setIsDebugDrawerOpen(true); - }} - getCurrentAgentId={getCurrentAgentId} - onGenerateAgent={handleGenerateAgent} - onExportAgent={handleExportAgent} - onDeleteAgent={handleDeleteAgent} - editingAgent={editingAgent} - onExitCreation={handleExitCreation} - isEmbeddingConfigured={isEmbeddingConfigured} - onUnsavedChange={(dirty) => { - unsavedRef.current = dirty; - }} - registerSaveHandler={(handler) => { - saveHandlerRef.current = handler; - }} - registerReloadHandler={(handler) => { - reloadHandlerRef.current = handler; - }} - /> -
-
-
- - {/* Debug drawer */} - setIsDebugDrawerOpen(false)} - open={isDebugDrawerOpen} - width={LAYOUT_CONFIG.DRAWER_WIDTH} - destroyOnClose={true} - styles={{ - body: { - padding: 0, - height: "100%", - overflow: "hidden", - }, - }} - > -
- -
-
-
- ); -}); diff --git a/frontend/app/[locale]/agents/AgentSetupOrchestrator.tsx b/frontend/app/[locale]/agents/AgentSetupOrchestrator.tsx new file mode 100644 index 000000000..99f3c7d3b --- /dev/null +++ b/frontend/app/[locale]/agents/AgentSetupOrchestrator.tsx @@ -0,0 +1,70 @@ +"use client"; + +import { Card, Row, Col } from "antd"; + +import AgentManageComp from "./components/AgentManageComp"; +import AgentConfigComp from "./components/AgentConfigComp"; +import AgentInfoComp from "./components/AgentInfoComp"; + +interface AgentSetupOrchestratorProps { + onImportAgent?: () => void; +} + +export default function AgentSetupOrchestrator({ + onImportAgent, +}: AgentSetupOrchestratorProps) { + return ( + + + {/* Three-column layout using Ant Design Grid */} + + {/* Left column: Agent Management */} + + + + + {/* Middle column: Agent Config */} + + + + + {/* Right column: Agent Info */} + + + + + + ); +} diff --git a/frontend/app/[locale]/agents/AgentsContent.tsx b/frontend/app/[locale]/agents/AgentsContent.tsx deleted file mode 100644 index 84f29ac28..000000000 --- a/frontend/app/[locale]/agents/AgentsContent.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; - -import React, {useState, useEffect, useRef, forwardRef, useImperativeHandle} from "react"; -import {motion} from "framer-motion"; - -import {useSetupFlow} from "@/hooks/useSetupFlow"; -import { - ConnectionStatus, -} from "@/const/modelConfig"; - -import AgentConfig, {AgentConfigHandle} from "./AgentConfiguration"; - -interface AgentsContentProps { - /** Whether currently saving */ - isSaving?: boolean; - /** Connection status */ - connectionStatus?: ConnectionStatus; - /** Is checking connection */ - isCheckingConnection?: boolean; - /** Check connection callback */ - onCheckConnection?: () => void; - /** Callback to expose connection status */ - onConnectionStatusChange?: (status: ConnectionStatus) => void; - /** Callback to expose saving state */ - onSavingStateChange?: (isSaving: boolean) => void; -} - -/** - * AgentsContent - Main component for agent configuration - * Can be used in setup flow or as standalone page - */ -export default forwardRef(function AgentsContent({ - isSaving: externalIsSaving, - connectionStatus: externalConnectionStatus, - isCheckingConnection: externalIsCheckingConnection, - onCheckConnection: externalOnCheckConnection, - onConnectionStatusChange, - onSavingStateChange, -}: AgentsContentProps, ref) { - const agentConfigRef = useRef(null); - - // Use custom hook for common setup flow logic - const { - canAccessProtectedData, - pageVariants, - pageTransition, - } = useSetupFlow({ - requireAdmin: true, - externalConnectionStatus, - externalIsCheckingConnection, - onCheckConnection: externalOnCheckConnection, - onConnectionStatusChange, - nonAdminRedirect: "/setup/knowledges", - }); - - const [internalIsSaving, setInternalIsSaving] = useState(false); - const isSaving = externalIsSaving ?? internalIsSaving; - - // Expose AgentConfigHandle methods to parent - useImperativeHandle(ref, () => ({ - hasUnsavedChanges: () => agentConfigRef.current?.hasUnsavedChanges?.() ?? false, - saveAllChanges: async () => { - if (agentConfigRef.current?.saveAllChanges) { - await agentConfigRef.current.saveAllChanges(); - } - }, - reloadCurrentAgentData: async () => { - if (agentConfigRef.current?.reloadCurrentAgentData) { - await agentConfigRef.current.reloadCurrentAgentData(); - } - }, - }), []); - - // Update external saving state - useEffect(() => { - onSavingStateChange?.(isSaving); - }, [isSaving, onSavingStateChange]); - - return ( - <> - -
- {canAccessProtectedData ? ( - - ) : null} -
-
- - ); -}); - diff --git a/frontend/app/[locale]/agents/components/AgentConfigComp.tsx b/frontend/app/[locale]/agents/components/AgentConfigComp.tsx new file mode 100644 index 000000000..b5dbb459f --- /dev/null +++ b/frontend/app/[locale]/agents/components/AgentConfigComp.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { App, Button, Row, Col, Flex, Tooltip, Badge, Divider } from "antd"; +import CollaborativeAgent from "./agentConfig/CollaborativeAgent"; +import ToolManagement from "./agentConfig/ToolManagement"; + +import { updateToolList } from "@/services/mcpService"; +import { useAgentConfigStore } from "@/stores/agentConfigStore"; +import { useToolList } from "@/hooks/agent/useToolList"; +import McpConfigModal from "./agentConfig/McpConfigModal"; + +import { RefreshCw, Lightbulb, Plug } from "lucide-react"; + +interface AgentConfigCompProps {} + +export default function AgentConfigComp({}: AgentConfigCompProps) { + const { t } = useTranslation("common"); + const { message } = App.useApp(); + + // Get state from store + const currentAgentId = useAgentConfigStore((state) => state.currentAgentId); + + const isCreatingMode = useAgentConfigStore((state) => state.isCreatingMode); + + const editable = !!(currentAgentId || isCreatingMode); + + const [isMcpModalOpen, setIsMcpModalOpen] = useState(false); + const [isRefreshing, setIsRefreshing] = useState(false); + + // Use tool list hook for data management + const { groupedTools, invalidate } = useToolList(); + + const handleRefreshTools = useCallback(async () => { + setIsRefreshing(true); + try { + // Step 1: Update backend tool status, rescan MCP and local tools + const updateResult = await updateToolList(); + if (!updateResult.success) { + message.warning(t("toolManagement.message.updateStatusFailed")); + } + + // Step 2: Invalidate and refresh tool list cache + invalidate(); + message.success(t("toolManagement.message.refreshSuccess")); + } catch (error) { + message.error(t("toolManagement.message.refreshFailedRetry")); + } finally { + setIsRefreshing(false); + } + }, [invalidate]); + + + return ( + <> + {/* Import handled by Ant Design Upload (no hidden input required) */} + + + + + +

+ {t("businessLogic.config.title")} +

+
+ +
+ + + + + + + + + + +

+ {t("toolPool.title")} +

+ + {t("toolPool.tooltip.functionGuide")} + + } + color="#ffffff" + styles={{ + root: { + backgroundColor: "#ffffff", + color: "#374151", + border: "1px solid #e5e7eb", + borderRadius: "6px", + boxShadow: + "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)", + padding: "12px", + maxWidth: "800px", + minWidth: "700px", + width: "fit-content", + }, + }} + > + + +
+ + + + + + + +
+ + + + + + + + +
+ + setIsMcpModalOpen(false)} + /> + + ); +} diff --git a/frontend/app/[locale]/agents/components/AgentInfoComp.tsx b/frontend/app/[locale]/agents/components/AgentInfoComp.tsx new file mode 100644 index 000000000..d4832b6f3 --- /dev/null +++ b/frontend/app/[locale]/agents/components/AgentInfoComp.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { Row, Col, Flex, Badge, Divider, Button, Drawer, App } from "antd"; +import { Bug, Save, Info } from "lucide-react"; + +import { AGENT_SETUP_LAYOUT_DEFAULT } from "@/const/agentConfig"; +import { useAgentConfigStore } from "@/stores/agentConfigStore"; +import { useSaveGuard } from "@/hooks/agent/useSaveGuard"; +import { AgentBusinessInfo, AgentProfileInfo } from "@/types/agentConfig"; + +import AgentGenerateDetail from "./agentInfo/AgentGenerateDetail"; +import DebugConfig from "./agentInfo/DebugConfig"; + +export interface AgentInfoCompProps {} + +export default function AgentInfoComp({}: AgentInfoCompProps) { + const { t } = useTranslation("common"); + + // Get data from store + const { editedAgent, updateBusinessInfo, updateProfileInfo, isCreatingMode } = + useAgentConfigStore(); + + // Get state from store + const currentAgentId = useAgentConfigStore((state) => state.currentAgentId); + + const editable = !!(currentAgentId || isCreatingMode); + + // Save guard hook + const saveGuard = useSaveGuard(); + + // Debug drawer state + const [isDebugDrawerOpen, setIsDebugDrawerOpen] = useState(false); + + // Handle business info updates + const handleUpdateBusinessInfo = (updates: AgentBusinessInfo) => { + updateBusinessInfo(updates); + }; + + // Handle profile info updates + const handleUpdateProfile = (updates: AgentProfileInfo) => { + updateProfileInfo(updates); + }; + + return ( + <> + { + + + + + +

+ {t("guide.steps.describeBusinessLogic.title")} +

+
+ +
+ + + + + + + + + + + + + + + + + + +
+ } + + {!editable && ( + +
+
+
+ +

+ {t("systemPrompt.nonEditing.title")} +

+
+

+ {t("systemPrompt.nonEditing.subtitle")} +

+
+
+
+ )} + + {/* Debug drawer */} + setIsDebugDrawerOpen(false)} + open={isDebugDrawerOpen} + styles={{ + wrapper: { + width: AGENT_SETUP_LAYOUT_DEFAULT.DRAWER_WIDTH, + }, + body: { + padding: 0, + height: "100%", + overflow: "hidden", + }, + }} + > +
+ +
+
+ + ); +} diff --git a/frontend/app/[locale]/agents/components/AgentManageComp.tsx b/frontend/app/[locale]/agents/components/AgentManageComp.tsx new file mode 100644 index 000000000..7e5afe3c9 --- /dev/null +++ b/frontend/app/[locale]/agents/components/AgentManageComp.tsx @@ -0,0 +1,252 @@ +"use client"; + +import { useTranslation } from "react-i18next"; +import { + App, + Row, + Col, + Flex, + Tooltip, + Badge, + Divider, + Upload, + theme, +} from "antd"; +import { FileInput, Plus, X } from "lucide-react"; + +import { Agent } from "@/types/agentConfig"; +import AgentList from "./agentManage/AgentList"; +import { useSaveGuard } from "@/hooks/agent/useSaveGuard"; +import { useCallback } from "react"; +import { useAgentConfigStore } from "@/stores/agentConfigStore"; +import { importAgent } from "@/services/agentConfigService"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useAgentList } from "@/hooks/agent/useAgentList"; +import { useAgentInfo } from "@/hooks/agent/useAgentInfo"; +import log from "@/lib/logger"; +import { useState, useEffect } from "react"; + +interface AgentManageCompProps { + onImportAgent?: () => void; +} + +export default function AgentManageComp({ + onImportAgent, +}: AgentManageCompProps) { + const { t } = useTranslation("common"); + const { message } = App.useApp(); + + // Get state from store + const currentAgentId = useAgentConfigStore((state) => state.currentAgentId); + const hasUnsavedChanges = useAgentConfigStore( + (state) => state.hasUnsavedChanges + ); + const isCreatingMode = useAgentConfigStore((state) => state.isCreatingMode); + const setCurrentAgent = useAgentConfigStore((state) => state.setCurrentAgent); + const enterCreateMode = useAgentConfigStore((state) => state.enterCreateMode); + const reset = useAgentConfigStore((state) => state.reset); + + // Unsaved changes guard + const checkUnsavedChanges = useSaveGuard(); + + // Handle unsaved changes check and agent switching + const handleAgentSwitch = useCallback( + async (agentDetail: any) => { + const canSwitch = await checkUnsavedChanges.saveWithModal(); + if (canSwitch) { + setCurrentAgent(agentDetail); + } + }, + [checkUnsavedChanges] + ); + + const editable = currentAgentId || isCreatingMode; + + // Shared agent list via React Query + const { agents: agentList, isLoading: loading, refetch } = useAgentList(); + const queryClient = useQueryClient(); + + // State for selected agent info loading + const [selectedAgentId, setSelectedAgentId] = useState(null); + + const { + data: agentDetail, + isLoading: agentInfoLoading, + error: agentInfoError, + } = useAgentInfo(selectedAgentId); + + const importAgentMutation = useMutation({ + mutationFn: (agentData: any) => importAgent(agentData), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["agents"] }), + }); + + // Handle agent detail loading completion + useEffect(() => { + if ( + selectedAgentId && + agentDetail && + !agentInfoLoading && + !agentInfoError + ) { + // Handle agent switch with unsaved changes check + handleAgentSwitch(agentDetail); + setSelectedAgentId(null); + } else if (selectedAgentId && agentInfoError && !agentInfoLoading) { + // Handle error + log.error("Failed to load agent detail:", agentInfoError); + message.error(t("agentConfig.agents.detailsLoadFailed")); + setSelectedAgentId(null); + } + }, [ + selectedAgentId, + agentDetail, + agentInfoLoading, + agentInfoError, + handleAgentSwitch, + message, + t, + ]); + + // Handle select agent + const handleSelectAgent = async (agent: Agent) => { + // If already selected, deselect it + if ( + currentAgentId !== null && + String(currentAgentId) === String(agent.id) + ) { + const canDeselect = await checkUnsavedChanges.saveWithModal(); + if (canDeselect) { + setCurrentAgent(null); + } + return; + } + + // Set selected agent id to trigger the hook + setSelectedAgentId(Number(agent.id)); + }; + + return ( + <> + {/* Import handled by Ant Design Upload (no hidden input required) */} + + + + + +

+ {t("subAgentPool.management")} +

+
+ +
+ + + + + + {isCreatingMode ? ( + +
+ + + + +
+ {t("subAgentPool.button.exitCreate")} +
+
+ {t("subAgentPool.description.exitCreate")} +
+
+
+
+
+ ) : ( + +
+ + + + +
+ {t("subAgentPool.button.create")} +
+
+ {t("subAgentPool.description.createAgent")} +
+
+
+
+
+ )} + + + + +
+ + + + +
+ {t("subAgentPool.button.import")} +
+
+ {t("subAgentPool.description.importAgent")} +
+
+
+
+
+ +
+ +
+ { + if (currentAgentId === agentId) { + setCurrentAgent(null); + } + }} + /> +
+
+ + ); +} diff --git a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx deleted file mode 100644 index e5718c98b..000000000 --- a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx +++ /dev/null @@ -1,2446 +0,0 @@ -"use client"; - -import { useState, useEffect, useCallback, useRef, useMemo } from "react"; -import { useTranslation } from "react-i18next"; -import { TFunction } from "i18next"; - -import { App, Modal, Button, Tooltip, Row, Col } from "antd"; -import { WarningFilled } from "@ant-design/icons"; - -import { TooltipProvider } from "@/components/ui/tooltip"; -import { - fetchAgentList, - updateAgent, - deleteAgent, - exportAgent, - searchAgentInfo, - searchToolConfig, - updateToolConfig, -} from "@/services/agentConfigService"; -import { useAgentImport, ImportAgentData } from "@/hooks/useAgentImport"; -import { - Agent, - AgentSetupOrchestratorProps, - Tool, - ToolParam, -} from "@/types/agentConfig"; -import AgentImportWizard from "@/components/agent/AgentImportWizard"; -import log from "@/lib/logger"; -import { useConfirmModal } from "@/hooks/useConfirmModal"; -import { useAuth } from "@/hooks/useAuth"; - -import SubAgentPool from "./agent/SubAgentPool"; -import CollaborativeAgentDisplay from "./agent/CollaborativeAgentDisplay"; -import { MemoizedToolPool } from "./tool/ToolPool"; -import PromptManager from "./PromptManager"; -import AgentCallRelationshipModal from "./agent/AgentCallRelationshipModal"; -import SaveConfirmModal from "./SaveConfirmModal"; - -type PendingAction = () => void | Promise; - -/** - * Agent Setup Orchestrator - Main coordination component for agent setup workflow - */ -export default function AgentSetupOrchestrator({ - businessLogic, - setBusinessLogic, - businessLogicError = false, - selectedTools, - setSelectedTools, - isCreatingNewAgent, - setIsCreatingNewAgent, - mainAgentModel, - setMainAgentModel, - mainAgentModelId, - setMainAgentModelId, - mainAgentMaxStep, - setMainAgentMaxStep, - businessLogicModel, - setBusinessLogicModel, - businessLogicModelId, - setBusinessLogicModelId, - tools, - subAgentList = [], - loadingAgents = false, - mainAgentId, - setMainAgentId, - setSubAgentList, - enabledAgentIds, - setEnabledAgentIds, - onEditingStateChange, - onToolsRefresh, - dutyContent, - setDutyContent, - constraintContent, - setConstraintContent, - fewShotsContent, - setFewShotsContent, - agentName, - setAgentName, - agentDescription, - setAgentDescription, - agentDisplayName, - setAgentDisplayName, - agentAuthor, - setAgentAuthor, - isGeneratingAgent = false, - // SystemPromptDisplay related props - onDebug, - getCurrentAgentId, - onGenerateAgent, - onExportAgent, - onDeleteAgent, - editingAgent: editingAgentFromParent, - onExitCreation, - isEmbeddingConfigured, - onUnsavedChange, - registerSaveHandler, - registerReloadHandler, -}: AgentSetupOrchestratorProps) { - const { user, isSpeedMode } = useAuth(); - const [enabledToolIds, setEnabledToolIds] = useState([]); - const [isLoadingTools, setIsLoadingTools] = useState(false); - const [isImporting, setIsImporting] = useState(false); - const [toolConfigDrafts, setToolConfigDrafts] = useState< - Record - >({}); - const [pendingImportData, setPendingImportData] = useState<{ - agentInfo: any; - } | null>(null); - const [importingAction, setImportingAction] = useState< - "force" | "regenerate" | null - >(null); - - // Agent import wizard states - const [importWizardVisible, setImportWizardVisible] = useState(false); - const [importWizardData, setImportWizardData] = useState(null); - // Use generation state passed from parent component, not local state - - - - - const lastProcessedAgentIdForEmbedding = useRef(null); - - // Flag to track if we need to refresh enabledToolIds after tools update - const shouldRefreshEnabledToolIds = useRef(false); - // Track previous tools prop to detect when it's updated - const previousToolsRef = useRef(undefined); - - // Call relationship modal state - const [callRelationshipModalVisible, setCallRelationshipModalVisible] = - useState(false); - - // Edit agent related status - const [isEditingAgent, setIsEditingAgent] = useState(false); - const [editingAgent, setEditingAgent] = useState(null); - const activeEditingAgent = editingAgentFromParent || editingAgent; - const isAgentUnavailable = activeEditingAgent?.is_available === false; - const agentUnavailableReasons = - isAgentUnavailable && Array.isArray(activeEditingAgent?.unavailable_reasons) - ? (activeEditingAgent?.unavailable_reasons as string[]) - : []; - const mergeAgentAvailabilityMetadata = useCallback( - (detail: Agent, fallback?: Agent | null): Agent => { - const detailReasons = Array.isArray(detail?.unavailable_reasons) - ? detail.unavailable_reasons - : []; - const fallbackReasons = Array.isArray(fallback?.unavailable_reasons) - ? fallback!.unavailable_reasons! - : []; - const normalizedReasons = - detailReasons.length > 0 ? detailReasons : fallbackReasons; - - const normalizedAvailability = - normalizedReasons.length > 0 - ? false - : typeof detail?.is_available === "boolean" - ? detail.is_available - : typeof fallback?.is_available === "boolean" - ? fallback.is_available - : detail?.is_available; - - return { - ...detail, - unavailable_reasons: normalizedReasons, - is_available: normalizedAvailability, - }; - }, - [] - ); - - const numericMainAgentId = - mainAgentId !== null && - mainAgentId !== undefined && - String(mainAgentId).trim() !== "" - ? Number(mainAgentId) - : null; - const hasPersistedMainAgentId = - typeof numericMainAgentId === "number" && - !Number.isNaN(numericMainAgentId) && - numericMainAgentId > 0; - const isDraftCreationSession = - isCreatingNewAgent && !hasPersistedMainAgentId && !isEditingAgent; - - // Add a flag to track if it has been initialized to avoid duplicate calls - const hasInitialized = useRef(false); - // Baseline snapshot for change detection - const baselineRef = useRef(null); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [pendingAction, setPendingAction] = useState( - null - ); - const [isSaveConfirmOpen, setIsSaveConfirmOpen] = useState(false); - // When true, bypass unsaved-check to avoid reopening the confirm modal during a confirmed switch - const skipUnsavedCheckRef = useRef(false); - // Context for confirmation modal behavior - const [confirmContext, setConfirmContext] = useState<"switch" | "exitCreate">( - "switch" - ); - - const { t } = useTranslation("common"); - const { message } = App.useApp(); - const { confirm } = useConfirmModal(); - - // Common refresh agent list function, moved to the front to avoid hoisting issues - const refreshAgentList = async (t: TFunction, clearTools: boolean = true) => { - if (clearTools) { - setIsLoadingTools(true); - // Clear the tool selection status when loading starts - setSelectedTools([]); - setEnabledToolIds([]); - } - - try { - const result = await fetchAgentList(); - if (result.success) { - // Update agent list with basic info only - setSubAgentList(result.data); - // Removed success message to avoid duplicate notifications - } else { - message.error( - result.message || t("businessLogic.config.error.agentListFailed") - ); - } - } catch (error) { - log.error(t("agentConfig.agents.listFetchFailedDebug"), error); - message.error(t("businessLogic.config.error.agentListFailed")); - } finally { - if (clearTools) { - setIsLoadingTools(false); - } - } - }; - // Build current snapshot for dirty detection - const currentSnapshot = useMemo( - () => ({ - agentId: - (isEditingAgent && editingAgent ? editingAgent.id : mainAgentId) ?? - null, - agentName: agentName || "", - agentDescription: agentDescription || "", - agentDisplayName: agentDisplayName || "", - businessLogic: businessLogic || "", - dutyContent: dutyContent || "", - constraintContent: constraintContent || "", - fewShotsContent: fewShotsContent || "", - mainAgentModelId: mainAgentModelId ?? null, - businessLogicModelId: businessLogicModelId ?? null, - mainAgentMaxStep: Number(mainAgentMaxStep ?? 5), - enabledAgentIds: Array.from( - new Set( - (enabledAgentIds || []).map((n) => Number(n)).filter((n) => !isNaN(n)) - ) - ).sort((a, b) => a - b), - enabledToolIds: Array.from( - new Set( - (enabledToolIds || []).map((n) => Number(n)).filter((n) => !isNaN(n)) - ) - ).sort((a, b) => a - b), - selectedToolIds: Array.from( - new Set( - (selectedTools || []) - .map((t: any) => Number(t.id)) - .filter((id: number) => !isNaN(id)) - ) - ).sort((a, b) => a - b), - }), - [ - isEditingAgent, - editingAgent, - mainAgentId, - agentName, - agentDescription, - agentDisplayName, - businessLogic, - dutyContent, - constraintContent, - fewShotsContent, - mainAgentModelId, - businessLogicModelId, - mainAgentMaxStep, - enabledAgentIds, - enabledToolIds, - selectedTools, - ] - ); - - // Initialize baseline when entering edit mode or loading agent details - useEffect(() => { - if (isEditingAgent && editingAgent) { - baselineRef.current = { ...currentSnapshot }; - setHasUnsavedChanges(false); - } - }, [isEditingAgent, editingAgent]); - - useEffect(() => { - if (!isDraftCreationSession) { - setToolConfigDrafts({}); - } - }, [isDraftCreationSession]); - - // Initialize baseline when entering create mode so draft changes don't attach to previous agent - useEffect(() => { - if (isCreatingNewAgent && !isEditingAgent) { - // Ensure state clears have applied, then capture a clean baseline - setTimeout(() => { - baselineRef.current = { ...currentSnapshot }; - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - }, 0); - } - }, [isCreatingNewAgent, isEditingAgent, currentSnapshot, onUnsavedChange]); - - // Track changes to mark dirty - useEffect(() => { - if (!baselineRef.current) return; - const b = baselineRef.current; - const c = currentSnapshot; - const shallowEqual = - String(b.agentId ?? "") === String(c.agentId ?? "") && - b.agentName === c.agentName && - b.agentDescription === c.agentDescription && - b.agentDisplayName === c.agentDisplayName && - b.businessLogic === c.businessLogic && - b.dutyContent === c.dutyContent && - b.constraintContent === c.constraintContent && - b.fewShotsContent === c.fewShotsContent && - String(b.mainAgentModelId ?? "") === String(c.mainAgentModelId ?? "") && - String(b.businessLogicModelId ?? "") === - String(c.businessLogicModelId ?? "") && - Number(b.mainAgentMaxStep ?? 5) === Number(c.mainAgentMaxStep ?? 5) && - JSON.stringify(b.enabledAgentIds || []) === - JSON.stringify(c.enabledAgentIds || []) && - JSON.stringify(b.enabledToolIds || []) === - JSON.stringify(c.enabledToolIds || []) && - JSON.stringify(b.selectedToolIds || []) === - JSON.stringify(c.selectedToolIds || []); - setHasUnsavedChanges(!shallowEqual); - onUnsavedChange?.(!shallowEqual); - }, [currentSnapshot]); - - // Reload current agent's complete data from backend - const reloadCurrentAgentData = useCallback(async () => { - const currentAgentId = - (isEditingAgent && editingAgent ? editingAgent.id : mainAgentId) ?? null; - - if (!currentAgentId) { - // If no agent ID, just reset unsaved state - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - return; - } - - try { - // Call query interface to get complete Agent information - const result = await searchAgentInfo(Number(currentAgentId)); - - if (!result.success || !result.data) { - message.error( - result.message || t("businessLogic.config.error.agentDetailFailed") - ); - return; - } - - const agentDetail = mergeAgentAvailabilityMetadata( - result.data as Agent, - editingAgent - ); - setEditingAgent(agentDetail); - - // Reload all agent data to match backend state - setAgentName?.(agentDetail.name || ""); - setAgentDescription?.(agentDetail.description || ""); - setAgentDisplayName?.(agentDetail.display_name || ""); - setAgentAuthor?.(agentDetail.author || ""); - - // Load Agent data to interface - setMainAgentModel(agentDetail.model); - setMainAgentModelId(agentDetail.model_id ?? null); - setMainAgentMaxStep(agentDetail.max_step); - setBusinessLogic(agentDetail.business_description || ""); - setBusinessLogicModel(agentDetail.business_logic_model_name || null); - setBusinessLogicModelId(agentDetail.business_logic_model_id || null); - - // Use backend returned sub_agent_id_list to set enabled agent list - if ( - agentDetail.sub_agent_id_list && - agentDetail.sub_agent_id_list.length > 0 - ) { - setEnabledAgentIds( - agentDetail.sub_agent_id_list.map((id: any) => Number(id)) - ); - } else { - setEnabledAgentIds([]); - } - - // Load the segmented prompt content - setDutyContent?.(agentDetail.duty_prompt || ""); - setConstraintContent?.(agentDetail.constraint_prompt || ""); - setFewShotsContent?.(agentDetail.few_shots_prompt || ""); - - // Load Agent tools - // Only set enabledToolIds, let useEffect sync selectedTools from tools array - // This ensures tool objects in selectedTools match the structure in tools array - if (agentDetail.tools && agentDetail.tools.length > 0) { - // Set enabled tool IDs, ensure deduplication - const toolIds = Array.from( - new Set( - agentDetail.tools - .map((tool: any) => Number(tool.id)) - .filter((id: number) => !isNaN(id)) - ) - ).sort((a, b) => a - b); - setEnabledToolIds(toolIds); - // Don't set selectedTools directly - let useEffect handle it based on enabledToolIds - // This ensures tool objects match the structure from tools array - } else { - setEnabledToolIds([]); - // Don't set selectedTools directly - let useEffect handle it - } - - // Refresh agent list to ensure consistency, but don't clear tools to avoid flash - await refreshAgentList(t, false); - - // Update baseline and reset unsaved state after reload - // Use setTimeout to ensure state updates are processed before updating baseline - setTimeout(() => { - // Rebuild snapshot after state updates to get accurate baseline - const updatedSnapshot = { - agentId: Number(currentAgentId), - agentName: agentDetail.name || "", - agentDescription: agentDetail.description || "", - agentDisplayName: agentDetail.display_name || "", - businessLogic: agentDetail.business_description || "", - dutyContent: agentDetail.duty_prompt || "", - constraintContent: agentDetail.constraint_prompt || "", - fewShotsContent: agentDetail.few_shots_prompt || "", - mainAgentModelId: agentDetail.model_id ?? null, - businessLogicModelId: agentDetail.business_logic_model_id ?? null, - mainAgentMaxStep: Number(agentDetail.max_step ?? 5), - enabledAgentIds: (agentDetail.sub_agent_id_list || []) - .map((id: any) => Number(id)) - .sort(), - enabledToolIds: Array.from( - new Set( - (agentDetail.tools || []) - .map((tool: any) => Number(tool.id)) - .filter((id: number) => !isNaN(id)) - ) - ).sort((a, b) => a - b), - selectedToolIds: Array.from( - new Set( - (agentDetail.tools || []) - .map((tool: any) => Number(tool.id)) - .filter((id: number) => !isNaN(id)) - ) - ).sort((a, b) => a - b), - }; - baselineRef.current = updatedSnapshot; - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - }, 200); - } catch (error) { - log.error(t("agentConfig.agents.detailsLoadFailed"), error); - message.error(t("businessLogic.config.error.agentDetailFailed")); - // Even on error, reset unsaved state - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - } - }, [ - isEditingAgent, - editingAgent, - mainAgentId, - currentSnapshot, - t, - refreshAgentList, - onUnsavedChange, - ]); - - // Expose a save function to be reused by confirm modal flows - const saveAllChanges = useCallback(async () => { - await handleSaveNewAgent( - agentName || "", - agentDescription || "", - mainAgentModel, - mainAgentMaxStep, - businessLogic - ); - // Reload data from backend after save to ensure consistency - await reloadCurrentAgentData(); - }, [ - agentName, - agentDescription, - mainAgentModel, - mainAgentMaxStep, - businessLogic, - reloadCurrentAgentData, - ]); - - useEffect(() => { - if (registerSaveHandler) { - registerSaveHandler(saveAllChanges); - } - }, [registerSaveHandler, saveAllChanges]); - - useEffect(() => { - if (registerReloadHandler) { - registerReloadHandler(reloadCurrentAgentData); - } - }, [registerReloadHandler, reloadCurrentAgentData]); - - const confirmOrRun = useCallback( - (action: PendingAction) => { - // In creation mode, always show save confirmation dialog when clicking debug - // Also show when there are unsaved changes - if ((isCreatingNewAgent && !isEditingAgent) || hasUnsavedChanges) { - setPendingAction(() => action); - setConfirmContext("switch"); - setIsSaveConfirmOpen(true); - } else { - void Promise.resolve(action()); - } - }, - [hasUnsavedChanges, isCreatingNewAgent, isEditingAgent] - ); - - const handleToolConfigDraftSave = useCallback( - (updatedTool: Tool) => { - if (!isDraftCreationSession) { - return; - } - setToolConfigDrafts((prev) => ({ - ...prev, - [updatedTool.id]: - updatedTool.initParams?.map((param) => ({ ...param })) || [], - })); - setSelectedTools((prev: Tool[]) => { - if (!prev || prev.length === 0) { - return prev; - } - const index = prev.findIndex((tool) => tool.id === updatedTool.id); - if (index === -1) { - return prev; - } - const next = [...prev]; - next[index] = { - ...updatedTool, - initParams: - updatedTool.initParams?.map((param) => ({ ...param })) || [], - }; - return next; - }); - }, - [isDraftCreationSession, setSelectedTools] - ); - - // Function to directly update enabledAgentIds - const handleUpdateEnabledAgentIds = (newEnabledAgentIds: number[]) => { - setEnabledAgentIds(newEnabledAgentIds); - }; - - // Removed creation-mode sub-agent fetch; creation is deferred until saving - - // Listen for changes in the creation of a new Agent - useEffect(() => { - if (isCreatingNewAgent) { - if (!isEditingAgent) { - // Clear configuration in creating mode - setBusinessLogic(""); - } else { - // In edit mode, data is loaded in handleEditAgent, here validate the form - } - } else { - // When exiting the creation of a new Agent, reset the main Agent configuration - // Only refresh list when exiting creation mode in non-editing mode to avoid flicker when exiting editing mode - if (!isEditingAgent && hasInitialized.current) { - setBusinessLogic(""); - setMainAgentModel(null); - setMainAgentModelId(null); - setMainAgentMaxStep(5); - // Delay refreshing agent list to avoid jumping - setTimeout(() => { - refreshAgentList(t); - }, 200); - } - // Sign that has been initialized - hasInitialized.current = true; - } - }, [isCreatingNewAgent, isEditingAgent, mainAgentId]); - - const applyDraftParamsToTool = useCallback( - (tool: Tool): Tool => { - if (!isDraftCreationSession) { - return tool; - } - const draft = toolConfigDrafts[tool.id]; - if (!draft || draft.length === 0) { - return tool; - } - return { - ...tool, - initParams: draft.map((param) => ({ ...param })), - }; - }, - [isDraftCreationSession, toolConfigDrafts] - ); - - // Listen for changes in the tool status, update the selected tool - useEffect(() => { - if (!tools || isLoadingTools) return; - // Allow empty enabledToolIds array (it's valid when no tools are selected) - if (enabledToolIds === undefined || enabledToolIds === null) return; - - // Filter out unavailable tools (is_available === false) to prevent deleted MCP tools from showing - const enabledTools = tools - .filter( - (tool) => - enabledToolIds.includes(Number(tool.id)) && - tool.is_available !== false - ) - .map((tool) => applyDraftParamsToTool(tool)); - - setSelectedTools(enabledTools); - }, [ - tools, - enabledToolIds, - isLoadingTools, - applyDraftParamsToTool, - setSelectedTools, - ]); - - // Auto-unselect knowledge_base_search if embedding is not configured - useEffect(() => { - if (isEmbeddingConfigured) return; - if (!tools || tools.length === 0) return; - - const kbTool = tools.find((tool) => tool.name === "knowledge_base_search"); - if (!kbTool) return; - - const currentAgentId = ( - isEditingAgent && editingAgent - ? Number(editingAgent.id) - : mainAgentId - ? Number(mainAgentId) - : undefined - ) as number | undefined; - - if (!currentAgentId) return; - if (lastProcessedAgentIdForEmbedding.current === currentAgentId) return; - - const kbToolId = Number(kbTool.id); - if (!enabledToolIds || !enabledToolIds.includes(kbToolId)) { - lastProcessedAgentIdForEmbedding.current = currentAgentId; - return; - } - - const run = async () => { - try { - // Fetch existing params to avoid losing saved configuration - const search = await searchToolConfig(kbToolId, currentAgentId); - const params = - search.success && search.data?.params ? search.data.params : {}; - // Disable the tool - await updateToolConfig(kbToolId, currentAgentId, params, false); - // Update local state - setEnabledToolIds((prev) => prev.filter((id) => id !== kbToolId)); - const nextSelected = selectedTools.filter( - (tool) => tool.id !== kbTool.id - ); - setSelectedTools(nextSelected); - } catch (error) { - // Even if API fails, still inform user and prevent usage in UI - } finally { - confirm({ - title: t("embedding.agentToolAutoDeselectModal.title"), - content: t("embedding.agentToolAutoDeselectModal.content"), - okText: t("common.confirm"), - onOk: () => {}, - }); - lastProcessedAgentIdForEmbedding.current = currentAgentId; - } - }; - - run(); - }, [ - isEmbeddingConfigured, - tools, - enabledToolIds, - isEditingAgent, - editingAgent, - mainAgentId, - ]); - - // Listen for refresh agent list events from parent component - useEffect(() => { - const handleRefreshAgentList = () => { - refreshAgentList(t); - }; - - window.addEventListener("refreshAgentList", handleRefreshAgentList); - - return () => { - window.removeEventListener("refreshAgentList", handleRefreshAgentList); - }; - }, [t]); - - // Listen for tools updated events and refresh enabledToolIds if agent is selected - useEffect(() => { - const handleToolsUpdated = async () => { - // If there's a selected agent (mainAgentId or editingAgent), refresh enabledToolIds - const currentAgentId = (isEditingAgent && editingAgent - ? Number(editingAgent.id) - : mainAgentId - ? Number(mainAgentId) - : undefined) as number | undefined; - - if (currentAgentId) { - try { - // First, refresh the tools list to ensure it's up to date - // Pass false to prevent showing success message (MCP modal will show its own message) - if (onToolsRefresh) { - // First, synchronize the selected tools once using search_info. - await refreshAgentToolSelectionsFromServer(currentAgentId); - // Then refresh the tool list - await onToolsRefresh(false); - // Wait for React state to update and tools prop to be updated - // Use setTimeout to ensure tools prop is updated before refreshing enabledToolIds - await new Promise((resolve) => setTimeout(resolve, 300)); - // Set flag to refresh enabledToolIds after tools prop updates - shouldRefreshEnabledToolIds.current = true; - } - } catch (error) { - log.error("Failed to refresh tools after tools update:", error); - } - } - }; - - window.addEventListener("toolsUpdated", handleToolsUpdated); - - return () => { - window.removeEventListener("toolsUpdated", handleToolsUpdated); - }; - }, [mainAgentId, isEditingAgent, editingAgent, onToolsRefresh, t]); - - const refreshAgentToolSelectionsFromServer = useCallback( - async (agentId: number) => { - try { - const agentInfoResult = await searchAgentInfo(agentId); - if (agentInfoResult.success && agentInfoResult.data) { - const remoteTools = Array.isArray(agentInfoResult.data.tools) - ? agentInfoResult.data.tools - : []; - const enabledIdsFromServer = remoteTools - .filter( - (remoteTool: any) => - remoteTool && remoteTool.is_available !== false - ) - .map((remoteTool: any) => Number(remoteTool.id)) - .filter((id) => !Number.isNaN(id)); - - const filteredIds = enabledIdsFromServer.filter((toolId) => { - const toolMeta = tools?.find( - (tool) => Number(tool.id) === Number(toolId) - ); - return toolMeta && toolMeta.is_available !== false; - }); - - const dedupedIds = Array.from(new Set(filteredIds)); - setEnabledToolIds(dedupedIds); - log.info("Refreshed agent tool selection from search_info", { - agentId, - toolIds: dedupedIds, - }); - } else { - log.error( - "Failed to refresh agent tool selection via search_info", - agentInfoResult.message - ); - } - } catch (error) { - log.error( - "Failed to refresh agent tool selection via search_info:", - error - ); - } - }, - [tools, setEnabledToolIds, setSelectedTools] - ); - - // Refresh enabledToolIds when tools prop updates after toolsUpdated event - useEffect(() => { - const prevTools = previousToolsRef.current; - const haveTools = tools && tools.length > 0; - const prevLen = prevTools?.length ?? 0; - const currLen = tools?.length ?? 0; - const idsChanged = - prevTools === undefined || - JSON.stringify(prevTools?.map((t) => t.id).sort()) !== - JSON.stringify((tools || []).map((t) => t.id).sort()); - const grew = currLen > prevLen; - - // Always update the previous ref for future comparisons - previousToolsRef.current = tools; - - // If there are no tools, nothing to do - if (!haveTools) { - return; - } - - const currentAgentId = (isEditingAgent && editingAgent - ? Number(editingAgent.id) - : mainAgentId - ? Number(mainAgentId) - : undefined) as number | undefined; - - if (!currentAgentId) { - shouldRefreshEnabledToolIds.current = false; - return; - } - - const refreshEnabledToolIds = async () => { - try { - // Small delay to allow tools prop to stabilize after updates - await new Promise((resolve) => setTimeout(resolve, 50)); - await refreshAgentToolSelectionsFromServer(currentAgentId); - } catch (error) { - log.error( - "Failed to refresh enabled tool IDs after tools update:", - error - ); - } - shouldRefreshEnabledToolIds.current = false; - }; - - // Trigger when: - // 1) We explicitly flagged a refresh after a toolsUpdated event, OR - // 2) The tool list grew (e.g., an MCP tool was added) or IDs changed, - // which indicates the available tool set has changed and we should re-sync - if (shouldRefreshEnabledToolIds.current || grew || idsChanged) { - // Optimistically update selected tools to reduce perceived delay/flicker - if (haveTools && Array.isArray(enabledToolIds) && enabledToolIds.length > 0) { - try { - const optimisticSelected = (tools || []).filter((tool) => - enabledToolIds.includes(Number(tool.id)) - ); - setSelectedTools(optimisticSelected); - } catch (e) { - log.warn("Optimistic selection update failed; will rely on refresh", e); - } - } - refreshEnabledToolIds(); - } - }, [ - tools, - mainAgentId, - isEditingAgent, - editingAgent, - enabledToolIds, - refreshAgentToolSelectionsFromServer, - ]); - - // Immediately reflect UI selection from enabledToolIds and latest tools (no server wait) - useEffect(() => { - const haveTools = Array.isArray(tools) && tools.length > 0; - if (!haveTools) { - setSelectedTools([]); - return; - } - if (!Array.isArray(enabledToolIds) || enabledToolIds.length === 0) { - setSelectedTools([]); - return; - } - try { - const nextSelected = (tools || []).filter((tool) => - enabledToolIds.includes(Number(tool.id)) - ); - setSelectedTools(nextSelected); - } catch (e) { - log.warn("Failed to sync selectedTools from enabledToolIds", e); - } - }, [enabledToolIds, tools, setSelectedTools]); - - // When tools change, sanitize enabledToolIds against availability to prevent transient flicker - useEffect(() => { - if (!Array.isArray(tools) || tools.length === 0) { - return; - } - if (!Array.isArray(enabledToolIds)) { - return; - } - const availableIdSet = new Set( - (tools || []) - .filter((t) => t && t.is_available !== false) - .map((t) => Number(t.id)) - .filter((id) => !Number.isNaN(id)) - ); - const sanitized = enabledToolIds.filter((id) => availableIdSet.has(Number(id))); - if ( - sanitized.length !== enabledToolIds.length || - sanitized.some((id, idx) => Number(id) !== Number(enabledToolIds[idx])) - ) { - setEnabledToolIds(sanitized); - } - }, [tools, enabledToolIds, setEnabledToolIds]); - - // Handle the creation of a new Agent - const handleCreateNewAgent = async () => { - // Set to create mode - setIsEditingAgent(false); - setEditingAgent(null); - setIsCreatingNewAgent(true); - - // Clear all content when creating new agent to avoid showing cached data - setBusinessLogic(""); - setDutyContent?.(""); - setConstraintContent?.(""); - setFewShotsContent?.(""); - setAgentName?.(""); - setAgentDescription?.(""); - setAgentDisplayName?.(""); - setAgentAuthor?.(""); - setAgentAuthor?.(""); - - // Clear tool and agent selections - setSelectedTools([]); - setEnabledToolIds([]); - setEnabledAgentIds([]); - setToolConfigDrafts({}); - setMainAgentId?.(null); - - // Clear business logic model to allow default from global settings - // The useEffect in PromptManager will set it to the default from localStorage - setBusinessLogicModel(null); - setBusinessLogicModelId(null); - - // Clear main agent model selection to trigger default model selection - // The useEffect in AgentConfigModal will set it to the default from localStorage - setMainAgentModel(null); - setMainAgentModelId(null); - - try { - await onToolsRefresh?.(false); - } catch (error) { - log.error("Failed to refresh tools in creation mode:", error); - } - - onEditingStateChange?.(false, null); - }; - - // Reset the status when the user cancels the creation of an Agent - const handleCancelCreating = async () => { - // First notify external editing state change to avoid UI jumping - onEditingStateChange?.(false, null); - - // Delay resetting state to let UI complete state switching first - setTimeout(() => { - // Use the parent's exit creation handler to properly clear cache - if (onExitCreation) { - onExitCreation(); - } else { - setIsCreatingNewAgent(false); - } - setIsEditingAgent(false); - setEditingAgent(null); - - // Clear the mainAgentId - setMainAgentId(null); - - // Note: Content clearing is handled by onExitCreation above - // Delay clearing tool and collaborative agent selection to avoid jumping - setTimeout(() => { - setSelectedTools([]); - setEnabledToolIds([]); - setEnabledAgentIds([]); - }, 200); - // Reset unsaved state and baseline - baselineRef.current = null; - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - }, 100); - }; - - // Handle exit edit mode - const handleExitEditMode = async () => { - if (isCreatingNewAgent) { - // If in creation mode, check unsaved changes first - if (hasUnsavedChanges) { - setConfirmContext("exitCreate"); - setPendingAction(null); - setIsSaveConfirmOpen(true); - return; - } - await handleCancelCreating(); - } else if (isEditingAgent) { - // If in editing mode, clear related states first, then update editing state to avoid flickering - // First clear tool and agent selection states - setSelectedTools([]); - setEnabledToolIds([]); - setEnabledAgentIds([]); - - // Clear right-side name description box - setAgentName?.(""); - setAgentDescription?.(""); - - // Clear business logic - setBusinessLogic(""); - - // Clear segmented prompt content - setDutyContent?.(""); - setConstraintContent?.(""); - setFewShotsContent?.(""); - - // Notify external editing state change - onEditingStateChange?.(false, null); - - // Finally update editing state to avoid triggering refresh logic in useEffect - setIsEditingAgent(false); - setEditingAgent(null); - setMainAgentId(null); - - // Ensure tool pool won't show loading state - setIsLoadingTools(false); - - // Reset unsaved state and baseline when explicitly exiting edit mode - baselineRef.current = null; - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - } - }; - - // Handle the creation of a new Agent - const persistDraftToolConfigs = useCallback( - async (agentId: number, toolIdsToEnable: number[]) => { - if (!toolIdsToEnable || toolIdsToEnable.length === 0) { - return; - } - - const payloads = toolIdsToEnable - .map((toolId) => { - const toolIdStr = String(toolId); - const draftParams = toolConfigDrafts[toolIdStr]; - const baseTool = - selectedTools.find((tool) => Number(tool.id) === toolId) || - tools.find((tool) => Number(tool.id) === toolId); - const paramsSource = - (draftParams && draftParams.length > 0 - ? draftParams - : baseTool?.initParams) || []; - if (!paramsSource || paramsSource.length === 0) { - return null; - } - const params = paramsSource.reduce((acc, param) => { - acc[param.name] = param.value; - return acc; - }, {} as Record); - return { - toolId, - params, - }; - }) - .filter(Boolean) as Array<{ - toolId: number; - params: Record; - }>; - - if (payloads.length === 0) { - return; - } - - let persistError = false; - for (const payload of payloads) { - try { - await updateToolConfig(payload.toolId, agentId, payload.params, true); - } catch (error) { - persistError = true; - log.error("Failed to persist tool configuration for new agent:", error); - } - } - - if (persistError) { - message.error(t("toolConfig.message.saveError")); - } - }, - [toolConfigDrafts, selectedTools, tools, message, t] - ); - - const handleSaveNewAgent = async ( - name: string, - description: string, - model: string | null, - max_step: number, - business_description: string - ) => { - if (name.trim()) { - try { - let result; - - // Generate deduplicated enabledToolIds from selectedTools to ensure consistency - const deduplicatedToolIds = Array.from( - new Set( - (selectedTools || []) - .map((tool) => Number(tool.id)) - .filter((id) => !isNaN(id)) - ) - ).sort((a, b) => a - b); - - // Generate deduplicated enabledAgentIds to ensure consistency - const deduplicatedAgentIds = Array.from( - new Set( - (enabledAgentIds || []) - .map((id) => Number(id)) - .filter((id) => !isNaN(id)) - ) - ).sort((a, b) => a - b); - - // Determine author value: use provided author, or default to user email in Full mode - const finalAuthor = agentAuthor || (!isSpeedMode && user?.email ? user.email : undefined); - - if (isEditingAgent && editingAgent) { - // Editing existing agent - result = await updateAgent( - Number(editingAgent.id), - name, - description, - model === null ? undefined : model, - max_step, - false, - true, - business_description, - dutyContent, - constraintContent, - fewShotsContent, - agentDisplayName, - mainAgentModelId ?? undefined, - businessLogicModel ?? undefined, - businessLogicModelId ?? undefined, - deduplicatedToolIds, - deduplicatedAgentIds, - finalAuthor - ); - } else { - // Creating new agent on save - result = await updateAgent( - undefined, - name, - description, - model === null ? undefined : model, - max_step, - false, - true, - business_description, - dutyContent, - constraintContent, - fewShotsContent, - agentDisplayName, - mainAgentModelId ?? undefined, - businessLogicModel ?? undefined, - businessLogicModelId ?? undefined, - deduplicatedToolIds, - deduplicatedAgentIds, - finalAuthor - ); - } - - if (result.success) { - if (!isEditingAgent && result.data?.agent_id) { - await persistDraftToolConfigs( - Number(result.data.agent_id), - deduplicatedToolIds - ); - setToolConfigDrafts({}); - } - // If created, set new mainAgentId for subsequent operations - if (!isEditingAgent && result.data?.agent_id) { - setMainAgentId(String(result.data.agent_id)); - } - message.success(t("businessLogic.config.message.agentSaveSuccess")); - - // Reset unsaved changes state to remove blue indicator - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - - // If editing existing agent, reload data and maintain edit state - if (isEditingAgent && editingAgent) { - // Reload agent data to sync with backend - await reloadCurrentAgentData(); - } else if (result.data?.agent_id) { - // On create success: auto-select and enter edit mode for the new agent - const newId = Number(result.data.agent_id); - try { - const detail = await searchAgentInfo(newId); - if (detail.success && detail.data) { - const agentDetail = mergeAgentAvailabilityMetadata( - detail.data as Agent - ); - setIsEditingAgent(true); - setEditingAgent(agentDetail); - setMainAgentId(agentDetail.id); - setIsCreatingNewAgent(false); - // Populate UI fields - setAgentName?.(agentDetail.name || ""); - setAgentDescription?.(agentDetail.description || ""); - setAgentDisplayName?.(agentDetail.display_name || ""); - setAgentAuthor?.(agentDetail.author || ""); - onEditingStateChange?.(true, agentDetail); - setMainAgentModel(agentDetail.model); - setMainAgentModelId(agentDetail.model_id ?? null); - setMainAgentMaxStep(agentDetail.max_step); - setBusinessLogic(agentDetail.business_description || ""); - setBusinessLogicModel( - agentDetail.business_logic_model_name || null - ); - setBusinessLogicModelId( - agentDetail.business_logic_model_id || null - ); - if ( - agentDetail.sub_agent_id_list && - agentDetail.sub_agent_id_list.length > 0 - ) { - setEnabledAgentIds( - agentDetail.sub_agent_id_list.map((id: any) => Number(id)) - ); - } else { - setEnabledAgentIds([]); - } - setDutyContent?.(agentDetail.duty_prompt || ""); - setConstraintContent?.(agentDetail.constraint_prompt || ""); - setFewShotsContent?.(agentDetail.few_shots_prompt || ""); - if (agentDetail.tools && agentDetail.tools.length > 0) { - setSelectedTools(agentDetail.tools); - setEnabledToolIds( - agentDetail.tools - .map((tool: any) => Number(tool.id)) - .filter((id: number) => !isNaN(id)) as number[] - ); - } else { - setSelectedTools([]); - setEnabledToolIds([]); - } - // Establish clean baseline for the freshly created agent to avoid modal on switch - setTimeout(() => { - baselineRef.current = { - agentId: agentDetail.id, - agentName: agentDetail.name || "", - agentDescription: agentDetail.description || "", - agentDisplayName: agentDetail.display_name || "", - businessLogic: agentDetail.business_description || "", - dutyContent: agentDetail.duty_prompt || "", - constraintContent: agentDetail.constraint_prompt || "", - fewShotsContent: agentDetail.few_shots_prompt || "", - mainAgentModelId: agentDetail.model_id ?? null, - businessLogicModelId: - agentDetail.business_logic_model_id ?? null, - mainAgentMaxStep: Number(agentDetail.max_step ?? 5), - enabledAgentIds: Array.from( - new Set( - (agentDetail.sub_agent_id_list || []) - .map((n: any) => Number(n)) - .filter((n: number) => !isNaN(n)) - ) - ) as number[], - enabledToolIds: Array.from( - new Set( - (agentDetail.tools || []) - .map((t: any) => Number(t.id)) - .filter((id: number) => !isNaN(id)) - ) - ) as number[], - selectedToolIds: Array.from( - new Set( - (agentDetail.tools || []) - .map((t: any) => Number(t.id)) - .filter((id: number) => !isNaN(id)) - ) - ) as number[], - } as any; - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - }, 0); - } else { - // Fallback: set minimal selection - setIsEditingAgent(true); - setEditingAgent({ id: newId } as any); - setMainAgentId(String(newId)); - setIsCreatingNewAgent(false); - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - } - } catch { - setIsEditingAgent(true); - setEditingAgent({ id: newId } as any); - setMainAgentId(String(newId)); - setIsCreatingNewAgent(false); - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - } - } - - // Refresh agent list and keep tools intact to avoid flashing - refreshAgentList(t, false); - } else { - message.error( - result.message || t("businessLogic.config.error.saveFailed") - ); - } - } catch (error) { - log.error("Error saving agent:", error); - message.error(t("businessLogic.config.error.saveRetry")); - } - } else { - if (!name.trim()) { - message.error(t("businessLogic.config.error.nameEmpty")); - } - if (!mainAgentId) { - message.error(t("businessLogic.config.error.noAgentId")); - } - } - }; - - const handleSaveAgent = () => { - // The save button's disabled state is controlled by canSaveAgent, which already validates the required fields. - // We can still add checks here for better user feedback in case the function is triggered unexpectedly. - if (!agentName || agentName.trim() === "") { - message.warning(t("businessLogic.config.message.completeAgentInfo")); - return; - } - - if (!mainAgentModel) { - message.warning(t("businessLogic.config.message.selectModelRequired")); - return; - } - - const hasPromptContent = - dutyContent?.trim() || - constraintContent?.trim() || - fewShotsContent?.trim(); - if (!hasPromptContent) { - message.warning(t("businessLogic.config.message.generatePromptFirst")); - return; - } - - // Always use agentName and agentDescription as they are bound to the inputs in both create and edit modes. - handleSaveNewAgent( - agentName, - agentDescription || "", - mainAgentModel, - mainAgentMaxStep, - businessLogic - ); - }; - - const handleEditAgent = async (agent: Agent, t: TFunction) => { - // Check for unsaved changes before switching agents (unless bypass flag set) - if (hasUnsavedChanges && !skipUnsavedCheckRef.current) { - setPendingAction(() => () => handleEditAgent(agent, t)); - setIsSaveConfirmOpen(true); - return; - } - - try { - // Call query interface to get complete Agent information - const result = await searchAgentInfo(Number(agent.id)); - - if (!result.success || !result.data) { - message.error( - result.message || t("businessLogic.config.error.agentDetailFailed") - ); - return; - } - - const agentDetail = mergeAgentAvailabilityMetadata( - result.data as Agent, - agent - ); - - // Set editing state and highlight after successfully getting information - setIsEditingAgent(true); - setEditingAgent(agentDetail); - // Set mainAgentId to current editing Agent ID - setMainAgentId(agentDetail.id); - // When editing existing agent, ensure exit creation mode AFTER setting all data - // Use setTimeout to ensure all data is set before triggering useEffect - setTimeout(() => { - setIsCreatingNewAgent(false); - }, 100); // Increase delay to ensure state updates are processed - - // First set right-side name description box data to ensure immediate display - - setAgentName?.(agentDetail.name || ""); - setAgentDescription?.(agentDetail.description || ""); - setAgentDisplayName?.(agentDetail.display_name || ""); - setAgentAuthor?.(agentDetail.author || ""); - - // Notify external editing state change (use complete data) - onEditingStateChange?.(true, agentDetail); - - // Load Agent data to interface - setMainAgentModel(agentDetail.model); - setMainAgentModelId(agentDetail.model_id ?? null); - setMainAgentMaxStep(agentDetail.max_step); - setBusinessLogic(agentDetail.business_description || ""); - setBusinessLogicModel(agentDetail.business_logic_model_name || null); - setBusinessLogicModelId(agentDetail.business_logic_model_id || null); - - // Use backend returned sub_agent_id_list to set enabled agent list - if ( - agentDetail.sub_agent_id_list && - agentDetail.sub_agent_id_list.length > 0 - ) { - setEnabledAgentIds( - agentDetail.sub_agent_id_list.map((id: any) => Number(id)) - ); - } else { - setEnabledAgentIds([]); - } - - // Load the segmented prompt content - setDutyContent?.(agentDetail.duty_prompt || ""); - setConstraintContent?.(agentDetail.constraint_prompt || ""); - setFewShotsContent?.(agentDetail.few_shots_prompt || ""); - - // Load Agent tools - // Filter out unavailable tools (is_available === false) to prevent deleted MCP tools from showing - if (agentDetail.tools && agentDetail.tools.length > 0) { - const availableTools = agentDetail.tools.filter( - (tool: any) => tool.is_available !== false - ); - const toolIds = Array.from( - new Set( - availableTools - .map((tool: any) => Number(tool.id)) - .filter((id: number) => !isNaN(id)) - ) - ).sort((a, b) => a - b); - setSelectedTools(availableTools); - setEnabledToolIds(toolIds); - } else { - setSelectedTools([]); - setEnabledToolIds([]); - } - } catch (error) { - log.error(t("agentConfig.agents.detailsLoadFailed"), error); - message.error(t("businessLogic.config.error.agentDetailFailed")); - // If error occurs, reset editing state - setIsEditingAgent(false); - setEditingAgent(null); - // Note: Don't reset isCreatingNewAgent, keep agent pool display - onEditingStateChange?.(false, null); - } - }; - - // Handle the update of the model - // Handle Business Logic Model change - const handleBusinessLogicModelChange = (value: string, modelId?: number) => { - setBusinessLogicModel(value); - if (modelId !== undefined) { - setBusinessLogicModelId(modelId); - } - }; - - const handleModelChange = async (value: string, modelId?: number) => { - const targetAgentId = - isEditingAgent && editingAgent ? editingAgent.id : mainAgentId; - - // Update local state first - setMainAgentModel(value); - if (modelId !== undefined) { - setMainAgentModelId(modelId); - } - - // If no agent ID yet (e.g., during initial creation setup), just update local state - // The model will be saved when the agent is fully created - // Also skip update API call if in create mode (agent not saved yet) - if (!targetAgentId || isCreatingNewAgent) { - return; - } - - // Call updateAgent API to save the model change - try { - const result = await updateAgent( - Number(targetAgentId), - undefined, // name - undefined, // description - value, // modelName - undefined, // maxSteps - undefined, // provideRunSummary - undefined, // enabled - undefined, // businessDescription - undefined, // dutyPrompt - undefined, // constraintPrompt - undefined, // fewShotsPrompt - undefined, // displayName - modelId, // modelId - undefined, // businessLogicModelName - undefined, // businessLogicModelId - undefined // enabledToolIds - ); - - if (!result.success) { - message.error( - result.message || t("businessLogic.config.error.modelUpdateFailed") - ); - // Revert local state on failure - setMainAgentModel(mainAgentModel); - setMainAgentModelId(mainAgentModelId); - } - } catch (error) { - log.error("Error updating agent model:", error); - message.error(t("businessLogic.config.error.modelUpdateFailed")); - // Revert local state on failure - setMainAgentModel(mainAgentModel); - setMainAgentModelId(mainAgentModelId); - } - }; - - // Handle the update of the maximum number of steps - const handleMaxStepChange = async (value: number | null) => { - const targetAgentId = - isEditingAgent && editingAgent ? editingAgent.id : mainAgentId; - - const newValue = value ?? 5; - - // Update local state first - setMainAgentMaxStep(newValue); - - // If no agent ID yet (e.g., during initial creation setup), just update local state - // The max steps will be saved when the agent is fully created - // Also skip update API call if in create mode (agent not saved yet) - if (!targetAgentId || isCreatingNewAgent) { - return; - } - - // Call updateAgent API to save the max steps change - try { - const result = await updateAgent( - Number(targetAgentId), - undefined, // name - undefined, // description - undefined, // modelName - newValue, // maxSteps - undefined, // provideRunSummary - undefined, // enabled - undefined, // businessDescription - undefined, // dutyPrompt - undefined, // constraintPrompt - undefined, // fewShotsPrompt - undefined, // displayName - undefined, // modelId - undefined, // businessLogicModelName - undefined, // businessLogicModelId - undefined // enabledToolIds - ); - - if (!result.success) { - message.error( - result.message || t("businessLogic.config.error.maxStepsUpdateFailed") - ); - // Revert local state on failure - setMainAgentMaxStep(mainAgentMaxStep); - } - } catch (error) { - log.error("Error updating agent max steps:", error); - message.error(t("businessLogic.config.error.maxStepsUpdateFailed")); - // Revert local state on failure - setMainAgentMaxStep(mainAgentMaxStep); - } - }; - - // Use unified import hooks - one for normal import, one for force import - const { importFromData: runNormalImport } = useAgentImport({ - onSuccess: () => { - message.success(t("businessLogic.config.error.agentImportSuccess")); - refreshAgentList(t, false); - }, - onError: (error) => { - log.error(t("agentConfig.agents.importFailed"), error); - message.error(t("businessLogic.config.error.agentImportFailed")); - }, - forceImport: false, - }); - - const { importFromData: runForceImport } = useAgentImport({ - onSuccess: () => { - message.success(t("businessLogic.config.error.agentImportSuccess")); - refreshAgentList(t, false); - }, - onError: (error) => { - log.error(t("agentConfig.agents.importFailed"), error); - message.error(t("businessLogic.config.error.agentImportFailed")); - }, - forceImport: true, - }); - - const runAgentImport = useCallback( - async ( - agentPayload: any, - options?: { forceImport?: boolean } - ) => { - setIsImporting(true); - try { - if (options?.forceImport) { - await runForceImport(agentPayload); - } else { - await runNormalImport(agentPayload); - } - return true; - } catch (error) { - return false; - } finally { - setIsImporting(false); - } - }, - [runNormalImport, runForceImport] - ); - - // Handle importing agent - use AgentImportWizard for ExportAndImportDataFormat - const handleImportAgent = (t: TFunction) => { - // Create a hidden file input element - const fileInput = document.createElement("input"); - fileInput.type = "file"; - fileInput.accept = ".json"; - fileInput.onchange = async (event) => { - setPendingImportData(null); - const file = (event.target as HTMLInputElement).files?.[0]; - if (!file) return; - - // Check file type - if (!file.name.endsWith(".json")) { - message.error(t("businessLogic.config.error.invalidFileType")); - return; - } - - try { - // Read file content - const fileContent = await file.text(); - let agentInfo; - - try { - agentInfo = JSON.parse(fileContent); - } catch (parseError) { - message.error(t("businessLogic.config.error.invalidFileType")); - return; - } - - // Check if it's ExportAndImportDataFormat (has agent_id and agent_info) - if (agentInfo.agent_id && agentInfo.agent_info && typeof agentInfo.agent_info === "object") { - // Use AgentImportWizard for full agent import with configuration - const importData: ImportAgentData = { - agent_id: agentInfo.agent_id, - agent_info: agentInfo.agent_info, - mcp_info: agentInfo.mcp_info || [], - }; - setImportWizardData(importData); - setImportWizardVisible(true); - return; - } - - // Fallback to legacy import logic for other formats - const normalizeValue = (value?: string | null) => - typeof value === "string" ? value.trim() : ""; - - const extractImportedAgents = (data: any): any[] => { - if (!data) { - return []; - } - - if (Array.isArray(data)) { - return data; - } - - if (data.agent_info && typeof data.agent_info === "object") { - return Object.values(data.agent_info).filter( - (item) => item && typeof item === "object" - ); - } - - if (data.agentInfo && typeof data.agentInfo === "object") { - return Object.values(data.agentInfo).filter( - (item) => item && typeof item === "object" - ); - } - - return [data]; - }; - - const importedAgents = extractImportedAgents(agentInfo); - const agentList = Array.isArray(subAgentList) ? subAgentList : []; - - const existingNames = new Set( - agentList - .map((agent) => normalizeValue(agent?.name)) - .filter((name) => !!name) - ); - const existingDisplayNames = new Set( - agentList - .map((agent) => normalizeValue(agent?.display_name)) - .filter((name) => !!name) - ); - - const duplicateNames = Array.from( - new Set( - importedAgents - .map((agent) => normalizeValue(agent?.name)) - .filter( - (name) => name && existingNames.has(name) - ) as string[] - ) - ); - const duplicateDisplayNames = Array.from( - new Set( - importedAgents - .map((agent) => - normalizeValue(agent?.display_name ?? agent?.displayName) - ) - .filter( - (displayName) => - displayName && existingDisplayNames.has(displayName) - ) as string[] - ) - ); - - const hasNameConflict = duplicateNames.length > 0; - const hasDisplayNameConflict = duplicateDisplayNames.length > 0; - - if (hasNameConflict || hasDisplayNameConflict) { - setPendingImportData({ - agentInfo, - }); - } else { - await runAgentImport(agentInfo); - } - } catch (error) { - log.error(t("agentConfig.agents.importFailed"), error); - message.error(t("businessLogic.config.error.agentImportFailed")); - } - }; - - fileInput.click(); - }; - - // Handle import completion from wizard - const handleImportComplete = () => { - refreshAgentList(t, false); - setImportWizardVisible(false); - setImportWizardData(null); - }; - - const handleConfirmedDuplicateImport = useCallback(async () => { - if (!pendingImportData) { - return; - } - setImportingAction("regenerate"); - const success = await runAgentImport(pendingImportData.agentInfo); - if (success) { - setPendingImportData(null); - } - setImportingAction(null); - }, [pendingImportData, runAgentImport, t]); - - const handleForceDuplicateImport = useCallback(async () => { - if (!pendingImportData) { - return; - } - setImportingAction("force"); - const success = await runAgentImport(pendingImportData.agentInfo, { - forceImport: true, - }); - if (success) { - setPendingImportData(null); - } - setImportingAction(null); - }, [pendingImportData, runAgentImport, t]); - - // Handle confirmed deletion - const handleConfirmDelete = async (agent: Agent) => { - try { - const result = await deleteAgent(Number(agent.id)); - if (result.success) { - message.success( - t("businessLogic.config.error.agentDeleteSuccess", { - name: agent.name, - }) - ); - // If currently editing the deleted agent, reset to initial clean state and avoid confirm modal on next switch - const deletedId = Number(agent.id); - const currentEditingId = - (isEditingAgent && editingAgent ? Number(editingAgent.id) : null) ?? - null; - if (currentEditingId === deletedId) { - // Clear editing/creation states - setIsEditingAgent(false); - setEditingAgent(null); - setIsCreatingNewAgent(false); - setMainAgentId(null); - // Clear form/content states - setBusinessLogic(""); - setDutyContent?.(""); - setConstraintContent?.(""); - setFewShotsContent?.(""); - setAgentName?.(""); - setAgentDescription?.(""); - setAgentDisplayName?.(""); - setSelectedTools([]); - setEnabledToolIds([]); - setEnabledAgentIds([]); - // Reset baseline/dirty and bypass next unsaved check - baselineRef.current = null; - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - skipUnsavedCheckRef.current = true; - setTimeout(() => { - skipUnsavedCheckRef.current = false; - }, 0); - onEditingStateChange?.(false, null); - } else { - // If deleting another agent that is in enabledAgentIds, remove it and update baseline - // to avoid triggering false unsaved changes indicator - const deletedId = Number(agent.id); - if (enabledAgentIds.includes(deletedId)) { - const updatedEnabledAgentIds = enabledAgentIds.filter( - (id) => id !== deletedId - ); - setEnabledAgentIds(updatedEnabledAgentIds); - // Update baseline to reflect this change so it doesn't trigger unsaved changes - if (baselineRef.current) { - baselineRef.current = { - ...baselineRef.current, - enabledAgentIds: updatedEnabledAgentIds.sort((a, b) => a - b), - }; - } - } - } - // Refresh agent list without clearing tools to avoid triggering false unsaved changes indicator - refreshAgentList(t, false); - } else { - message.error( - result.message || t("businessLogic.config.error.agentDeleteFailed") - ); - } - } catch (error) { - log.error(t("agentConfig.agents.deleteFailed"), error); - message.error(t("businessLogic.config.error.agentDeleteFailed")); - } - }; - - // Handle export agent from list - const handleExportAgentFromList = async (agent: Agent) => { - try { - const result = await exportAgent(Number(agent.id)); - if (result.success && result.data) { - // Create a blob and download the file - const blob = new Blob([JSON.stringify(result.data, null, 2)], { - type: "application/json", - }); - const url = URL.createObjectURL(blob); - const link = document.createElement("a"); - link.href = url; - link.download = `${agent.name || "agent"}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - message.success(t("businessLogic.config.message.agentExportSuccess")); - } else { - message.error( - result.message || t("businessLogic.config.error.agentExportFailed") - ); - } - } catch (error) { - log.error("Failed to export agent:", error); - message.error(t("businessLogic.config.error.agentExportFailed")); - } - }; - - // Handle copy agent from list - const handleCopyAgentFromList = async (agent: Agent) => { - try { - // Fetch source agent detail before duplicating - const detailResult = await searchAgentInfo(Number(agent.id)); - if (!detailResult.success || !detailResult.data) { - message.error(detailResult.message); - return; - } - const detail = detailResult.data; - - // Prepare copy names - const copyName = `${detail.name || "agent"}_copy`; - const copyDisplayName = `${ - detail.display_name || t("agentConfig.agents.defaultDisplayName") - }${t("agent.copySuffix")}`; - - // Gather tool and sub-agent identifiers from the source agent - const tools = Array.isArray(detail.tools) ? detail.tools : []; - const unavailableTools = tools.filter( - (tool: any) => tool && tool.is_available === false - ); - const unavailableToolNames = unavailableTools - .map( - (tool: any) => - tool?.display_name || tool?.name || tool?.tool_name || "" - ) - .filter((name: string) => Boolean(name)); - - const enabledToolIds = tools - .filter((tool: any) => tool && tool.is_available !== false) - .map((tool: any) => Number(tool.id)) - .filter((id: number) => Number.isFinite(id)); - const subAgentIds = (Array.isArray(detail.sub_agent_id_list) - ? detail.sub_agent_id_list - : [] - ) - .map((id: any) => Number(id)) - .filter((id: number) => Number.isFinite(id)); - - // Create a new agent using the source agent fields - const createResult = await updateAgent( - undefined, - copyName, - detail.description, - detail.model, - detail.max_step, - detail.provide_run_summary, - detail.enabled, - detail.business_description, - detail.duty_prompt, - detail.constraint_prompt, - detail.few_shots_prompt, - copyDisplayName, - detail.model_id ?? undefined, - detail.business_logic_model_name ?? undefined, - detail.business_logic_model_id ?? undefined, - enabledToolIds, - subAgentIds - ); - if (!createResult.success || !createResult.data?.agent_id) { - message.error( - createResult.message || - t("agentConfig.agents.copyFailed") - ); - return; - } - const newAgentId = Number(createResult.data.agent_id); - const copiedAgentFallback: Agent = { - ...detail, - id: String(newAgentId), - name: copyName, - display_name: copyDisplayName, - sub_agent_id_list: subAgentIds, - }; - - // Copy tool configuration to the new agent - for (const tool of tools) { - if (!tool || tool.is_available === false) { - continue; - } - const params = - tool.initParams?.reduce((acc: Record, param: any) => { - acc[param.name] = param.value; - return acc; - }, {}) || {}; - try { - await updateToolConfig(Number(tool.id), newAgentId, params, true); - } catch (error) { - log.error("Failed to copy tool configuration while duplicating agent:", error); - message.error( - t("agentConfig.agents.copyFailed") - ); - return; - } - } - - // Refresh UI state and notify user about copy result - await refreshAgentList(t, false); - message.success(t("agentConfig.agents.copySuccess")); - if (unavailableTools.length > 0) { - const names = - unavailableToolNames.join(", ") || - unavailableTools - .map((tool: any) => Number(tool?.id)) - .filter((id: number) => !Number.isNaN(id)) - .join(", "); - message.warning( - t("agentConfig.agents.copyUnavailableTools", { - count: unavailableTools.length, - names, - }) - ); - } - // Auto select the newly copied agent for editing - await handleEditAgent(copiedAgentFallback, t); - } catch (error) { - log.error("Failed to copy agent:", error); - message.error(t("agentConfig.agents.copyFailed")); - } - }; - - const handleCopyAgentWithConfirm = (agent: Agent) => { - confirm({ - title: t("agentConfig.agents.copyConfirmTitle"), - content: t("agentConfig.agents.copyConfirmContent", { - name: agent?.display_name || agent?.name || "", - }), - onOk: () => handleCopyAgentFromList(agent), - }); - }; - - // Handle delete agent from list - const handleDeleteAgentFromList = (agent: Agent) => { - confirm({ - title: t("businessLogic.config.modal.deleteTitle"), - content: t("businessLogic.config.modal.deleteContent", { - name: agent.name, - }), - onOk: () => handleConfirmDelete(agent), - }); - }; - - // Refresh tool list - const handleToolsRefresh = useCallback( - async (showSuccessMessage = true) => { - if (onToolsRefresh) { - // Before refreshing the tool list, synchronize the selected tools using search_info. - const currentAgentId = (isEditingAgent && editingAgent - ? Number(editingAgent.id) - : mainAgentId - ? Number(mainAgentId) - : undefined) as number | undefined; - if (currentAgentId) { - await refreshAgentToolSelectionsFromServer(currentAgentId); - } - const refreshedTools = await onToolsRefresh(showSuccessMessage); - if (refreshedTools) { - shouldRefreshEnabledToolIds.current = true; - } - return refreshedTools; - } - return undefined; - }, - [onToolsRefresh, isEditingAgent, editingAgent, mainAgentId, refreshAgentToolSelectionsFromServer] - ); - - // Handle view call relationship - const handleViewCallRelationship = () => { - const currentAgentId = getCurrentAgentId?.() ?? undefined; - if (currentAgentId) { - setCallRelationshipModalVisible(true); - } - }; - - // Get button tooltip information - const getLocalButtonTitle = () => { - if (!businessLogic || businessLogic.trim() === "") { - return t("businessLogic.config.message.businessDescriptionRequired"); - } - if (!mainAgentModel) { - return t("businessLogic.config.message.selectModelRequired"); - } - if ( - !dutyContent?.trim() && - !constraintContent?.trim() && - !fewShotsContent?.trim() - ) { - return t("businessLogic.config.message.generatePromptFirst"); - } - if (!agentName || agentName.trim() === "") { - return t("businessLogic.config.message.completeAgentInfo"); - } - return ""; - }; - - // Check if agent can be saved - const localCanSaveAgent = !!( - businessLogic?.trim() && - agentName?.trim() && - mainAgentModel && - (dutyContent?.trim() || - constraintContent?.trim() || - fewShotsContent?.trim()) - ); - - const isForceDuplicateDisabled = - isImporting && importingAction === "regenerate"; - const isRegenerateDuplicateDisabled = - isImporting && importingAction === "force"; - const isForceDuplicateLoading = isImporting && importingAction === "force"; - - return ( - -
- {/* Three-column layout using Ant Design Grid */} - - {/* Left column: SubAgentPool */} - - handleEditAgent(agent, t)} - onCreateNewAgent={() => confirmOrRun(handleCreateNewAgent)} - onExitEditMode={handleExitEditMode} - onImportAgent={() => handleImportAgent(t)} - subAgentList={subAgentList} - loadingAgents={loadingAgents} - isImporting={isImporting} - isGeneratingAgent={isGeneratingAgent} - editingAgent={editingAgent} - isCreatingNewAgent={isCreatingNewAgent} - onCopyAgent={handleCopyAgentWithConfirm} - onExportAgent={handleExportAgentFromList} - onDeleteAgent={handleDeleteAgentFromList} - unsavedAgentId={ - hasUnsavedChanges && isEditingAgent && editingAgent - ? Number(editingAgent.id) - : null - } - /> - - - {/* Middle column: Agent capability configuration */} - - {/* Header: Configure Agent Capabilities */} -
-
-
- 2 -
-

- {t("businessLogic.config.title")} -

-
-
- - {/* Content: Two sections */} -
-
- {/* Upper section: Collaborative Agent Display - fixed area */} - - - {/* Lower section: Tool Pool - flexible area */} -
- { - if (isLoadingTools) return; - const toolId = Number(tool.id); - if (isSelected) { - // Avoid duplicate tools - if (!selectedTools.some((t) => t.id === tool.id)) { - setSelectedTools([...selectedTools, tool]); - } - // Sync enabledToolIds, ensure no duplicates - setEnabledToolIds((prev) => { - if (prev.includes(toolId)) { - return prev; - } - return [...prev, toolId].sort((a, b) => a - b); - }); - } else { - setSelectedTools( - selectedTools.filter((t) => t.id !== tool.id) - ); - // Sync enabledToolIds - setEnabledToolIds((prev) => - prev.filter((id) => id !== toolId) - ); - } - }} - tools={tools} - loadingTools={isLoadingTools} - mainAgentId={ - isEditingAgent && editingAgent - ? editingAgent.id - : mainAgentId - } - localIsGenerating={isGeneratingAgent} - onToolsRefresh={handleToolsRefresh} - isEditingMode={isEditingAgent || isCreatingNewAgent} - isGeneratingAgent={isGeneratingAgent} - isEmbeddingConfigured={isEmbeddingConfigured} - agentUnavailableReasons={agentUnavailableReasons} - onToolConfigSave={handleToolConfigDraftSave} - toolConfigDrafts={toolConfigDrafts} - /> -
-
-
- - - {/* Right column: System Prompt Display */} - - confirmOrRun(() => onDebug()) : () => {}} - agentId={ - getCurrentAgentId - ? getCurrentAgentId() - : isEditingAgent && editingAgent - ? Number(editingAgent.id) - : isCreatingNewAgent && mainAgentId - ? Number(mainAgentId) - : undefined - } - businessLogic={businessLogic} - businessLogicError={businessLogicError} - dutyContent={dutyContent} - constraintContent={constraintContent} - fewShotsContent={fewShotsContent} - onDutyContentChange={setDutyContent} - onConstraintContentChange={setConstraintContent} - onFewShotsContentChange={setFewShotsContent} - agentName={agentName} - agentDescription={agentDescription} - onAgentNameChange={setAgentName} - onAgentDescriptionChange={setAgentDescription} - agentDisplayName={agentDisplayName} - onAgentDisplayNameChange={setAgentDisplayName} - agentAuthor={agentAuthor} - onAgentAuthorChange={setAgentAuthor} - isEditingMode={isEditingAgent || isCreatingNewAgent} - mainAgentModel={mainAgentModel ?? undefined} - mainAgentModelId={mainAgentModelId} - mainAgentMaxStep={mainAgentMaxStep} - onModelChange={(value: string, modelId?: number) => - handleModelChange(value, modelId) - } - onMaxStepChange={handleMaxStepChange} - onBusinessLogicChange={(value: string) => setBusinessLogic(value)} - onBusinessLogicModelChange={handleBusinessLogicModelChange} - businessLogicModel={businessLogicModel} - businessLogicModelId={businessLogicModelId} - onGenerateAgent={onGenerateAgent || (() => {})} - onSaveAgent={handleSaveAgent} - isGeneratingAgent={isGeneratingAgent} - isCreatingNewAgent={isCreatingNewAgent} - canSaveAgent={localCanSaveAgent} - getButtonTitle={getLocalButtonTitle} - editingAgent={editingAgentFromParent || editingAgent} - onViewCallRelationship={handleViewCallRelationship} - /> - -
- - - {/* Save confirmation modal for unsaved changes (debug/navigation hooks) */} - { - if (confirmContext === "exitCreate") { - // Discard draft and return to initial state - await handleCancelCreating(); - setIsSaveConfirmOpen(false); - } else { - // Discard while switching/editing: reload backend state of current agent - await reloadCurrentAgentData(); - setHasUnsavedChanges(false); - onUnsavedChange?.(false); - setIsSaveConfirmOpen(false); - const action = pendingAction; - setPendingAction(null); - if (action) { - skipUnsavedCheckRef.current = true; - setTimeout(async () => { - try { - await Promise.resolve(action()); - } finally { - skipUnsavedCheckRef.current = false; - } - }, 0); - } - } - }} - onSave={async () => { - // Save changes: for create mode or edit mode, reuse unified save path - await saveAllChanges(); - setIsSaveConfirmOpen(false); - const action = pendingAction; - setPendingAction(null); - if (action) { - // Continue pending action after save (e.g., switch) - skipUnsavedCheckRef.current = true; - setTimeout(async () => { - try { - await Promise.resolve(action()); - } finally { - skipUnsavedCheckRef.current = false; - } - }, 0); - } - }} - onClose={() => { - // Only close modal, don't execute discard logic - setIsSaveConfirmOpen(false); - }} - canSave={localCanSaveAgent} - invalidReason={ - localCanSaveAgent ? undefined : getLocalButtonTitle() || undefined - } - /> - {/* Duplicate import confirmation */} - - - {t("businessLogic.config.import.duplicateTitle")} -
- } - onCancel={() => { - if (isImporting) { - return; - } - setPendingImportData(null); - }} - maskClosable={!isImporting} - closable={!isImporting} - centered - footer={ -
- - - - - - - -
- } - > -

- {t("businessLogic.config.import.duplicateDescription")} -

- - {/* Agent Import Wizard */} - { - setImportWizardVisible(false); - setImportWizardData(null); - }} - initialData={importWizardData} - onImportComplete={handleImportComplete} - title={undefined} // Use default title - agentDisplayName={ - importWizardData?.agent_info?.[String(importWizardData.agent_id)]?.display_name - } - agentDescription={ - importWizardData?.agent_info?.[String(importWizardData.agent_id)]?.description - } - /> - {/* Auto unselect knowledge_base_search notice when embedding not configured */} - - {/* Agent call relationship modal */} - setCallRelationshipModalVisible(false)} - agentId={ - (getCurrentAgentId - ? getCurrentAgentId() - : isEditingAgent && editingAgent - ? Number(editingAgent.id) - : isCreatingNewAgent && mainAgentId - ? Number(mainAgentId) - : undefined) ?? 0 - } - agentName={ - editingAgentFromParent || editingAgent - ? (editingAgentFromParent || editingAgent)?.display_name || - (editingAgentFromParent || editingAgent)?.name || - "" - : "" - } - /> - -
- ); -} diff --git a/frontend/app/[locale]/agents/components/PromptManager.tsx b/frontend/app/[locale]/agents/components/PromptManager.tsx deleted file mode 100644 index c7d7e45f0..000000000 --- a/frontend/app/[locale]/agents/components/PromptManager.tsx +++ /dev/null @@ -1,730 +0,0 @@ -"use client"; - -import { useState, useRef, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { Modal, Badge, Input, App, Select } from "antd"; -import { Zap, LoaderCircle, Info } from "lucide-react"; - -import { - SimplePromptEditorProps, - ExpandEditModalProps, -} from "@/types/agentConfig"; -import { updateAgent } from "@/services/agentConfigService"; -import { modelService } from "@/services/modelService"; -import { ModelOption } from "@/types/modelConfig"; - -import AgentConfigModal, { AgentConfigModalProps } from "./agent/AgentConfigModal"; - -import log from "@/lib/logger"; - -export function SimplePromptEditor({ - value, - onChange, - height, - bordered = false, -}: SimplePromptEditorProps) { - const [internalValue, setInternalValue] = useState(value); - const isInternalChange = useRef(false); - const onChangeRef = useRef(onChange); - - // Keep onChange ref updated - useEffect(() => { - onChangeRef.current = onChange; - }, [onChange]); - - // Sync external value changes to internal state (only when not from internal change) - useEffect(() => { - // Only update if the change is from external source (not from user input) - if (!isInternalChange.current) { - setInternalValue(value); - } - }, [value]); // Only depend on value prop - internalValue comparison handled via ref flag - - const handleChange = (e: React.ChangeEvent) => { - const newValue = e.target.value; - isInternalChange.current = true; - setInternalValue(newValue); - // Use ref to avoid stale closure issues - onChangeRef.current(newValue); - // Reset flag after a microtask to allow state update - Promise.resolve().then(() => { - isInternalChange.current = false; - }); - }; - - return ( - - ); -} - -// Expand edit modal - -function ExpandEditModal({ - open, - title, - content, - index, - onClose, - onSave, -}: ExpandEditModalProps) { - const { t } = useTranslation("common"); - const [editContent, setEditContent] = useState(content); - - // Update edit content when content or open state changes - useEffect(() => { - if (open) { - // Always use the latest content when modal opens - setEditContent(content); - } - }, [content, open]); - - const handleSave = () => { - onSave(editContent); - onClose(); - }; - - const handleClose = () => { - // Close without saving changes - onClose(); - }; - - const getBadgeProps = (index: number) => { - switch (index) { - case 1: - return { status: "success" as const }; - case 2: - return { status: "warning" as const }; - case 3: - return { color: "#1677ff" }; - case 4: - return { status: "default" as const }; - default: - return { status: "default" as const }; - } - }; - - const calculateModalHeight = (content: string) => { - const lineCount = content.split("\n").length; - const contentLength = content.length; - const heightByLines = 25 + Math.floor(lineCount / 8) * 5; - const heightByContent = 25 + Math.floor(contentLength / 200) * 3; - const calculatedHeight = Math.max(heightByLines, heightByContent); - return Math.max(25, Math.min(85, calculatedHeight)); - }; - - return ( - -
- - {title} -
- - - } - open={open} - closeIcon={null} - onCancel={handleClose} - footer={null} - width={1000} - styles={{ - body: { padding: "20px" }, - content: { top: 20 }, - }} - > -
- -
- { - setEditContent(newContent); - }} - bordered={true} - height={"100%"} - /> -
-
-
- ); -} - -// Main prompt manager component -export interface PromptManagerProps { - // Basic data - agentId?: number; - businessLogic?: string; - businessLogicError?: boolean; - dutyContent?: string; - constraintContent?: string; - fewShotsContent?: string; - - // Agent information - agentName?: string; - agentDescription?: string; - agentDisplayName?: string; - mainAgentModel?: string; - mainAgentModelId?: number | null; - mainAgentMaxStep?: number; - - // Business Logic Model (independent from main agent model) - businessLogicModel?: string | null; - businessLogicModelId?: number | null; - - // Edit state - isEditingMode?: boolean; - isGeneratingAgent?: boolean; - isCreatingNewAgent?: boolean; - canSaveAgent?: boolean; - - // Callback functions - onBusinessLogicChange?: (content: string) => void; - onBusinessLogicModelChange?: (value: string, modelId?: number) => void; - onDutyContentChange?: (content: string) => void; - onConstraintContentChange?: (content: string) => void; - onFewShotsContentChange?: (content: string) => void; - onAgentNameChange?: (name: string) => void; - onAgentDescriptionChange?: (description: string) => void; - onAgentDisplayNameChange?: (displayName: string) => void; - agentAuthor?: string; - onAgentAuthorChange?: (author: string) => void; - onModelChange?: (value: string, modelId?: number) => void; - onMaxStepChange?: (value: number | null) => void; - onGenerateAgent?: (model: ModelOption) => void; - onSaveAgent?: () => void; - onDebug?: () => void; - getButtonTitle?: () => string; - onViewCallRelationship?: () => void; - - // Agent being edited - editingAgent?: any; - - // Model selection callbacks - onModelSelect?: (model: ModelOption | null) => void; - selectedGenerateModel?: ModelOption | null; -} - -export default function PromptManager({ - agentId, - businessLogic = "", - businessLogicError = false, - dutyContent = "", - constraintContent = "", - fewShotsContent = "", - agentName = "", - agentDescription = "", - agentDisplayName = "", - agentAuthor = "", - onAgentAuthorChange, - mainAgentModel = "", - mainAgentModelId = null, - mainAgentMaxStep = 5, - businessLogicModel = null, - businessLogicModelId = null, - isEditingMode = false, - isGeneratingAgent = false, - isCreatingNewAgent = false, - canSaveAgent = false, - onBusinessLogicChange, - onBusinessLogicModelChange, - onDutyContentChange, - onConstraintContentChange, - onFewShotsContentChange, - onAgentNameChange, - onAgentDescriptionChange, - onAgentDisplayNameChange, - onModelChange, - onMaxStepChange, - onGenerateAgent, - onSaveAgent, - onDebug, - getButtonTitle, - onViewCallRelationship, - editingAgent, - onModelSelect, - selectedGenerateModel, -}: PromptManagerProps) { - const { t } = useTranslation("common"); - const { message } = App.useApp(); - - // Local state for business logic input to enable debouncing - const [localBusinessLogic, setLocalBusinessLogic] = useState(businessLogic); - const businessLogicDebounceTimer = useRef(null); - - // Sync local state when prop changes (from external updates) - useEffect(() => { - setLocalBusinessLogic(businessLogic); - }, [businessLogic]); - - // Cleanup timer on unmount - useEffect(() => { - return () => { - if (businessLogicDebounceTimer.current) { - clearTimeout(businessLogicDebounceTimer.current); - } - }; - }, []); - - // Modal states - const [expandModalOpen, setExpandModalOpen] = useState(false); - const [expandIndex, setExpandIndex] = useState(0); - - // Model selection states - const [availableModels, setAvailableModels] = useState([]); - const [loadingModels, setLoadingModels] = useState(false); - // Fallback internal selection when parent does not control selection - const [internalSelectedModel, setInternalSelectedModel] = useState< - ModelOption | null - >(selectedGenerateModel ?? null); - - // Keep internal state in sync when parent-controlled value changes - useEffect(() => { - if (selectedGenerateModel && selectedGenerateModel?.id !== internalSelectedModel?.id) { - setInternalSelectedModel(selectedGenerateModel); - } - if (!selectedGenerateModel && internalSelectedModel) { - // Parent cleared selection; keep internal unless explicitly needed - } - }, [selectedGenerateModel]); - - // Load available models on component mount - useEffect(() => { - loadAvailableModels(); - }, []); - - const loadAvailableModels = async () => { - setLoadingModels(true); - try { - const models = await modelService.getLLMModels(); - setAvailableModels(models); - } catch (error) { - log.error("Failed to load available models:", error); - message.error(t("businessLogic.config.error.loadModelsFailed")); - } finally { - setLoadingModels(false); - } - }; - - // Ensure a separate Business Logic LLM default selection using global default on creation - // IMPORTANT: Only read from localStorage when creating a NEW agent, not when editing existing agent - useEffect(() => { - if (!isCreatingNewAgent) return; // Only apply to new agents - if (!availableModels || availableModels.length === 0) return; - if (businessLogicModelId) return; // Already set - - try { - const storedModelConfig = localStorage.getItem("model"); - const parsed = storedModelConfig ? JSON.parse(storedModelConfig) : null; - const defaultDisplayName = parsed?.llm?.displayName || ""; - const defaultModelName = parsed?.llm?.modelName || ""; - - let target = null as ModelOption | null; - if (defaultDisplayName) { - target = availableModels.find((m) => m.displayName === defaultDisplayName) || null; - } - if (!target && defaultModelName) { - target = availableModels.find((m) => m.name === defaultModelName) || null; - } - if (!target) { - target = availableModels[0] || null; - } - if (target && onBusinessLogicModelChange) { - onBusinessLogicModelChange(target.displayName, target.id); - } else if (target) { - if (onModelSelect) { - onModelSelect(target); - } else { - setInternalSelectedModel(target); - } - } - } catch (_e) { - // ignore parse errors - } - }, [isCreatingNewAgent, availableModels, businessLogicModelId, onBusinessLogicModelChange, onModelSelect]); - - // When editing an existing agent, load previously selected business logic model - useEffect(() => { - if (isCreatingNewAgent) return; - if (!availableModels || availableModels.length === 0) return; - if (selectedGenerateModel) return; // already set by parent/user - - let target: ModelOption | null = null; - if (businessLogicModelId) { - target = availableModels.find((m) => m.id === businessLogicModelId) || null; - } - if (!target && businessLogicModel) { - target = - availableModels.find((m) => m.displayName === businessLogicModel) || - availableModels.find((m) => m.name === businessLogicModel) || - null; - } - if (target) { - if (onModelSelect) { - onModelSelect(target); - } else { - setInternalSelectedModel(target); - } - } - }, [ - isCreatingNewAgent, - availableModels, - selectedGenerateModel, - businessLogicModelId, - businessLogicModel, - onModelSelect, - ]); - - // Handle model selection for prompt generation - const handleModelSelect = (modelId: number) => { - const model = availableModels.find((m) => m.id === modelId); - if (!model) return; - if (onBusinessLogicModelChange) { - onBusinessLogicModelChange(model.displayName, model.id); - } else if (onModelSelect) { - onModelSelect(model); - } else { - setInternalSelectedModel(model); - } - }; - - // Handle generate button click - const handleGenerateClick = () => { - if (availableModels.length === 0) { - message.warning(t("businessLogic.config.error.noAvailableModels")); - return; - } - - // Check if a model is selected: priority order is businessLogicModelId, selectedGenerateModel, internalSelectedModel - let chosen: ModelOption | null = null; - if (businessLogicModelId) { - chosen = availableModels.find((m) => m.id === businessLogicModelId) || null; - } - if (!chosen && selectedGenerateModel) { - chosen = selectedGenerateModel; - } - if (!chosen && internalSelectedModel) { - chosen = internalSelectedModel; - } - - if (!chosen) { - message.warning(t("businessLogic.config.modelPlaceholder")); - return; - } - if (onGenerateAgent) { - onGenerateAgent(chosen); - } - }; - - // Select options for available models - const modelSelectOptions = availableModels.map((model) => ({ - value: model.id, - label: model.displayName || model.name, - disabled: model.connect_status !== "available", - })); - - // Handle expand edit - const handleExpandCard = (index: number) => { - setExpandIndex(index); - setExpandModalOpen(true); - }; - - // Handle expand edit save - const handleExpandSave = (newContent: string) => { - switch (expandIndex) { - case 2: - onDutyContentChange?.(newContent); - break; - case 3: - onConstraintContentChange?.(newContent); - break; - case 4: - onFewShotsContentChange?.(newContent); - break; - } - }; - - // Handle manual save - const handleSavePrompt = async () => { - // Don't call update API if no agent ID or in create mode (agent not saved yet) - if (!agentId || isCreatingNewAgent) return; - - try { - const result = await updateAgent( - Number(agentId), - agentName, - agentDescription, - mainAgentModel, - mainAgentMaxStep, - false, - undefined, - businessLogic, - dutyContent, - constraintContent, - fewShotsContent, - agentDisplayName, - mainAgentModelId ?? undefined - ); - - if (result.success) { - onDutyContentChange?.(dutyContent); - onConstraintContentChange?.(constraintContent); - onFewShotsContentChange?.(fewShotsContent); - onAgentDisplayNameChange?.(agentDisplayName); - message.success(t("systemPrompt.message.save.success")); - } else { - throw new Error(result.message); - } - } catch (error) { - log.error(t("systemPrompt.message.save.error"), error); - message.error(t("systemPrompt.message.save.error")); - } - }; - - return ( -
- - - {/* Non-editing mode overlay */} - {!isEditingMode && ( -
-
-
- -

- {t("systemPrompt.nonEditing.title")} -

-
-

- {t("systemPrompt.nonEditing.subtitle")} -

-
-
- )} - - {/* Main title */} -
-
-
- 3 -
-

- {t("guide.steps.describeBusinessLogic.title")} -

-
-
- - {/* Main content */} -
- {/* Business logic description section */} -
-
-

- {t("businessLogic.title")} -

-
- -
- {/* Textarea content area */} -
- { - const newValue = e.target.value; - // Update local state immediately for responsive UI - setLocalBusinessLogic(newValue); - // Clear existing timer - if (businessLogicDebounceTimer.current) { - clearTimeout(businessLogicDebounceTimer.current); - } - // Debounce the parent update to reduce re-renders - businessLogicDebounceTimer.current = setTimeout(() => { - onBusinessLogicChange?.(newValue); - }, 150); // 150ms debounce delay - }} - placeholder={t("businessLogic.placeholder")} - className="w-full resize-none text-sm transition-all duration-300" - style={{ - minHeight: "80px", - maxHeight: "160px", - border: "none", - boxShadow: "none", - padding: 0, - background: "transparent", - overflowX: "hidden", - overflowY: "auto", - }} - autoSize={false} - disabled={!isEditingMode || isGeneratingAgent} - /> -
- - {/* Control area */} -
-
- {t("businessLogic.config.model")}: - { - handleAgentDisplayNameChange(e.target.value); - }} - placeholder={t("agent.displayNamePlaceholder")} - size="large" - disabled={!isEditingMode} - status={ - agentDisplayNameError || - ((isCreatingNewAgent || currentDisplayName !== originalDisplayName) && - agentDisplayNameStatus === NAME_CHECK_STATUS.EXISTS_IN_TENANT) || - shouldShowDuplicateDisplayNameReason - ? "error" - : "" - } - /> - {agentDisplayNameError && ( -

{agentDisplayNameError}

- )} - {!agentDisplayNameError && - (isCreatingNewAgent || currentDisplayName !== originalDisplayName) && - agentDisplayNameStatus === NAME_CHECK_STATUS.EXISTS_IN_TENANT && ( -

- {t("agent.error.displayNameExists", { - displayName: agentDisplayName, - })} -

- )} - {!agentDisplayNameError && - agentDisplayNameStatus !== NAME_CHECK_STATUS.EXISTS_IN_TENANT && - shouldShowDuplicateDisplayNameReason && ( -

- {t("agent.error.displayNameExists", { - displayName: agentDisplayName || editingAgent?.display_name || "", - })} -

- )} -
- - {/* Agent Name */} -
- - { - handleAgentNameChange(e.target.value); - }} - placeholder={t("agent.namePlaceholder")} - size="large" - disabled={!isEditingMode} - status={ - agentNameError || - ((isCreatingNewAgent || currentAgentName !== originalAgentName) && - agentNameStatus === NAME_CHECK_STATUS.EXISTS_IN_TENANT) || - shouldShowDuplicateNameReason - ? "error" - : "" - } - /> - {agentNameError && ( -

{agentNameError}

- )} - {!agentNameError && - (isCreatingNewAgent || currentAgentName !== originalAgentName) && - agentNameStatus === NAME_CHECK_STATUS.EXISTS_IN_TENANT && ( -

- {t("agent.error.nameExists", { name: agentName })} -

- )} - {!agentNameError && - agentNameStatus !== NAME_CHECK_STATUS.EXISTS_IN_TENANT && - shouldShowDuplicateNameReason && ( -

- {t("agent.error.nameExists", { - name: agentName || editingAgent?.name || "", - })} -

- )} -
- - {/* Agent Author */} -
- - { - onAgentAuthorChange?.(e.target.value); - }} - placeholder={t("agent.authorPlaceholder")} - size="large" - disabled={!isEditingMode} - /> - {isCreatingNewAgent && !isSpeedMode && !agentAuthor && user?.email && ( -

- {t("agent.author.hint", { defaultValue: "Default: {{email}}", email: user.email })} -

- )} -
- - {/* Model Selection */} -
- - - {shouldShowModelUnavailableReason && ( -

- {t("agent.error.modelUnavailable", { - modelName: effectiveModelName, - })} -

- )} - {llmModels.length === 0 && ( -

- {t("businessLogic.config.error.noAvailableModels")} -

- )} -
- - {/* Max Steps */} -
- - onMaxStepChange?.(value)} - size="large" - disabled={!isEditingMode} - style={{ width: "100%" }} - /> -
- - {/* Agent Description */} -
- - onAgentDescriptionChange?.(e.target.value)} - placeholder={t("agent.descriptionPlaceholder")} - rows={6} - size="large" - disabled={!isEditingMode} - style={{ - minHeight: "150px", - maxHeight: "200px", - boxShadow: "none", - }} - /> -
-
- ); - - const renderDutyContent = () => ( -
-
- { - setLocalDutyContent(value); - // Immediate update to parent component - if (onDutyContentChange) { - onDutyContentChange(value); - } - }} - /> -
-
- ); - - const renderConstraintContent = () => ( -
-
- { - setLocalConstraintContent(value); - // Immediate update to parent component - if (onConstraintContentChange) { - onConstraintContentChange(value); - } - }} - /> -
-
- ); - - const renderFewShotsContent = () => ( -
-
- { - setLocalFewShotsContent(value); - // Immediate update to parent component - if (onFewShotsContentChange) { - onFewShotsContentChange(value); - } - }} - /> -
-
- ); - - return ( -
- {/* Section Title */} -
-
-

- {t("agent.detailContent.title")} -

-
-
- - {/* Segmented Control */} -
-
-
- - - - -
-
-
- - {/* Content area - flexible height */} -
- {/* Floating expand buttons - positioned outside scrollable content */} - {(activeSegment === "duty" || - activeSegment === "constraint" || - activeSegment === "few-shots") && ( -
- - {/* Action Buttons - Fixed at bottom - Only show in editing mode */} - {isEditingMode && ( -
- {/*
*/} -
- {/* Debug Button - Always show in editing mode */} - - - - {/* Save Button - Different logic for new agent vs existing agent */} - {isCreatingNewAgent ? ( - - ) : ( - - )} -
-
- )} - - {/* Generating prompt overlay */} - {isGeneratingAgent && ( -
-
- -
- {t("agent.generating.title")} -
-
- {t("agent.generating.subtitle")} -
-
-
- )} -
- ); -} diff --git a/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx b/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx deleted file mode 100644 index 0e6ed669a..000000000 --- a/frontend/app/[locale]/agents/components/agent/CollaborativeAgentDisplay.tsx +++ /dev/null @@ -1,210 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useTranslation } from "react-i18next"; -import { Tag, App } from "antd"; -import { Plus, X } from "lucide-react"; - -import { CollaborativeAgentDisplayProps } from "@/types/agentConfig"; - -export default function CollaborativeAgentDisplay({ - availableAgents, - selectedAgentIds, - parentAgentId, - onAgentIdsChange, - isEditingMode, - isGeneratingAgent, - className, - style, -}: CollaborativeAgentDisplayProps) { - const { t } = useTranslation("common"); - const { message } = App.useApp(); - const [isDropdownVisible, setIsDropdownVisible] = useState(false); - const [selectedAgentToAdd, setSelectedAgentToAdd] = useState( - null - ); - const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }); - - // Click outside to close dropdown - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as Element; - // Check if the clicked element is inside the dropdown - if (isDropdownVisible && !target.closest(".collaborative-dropdown")) { - setIsDropdownVisible(false); - } - }; - - if (isDropdownVisible) { - document.addEventListener("mousedown", handleClickOutside); - } - - return () => { - document.removeEventListener("mousedown", handleClickOutside); - }; - }, [isDropdownVisible]); - - // Get detailed information of selected agents - const selectedAgents = availableAgents.filter((agent) => - selectedAgentIds.includes(Number(agent.id)) - ); - - // Get selectable agents (excluding already selected and self) - const availableAgentsToSelect = availableAgents.filter( - (agent) => - !selectedAgentIds.includes(Number(agent.id)) && - agent.is_available !== false && - Number(agent.id) !== parentAgentId - ); - - // Handle adding collaborative agent - const handleAddCollaborativeAgent = (agentIdToAdd?: string) => { - const targetAgentId = agentIdToAdd || selectedAgentToAdd; - - if (!targetAgentId) { - message.warning(t("collaborativeAgent.message.selectAgentFirst")); - return; - } - - // Update local state only - will be saved when agent is saved - const newSelectedAgentIds = [...selectedAgentIds, Number(targetAgentId)]; - onAgentIdsChange(newSelectedAgentIds); - setIsDropdownVisible(false); - setSelectedAgentToAdd(null); - }; - - // Handle removing collaborative agent - const handleRemoveCollaborativeAgent = (agentId: number) => { - // Update local state only - will be saved when agent is saved - const newSelectedAgentIds = selectedAgentIds.filter((id) => id !== agentId); - onAgentIdsChange(newSelectedAgentIds); - }; - - // Handle add button click - const handleAddButtonClick = (event: React.MouseEvent) => { - if (!isEditingMode) { - message.warning(t("collaborativeAgent.message.notInEditMode")); - return; - } - if (isGeneratingAgent) { - message.warning(t("collaborativeAgent.message.generatingInProgress")); - return; - } - - if (!isDropdownVisible) { - // Calculate dropdown position - const rect = event.currentTarget.getBoundingClientRect(); - setDropdownPosition({ - top: rect.bottom + window.scrollY + 4, - left: rect.left + window.scrollX, - }); - } - - setIsDropdownVisible(!isDropdownVisible); - }; - - // Render dropdown component - const renderDropdown = () => { - if (!isDropdownVisible) return null; - - return ( -
- {availableAgentsToSelect.length === 0 ? ( -
- {t("collaborativeAgent.select.noOptions")} -
- ) : ( -
- {availableAgentsToSelect.map((agent) => ( -
{ - handleAddCollaborativeAgent(agent.id); - }} - > - {agent.display_name || agent.name} - {agent.display_name && ( - - ({agent.name}) - - )} -
- ))} -
- )} -
- ); - }; - - return ( -
-
-

- {t("collaborativeAgent.title")} -

-
- - {/* Tag display area - fixed height to avoid layout jumping */} -
-
- {/* Add button always exists, just invisible in non-editing mode */} -
- - {/* Dropdown only renders in editing mode */} - {isEditingMode && renderDropdown()} -
- {selectedAgents.map((agent) => ( - handleRemoveCollaborativeAgent(Number(agent.id))} - closeIcon={} - style={{ - maxWidth: "200px", - }} - > - - {agent.display_name || agent.name} - - - ))} -
-
-
- ); -} - diff --git a/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx b/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx deleted file mode 100644 index 0f8bc8b9e..000000000 --- a/frontend/app/[locale]/agents/components/agent/SubAgentPool.tsx +++ /dev/null @@ -1,454 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useTranslation } from "react-i18next"; - -import { Button, Row, Col } from "antd"; -import { - AlertCircle, - Copy, - FileOutput, - Network, - FileInput, - Trash2, - Plus, - X, -} from "lucide-react"; - -import { ScrollArea } from "@/components/ui/scrollArea"; -import { Tooltip, TooltipProvider } from "@/components/ui/tooltip"; -import { Agent, SubAgentPoolProps } from "@/types/agentConfig"; - -import AgentCallRelationshipModal from "@/components/ui/AgentCallRelationshipModal"; - -/** - * Sub Agent Pool Component - */ -type ExtendedSubAgentPoolProps = SubAgentPoolProps & { - /** Agent id that currently has unsaved changes to show blue indicator */ - unsavedAgentId?: number | null; -}; - -export default function SubAgentPool({ - onEditAgent, - onCreateNewAgent, - onImportAgent, - onExitEditMode, - subAgentList = [], - loadingAgents = false, - isImporting = false, - isGeneratingAgent = false, - editingAgent = null, - isCreatingNewAgent = false, - onCopyAgent, - onExportAgent, - onDeleteAgent, - unsavedAgentId = null, -}: ExtendedSubAgentPoolProps) { - const { t } = useTranslation("common"); - - // Call relationship related state - const [callRelationshipModalVisible, setCallRelationshipModalVisible] = - useState(false); - const [selectedAgentForRelationship, setSelectedAgentForRelationship] = - useState(null); - - // Open call relationship modal - const handleViewCallRelationship = (agent: Agent) => { - setSelectedAgentForRelationship(agent); - setCallRelationshipModalVisible(true); - }; - - // Close call relationship modal - const handleCloseCallRelationshipModal = () => { - setCallRelationshipModalVisible(false); - setSelectedAgentForRelationship(null); - }; - - return ( - - -
-
-
-
- 1 -
-

- {t("subAgentPool.management")} -

-
-
- {loadingAgents && ( - - {t("subAgentPool.loading")} - - )} -
-
- -
- {/* Function operation block */} -
- - - -
{ - if (isCreatingNewAgent) { - // If currently in creation mode, click to exit creation mode - onExitEditMode?.(); - } else { - // Otherwise enter creation mode - onCreateNewAgent(); - } - }} - > -
-
- {/* Smoothly cross-fade and scale between Plus and X */} -
-
-
- {isCreatingNewAgent - ? t("subAgentPool.button.exitCreate") - : t("subAgentPool.button.create")} -
-
- {isCreatingNewAgent - ? t("subAgentPool.description.exitCreate") - : t("subAgentPool.description.createAgent")} -
-
-
-
-
- - - - -
-
-
- -
-
-
- {isImporting - ? t("subAgentPool.button.importing") - : t("subAgentPool.button.import")} -
-
- {isImporting - ? t("subAgentPool.description.importing") - : t("subAgentPool.description.importAgent")} -
-
-
-
-
- -
-
- - {/* Agent list block */} -
-
- {t("subAgentPool.section.agentList")} ({subAgentList.length}) -
-
- {subAgentList.map((agent) => { - const isAvailable = agent.is_available !== false; // Default is true, only unavailable when explicitly false - const isCurrentlyEditing = - editingAgent && - String(editingAgent.id) === String(agent.id); // Ensure type matching - const displayName = agent.display_name || agent.name; - - const agentItem = ( -
{ - // Prevent event bubbling - e.preventDefault(); - e.stopPropagation(); - - if (!isGeneratingAgent) { - // Allow all unavailable agents to enter edit mode for configuration - if (isCurrentlyEditing) { - // If currently editing this Agent, click to exit edit mode - onExitEditMode?.(); - } else { - // Enter edit mode (all agents can be edited) - onEditAgent(agent); - } - } - }} - > -
-
-
-
- {!isAvailable && ( - - )} - {displayName && ( - - {displayName} - - )} - {unsavedAgentId !== null && - String(unsavedAgentId) === String(agent.id) && ( - - )} -
-
-
- {agent.description} -
-
- - {/* Operation button area */} -
- {/* Copy agent button */} - {onCopyAgent && ( - -
-
-
- ); - - return
{agentItem}
; - })} -
-
-
-
- - {/* Agent call relationship modal */} - {selectedAgentForRelationship && ( - - )} -
-
- ); -} diff --git a/frontend/app/[locale]/agents/components/agentConfig/CollaborativeAgent.tsx b/frontend/app/[locale]/agents/components/agentConfig/CollaborativeAgent.tsx new file mode 100644 index 000000000..b39da4580 --- /dev/null +++ b/frontend/app/[locale]/agents/components/agentConfig/CollaborativeAgent.tsx @@ -0,0 +1,136 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { Tag, App, Card, Flex, Dropdown, Space, Col } from "antd"; +import { Plus, X } from "lucide-react"; +import { Agent } from "@/types/agentConfig"; +import { useAgentConfigStore } from "@/stores/agentConfigStore"; +import { useAgentList } from "@/hooks/agent/useAgentList"; +import { useAgentInfo } from "@/hooks/agent/useAgentInfo"; + +interface CollaborativeAgentProps {} + +export default function CollaborativeAgent({}: CollaborativeAgentProps) { + const { t } = useTranslation("common"); + const { message } = App.useApp(); + + const currentAgentId = useAgentConfigStore((state) => state.currentAgentId); + const isCreatingMode = useAgentConfigStore((state) => state.isCreatingMode); + const editedAgent = useAgentConfigStore((state) => state.editedAgent); + const updateSubAgentIds = useAgentConfigStore( + (state) => state.updateSubAgentIds + ); + + const { availableAgents } = useAgentList(); + + const editable = currentAgentId || isCreatingMode; + + // Get related agents - use edited agent state (which includes current agent data when editing) + const relatedAgentIds = Array.isArray(editedAgent?.sub_agent_id_list) + ? editedAgent.sub_agent_id_list + : []; + + const relatedAgents = ( + Array.isArray(availableAgents) ? availableAgents : [] + ).filter((agent: Agent) => relatedAgentIds.includes(Number(agent.id))); + + // Filter available agents (exclude already related ones and current agent) + const availableAgentsForMenu = ( + Array.isArray(availableAgents) ? availableAgents : [] + ).filter( + (agent: Agent) => + !relatedAgentIds.includes(Number(agent.id)) && + Number(agent.id) !== currentAgentId + ); + + const handleAddAgent = (agentId: number) => { + const newRelatedAgentIds = [ + ...(Array.isArray(relatedAgentIds) ? relatedAgentIds : []), + agentId, + ]; + updateSubAgentIds(newRelatedAgentIds); + }; + + const handleRemoveAgent = (agentId: number) => { + const newRelatedAgentIds = ( + Array.isArray(relatedAgentIds) ? relatedAgentIds : [] + ).filter((id: number) => id !== agentId); + updateSubAgentIds(newRelatedAgentIds); + }; + + const addRelatedAgent = (event: React.MouseEvent) => {}; + + const menuItems = Array.isArray(availableAgentsForMenu) + ? availableAgentsForMenu.map((agent: Agent) => ({ + key: String(agent.id), + label: ( + <> + {agent.display_name || agent.name} + {agent.display_name && ( + ({agent.name}) + )} + + ), + onClick: () => handleAddAgent(Number(agent.id)), + })) + : []; + + return ( + <> + +

+ {t("collaborativeAgent.title")} +

+ + + + + + + + +
+ + {relatedAgents.map((agent: Agent) => ( + handleRemoveAgent(Number(agent.id))} + className="bg-blue-50 text-blue-700 border-blue-200 truncate" + style={{ + maxWidth: "200px", + }} + > + {agent.display_name || agent.name} + + ))} + +
+
+
+
+ + + ); +} diff --git a/frontend/app/[locale]/agents/components/McpConfigModal.tsx b/frontend/app/[locale]/agents/components/agentConfig/McpConfigModal.tsx similarity index 97% rename from frontend/app/[locale]/agents/components/McpConfigModal.tsx rename to frontend/app/[locale]/agents/components/agentConfig/McpConfigModal.tsx index 724e2d4de..461c50c91 100644 --- a/frontend/app/[locale]/agents/components/McpConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/McpConfigModal.tsx @@ -648,7 +648,7 @@ export default function McpConfigModal({ {t("mcpConfig.addServer.title")} - +
- +
{t("mcpConfig.addContainer.configHint")} @@ -714,24 +714,19 @@ export default function McpConfigModal({ style={{ fontFamily: "monospace", fontSize: 12 }} />
-
- {t("mcpConfig.addContainer.port")}: +
+ {t("mcpConfig.addContainer.port")}: { - const value = e.target.value; - if (value === "") { - setContainerPort(undefined); - return; - } - const port = parseInt(value); - if (!isNaN(port) && port >= 1 && port <= 65535) { - setContainerPort(port); - } - // If invalid input, keep the previous valid value + const port = parseInt(e.target.value); + setContainerPort(isNaN(port) ? undefined : port); }} - style={{ width: 200 }} + min={1} + max={65535} + style={{ width: 150 }} disabled={actionsLocked} />
diff --git a/frontend/app/[locale]/agents/components/agentConfig/ToolManagement.tsx b/frontend/app/[locale]/agents/components/agentConfig/ToolManagement.tsx new file mode 100644 index 000000000..774a18a05 --- /dev/null +++ b/frontend/app/[locale]/agents/components/agentConfig/ToolManagement.tsx @@ -0,0 +1,368 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import ToolConfigModal from "./tool/ToolConfigModal"; +import { ToolGroup, Tool, ToolParam } from "@/types/agentConfig"; +import { Tabs, Collapse } from "antd"; +import { useAgentConfigStore } from "@/stores/agentConfigStore"; +import { useToolList } from "@/hooks/agent/useToolList"; +import { updateToolConfig } from "@/services/agentConfigService"; +import { useToolInfo } from "@/hooks/tool/useToolInfo"; +import { message } from "antd"; +import { useQueryClient } from "@tanstack/react-query"; + +import { Settings } from "lucide-react"; + +interface ToolManagementProps { + toolGroups: ToolGroup[]; + editable?: boolean; + currentAgentId?: number | undefined; +} + +/** + * ToolManagement - Component for displaying tools in tabs + * Provides a tabbed interface for tool organization + */ +export default function ToolManagement({ + toolGroups, + editable = true, + currentAgentId, +}: ToolManagementProps) { + const { t } = useTranslation("common"); + const queryClient = useQueryClient(); + + // Get state from store + const usedTools = useAgentConfigStore((state) => state.editedAgent.tools); + const updateTools = useAgentConfigStore((state) => state.updateTools); + + // Use tool list hook for data management + const { availableTools } = useToolList(); + + const [activeTabKey, setActiveTabKey] = useState(""); + const [expandedCategories, setExpandedCategories] = useState>( + new Set() + ); + const [isToolModalOpen, setIsToolModalOpen] = useState(false); + const [isClickSetting, setIsClickSetting] = useState(false); + const [selectedTool, setSelectedTool] = useState(null); + const [toolParams, setToolParams] = useState([]); + + // Get tool info for selected tool (when checking if config is needed) + const { data: selectedToolInfo } = useToolInfo( + (selectedTool) ? parseInt(selectedTool.id) : null, + currentAgentId ?? null + ); + + // Effect to handle tool selection when tool info is loaded + useEffect(() => { + if (selectedTool && selectedToolInfo) { + // Use instance params if available, otherwise use default params + const mergedParams = selectedTool.initParams?.map((param: ToolParam) => { + const instanceValue = selectedToolInfo?.params?.[param.name]; + return { + ...param, + value: instanceValue !== undefined ? instanceValue : param.value, + }; + }) || []; + setToolParams(mergedParams); + + const hasEmptyRequiredParams = mergedParams.some( + (param: ToolParam) => param.required && + (param.value === undefined || param.value === '' || param.value === null) + ); + if (isClickSetting || hasEmptyRequiredParams) { + // Open modal for configuration with pre-calculated params + setIsToolModalOpen(true); + setIsClickSetting(false) + } else { + // Add tool directly + const newSelectedTools = [...usedTools, { + ...selectedTool, + initParams: mergedParams + }]; + updateTools(newSelectedTools); + setSelectedTool(null); // Clear selected tool + setIsClickSetting(false) + } + + } + }, [selectedTool, selectedToolInfo]); + + // Create selected tool ID set for efficient lookup + const selectedToolIdsSet = new Set( + usedTools.map((tool) => tool.id) + ); + + // Set default active tab + useEffect(() => { + if (toolGroups.length > 0 && !activeTabKey) { + setActiveTabKey(toolGroups[0].key); + } + }, [toolGroups, activeTabKey]); + + const handleToolModalCancel = () => { + setIsToolModalOpen(false); + setSelectedTool(null); + setToolParams([]); + setIsClickSetting(false) + }; + + const handleToolModalSave = async (params: ToolParam[]) => { + if (!selectedTool || !currentAgentId) return; + + try { + // Convert params to backend format + const paramsObj = params.reduce((acc, param) => { + acc[param.name] = param.value; + return acc; + }, {} as Record); + + // Save tool config + const isEnabled = true; // New tool is enabled by default + const result = await updateToolConfig( + parseInt(selectedTool.id), + currentAgentId, + paramsObj, + isEnabled + ); + + if (result.success) { + // Add tool to selected tools with updated params + const updatedTool = { ...selectedTool, initParams: params }; + const newSelectedTools = [...usedTools, updatedTool]; + updateTools(newSelectedTools); + + message.success(t("toolConfig.message.saveSuccess")); + + queryClient.invalidateQueries({ + queryKey: ["toolInfo", parseInt(selectedTool.id), currentAgentId] + }); + + setIsToolModalOpen(false); + setSelectedTool(null); + setToolParams([]); + setIsClickSetting(false) + } else { + message.error(result.message || t("toolConfig.message.saveError")); + } + } catch (error) { + message.error(t("toolConfig.message.saveError")); + } + }; + + const handleToolSettingsClick = (tool: Tool) => { + setIsClickSetting(true) + setSelectedTool(tool); + }; + + const handleToolSelect = (toolId: number) => { + // Find the tool from available tools + const tool = availableTools.find((t) => parseInt(t.id) === toolId); + if (!tool) return; + + const isCurrentlySelected = usedTools.some( + (t) => parseInt(t.id) === toolId + ); + + if (isCurrentlySelected) { + const newSelectedTools = usedTools.filter((t) => parseInt(t.id) !== toolId); + updateTools(newSelectedTools); + } else { + setSelectedTool(tool); + } + } + + const handleToolClick = (toolId: string) => { + const numericId = parseInt(toolId, 10); + handleToolSelect(numericId); + }; + + // Generate Tabs configuration + const tabItems = toolGroups.map((group) => { + // Limit tab display to maximum 7 characters + const displayLabel = + t(group.label).length > 7 + ? `${t(group.label).substring(0, 7)}...` + : t(group.label); + + return { + key: group.key, + label: ( + + {displayLabel} + + ), + children: ( +
+ {group.subGroups ? ( + <> + {/* Collapsible categories using Ant Design Collapse */} +
+ { + const newSet = new Set( + typeof keys === "string" ? [keys] : keys + ); + setExpandedCategories(newSet); + }} + ghost + size="small" + className="tool-categories-collapse mt-1" + items={group.subGroups.map((subGroup, index) => ({ + key: subGroup.key, + label: ( + + {subGroup.label} + + ), + className: `tool-category-panel ${ + index === 0 ? "mt-1" : "mt-3" + }`, + children: ( +
+ {subGroup.tools.map((tool) => { + const isSelected = selectedToolIdsSet.has(tool.id); + return ( +
handleToolClick(tool.id) + : undefined + } + > + {tool.name} + { + e.stopPropagation(); + handleToolSettingsClick(tool); + } + : undefined + } + /> +
+ ); + })} +
+ ), + }))} + /> +
+ + ) : ( + // Regular layout for non-local tools +
+ {group.tools.map((tool) => { + const isSelected = selectedToolIdsSet.has(tool.id); + return ( +
handleToolClick(tool.id) : undefined + } + > + {tool.name} + { + e.stopPropagation(); + handleToolSettingsClick(tool); + } + : undefined + } + /> +
+ ); + })} +
+ )} +
+ ), + }; + }); + + return ( +
+ {toolGroups.length === 0 ? ( +
+ {t("toolPool.noTools")} +
+ ) : ( + + )} + + +
+ ); +} diff --git a/frontend/app/[locale]/agents/components/tool/ToolConfigModal.tsx b/frontend/app/[locale]/agents/components/agentConfig/tool/ToolConfigModal.tsx similarity index 70% rename from frontend/app/[locale]/agents/components/tool/ToolConfigModal.tsx rename to frontend/app/[locale]/agents/components/agentConfig/tool/ToolConfigModal.tsx index 81e45a6d9..b4ab08266 100644 --- a/frontend/app/[locale]/agents/components/tool/ToolConfigModal.tsx +++ b/frontend/app/[locale]/agents/components/agentConfig/tool/ToolConfigModal.tsx @@ -13,24 +13,23 @@ import { } from "antd"; import { TOOL_PARAM_TYPES } from "@/const/agentConfig"; -import { ToolParam, ToolConfigModalProps } from "@/types/agentConfig"; -import { - updateToolConfig, - searchToolConfig, - loadLastToolConfig, -} from "@/services/agentConfigService"; -import log from "@/lib/logger"; +import { ToolParam, Tool } from "@/types/agentConfig"; import { useModalPosition } from "@/hooks/useModalPosition"; import ToolTestPanel from "./ToolTestPanel"; +export interface ToolConfigModalProps { + isOpen: boolean; + onCancel: () => void; + onSave: (params: ToolParam[]) => void; // 修改:返回参数数组 + tool?: Tool; + initialParams: ToolParam[]; // 修改:变为必需,移除currentAgentId +} export default function ToolConfigModal({ isOpen, onCancel, onSave, tool, - mainAgentId, - selectedTools = [], - isEditingMode = true, + initialParams, }: ToolConfigModalProps) { const [currentParams, setCurrentParams] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -42,13 +41,6 @@ export default function ToolConfigModal({ const { windowWidth, mainModalTop, mainModalRight } = useModalPosition(isOpen); - const normalizedAgentId = - typeof mainAgentId === "number" && !Number.isNaN(mainAgentId) - ? mainAgentId - : null; - const canPersistToolConfig = - typeof normalizedAgentId === "number" && normalizedAgentId > 0; - // Apply transform to modal when test panel is visible // Move main modal to the left to center both panels together useEffect(() => { @@ -91,65 +83,15 @@ export default function ToolConfigModal({ }; }, [testPanelVisible, isOpen, windowWidth]); - // load tool config + // Initialize with provided params useEffect(() => { - const buildDefaultParams = () => - (tool?.initParams || []).map((param) => ({ - ...param, - value: param.value, - })); - - const loadToolConfig = async () => { - if (!tool) { - setCurrentParams([]); - return; - } - - // In creation mode (no agent ID), always use backend-provided default params - if (!normalizedAgentId) { - setCurrentParams(buildDefaultParams()); - return; - } - - setIsLoading(true); - try { - // In edit mode, load tool config for the specific agent - const result = await searchToolConfig( - parseInt(tool.id), - normalizedAgentId - ); - if (result.success) { - if (result.data?.params) { - const savedParams = tool.initParams.map((param) => { - const savedValue = result.data.params[param.name]; - return { - ...param, - value: savedValue !== undefined ? savedValue : param.value, - }; - }); - setCurrentParams(savedParams); - } else { - setCurrentParams(buildDefaultParams()); - } - } else { - message.error(result.message || t("toolConfig.message.loadError")); - setCurrentParams(buildDefaultParams()); - } - } catch (error) { - log.error(t("toolConfig.message.loadError"), error); - message.error(t("toolConfig.message.loadErrorUseDefault")); - setCurrentParams(buildDefaultParams()); - } finally { - setIsLoading(false); - } - }; - - if (isOpen && tool) { - loadToolConfig(); + if (isOpen && tool && initialParams) { + setCurrentParams(initialParams); + setIsLoading(false); } else { setCurrentParams([]); } - }, [isOpen, tool, normalizedAgentId, t, message]); + }, [tool, initialParams, isOpen]); // check required fields const checkRequiredFields = () => { @@ -182,78 +124,10 @@ export default function ToolConfigModal({ setCurrentParams(newParams); }; - // load last tool config - const handleLoadLastConfig = async () => { - if (!tool) return; - - try { - const result = await loadLastToolConfig(parseInt(tool.id)); - if (result.success && result.data) { - // Parse the last config data - const lastConfig = result.data; - - // Update current params with last config values - const updatedParams = currentParams.map((param) => { - const lastValue = lastConfig[param.name]; - return { - ...param, - value: lastValue !== undefined ? lastValue : param.value, - }; - }); - - setCurrentParams(updatedParams); - message.success(t("toolConfig.message.loadLastConfigSuccess")); - } else { - message.warning(t("toolConfig.message.loadLastConfigNotFound")); - } - } catch (error) { - log.error(t("toolConfig.message.loadLastConfigFailed"), error); - message.error(t("toolConfig.message.loadLastConfigFailed")); - } - }; - - const handleSave = async () => { - if (!tool || !checkRequiredFields()) return; - - try { - // convert params to backend format - const params = currentParams.reduce((acc, param) => { - acc[param.name] = param.value; - return acc; - }, {} as Record); - - if (!canPersistToolConfig) { - message.success(t("toolConfig.message.saveSuccess")); - onSave({ - ...tool, - initParams: currentParams, - }); - return; - } - - // decide enabled status based on whether the tool is in selectedTools - const isEnabled = selectedTools.some((t) => t.id === tool.id); - const result = await updateToolConfig( - parseInt(tool.id), - normalizedAgentId, - params, - isEnabled - ); - - if (result.success) { - message.success(t("toolConfig.message.saveSuccess")); - onSave({ - ...tool, - initParams: currentParams, - }); - } else { - message.error(result.message || t("toolConfig.message.saveError")); - } - } catch (error) { - log.error(t("toolConfig.message.saveFailed"), error); - message.error(t("toolConfig.message.saveFailed")); - } + const handleSave = () => { + if (!checkRequiredFields()) return; + onSave(currentParams); }; // Handle tool testing - open test panel @@ -372,12 +246,6 @@ export default function ToolConfigModal({
{`${tool?.name}`}
- - {isEditingMode && ( + {(