diff --git a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx index 40b42fc3..1b1484cc 100644 --- a/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx +++ b/frontend/app/[locale]/agents/components/AgentSetupOrchestrator.tsx @@ -11,13 +11,13 @@ import { TooltipProvider } from "@/components/ui/tooltip"; import { fetchAgentList, updateAgent, - importAgent, deleteAgent, exportAgent, searchAgentInfo, searchToolConfig, updateToolConfig, } from "@/services/agentConfigService"; +import { useAgentImport } from "@/hooks/useAgentImport"; import { Agent, AgentSetupOrchestratorProps, @@ -1541,6 +1541,31 @@ export default function AgentSetupOrchestrator({ } }; + // 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, @@ -1549,31 +1574,19 @@ export default function AgentSetupOrchestrator({ ) => { setIsImporting(true); try { - const result = await importAgent(agentPayload, options); - - if (result.success) { - message.success( - translationFn("businessLogic.config.error.agentImportSuccess") - ); - // Don't clear tools when importing to avoid triggering false unsaved changes indicator - await refreshAgentList(translationFn, false); - return true; + if (options?.forceImport) { + await runForceImport(agentPayload); + } else { + await runNormalImport(agentPayload); } - - message.error( - result.message || - translationFn("businessLogic.config.error.agentImportFailed") - ); - return false; + return true; } catch (error) { - log.error(translationFn("agentConfig.agents.importFailed"), error); - message.error(translationFn("businessLogic.config.error.agentImportFailed")); return false; } finally { setIsImporting(false); } }, - [message, refreshAgentList] + [runNormalImport, runForceImport] ); // Handle importing agent diff --git a/frontend/app/[locale]/market/MarketContent.tsx b/frontend/app/[locale]/market/MarketContent.tsx index 9af7a9aa..984b0f73 100644 --- a/frontend/app/[locale]/market/MarketContent.tsx +++ b/frontend/app/[locale]/market/MarketContent.tsx @@ -15,10 +15,11 @@ import { MarketAgentListParams, MarketAgentDetail, } from "@/types/market"; -import marketService from "@/services/marketService"; +import marketService, { MarketApiError } from "@/services/marketService"; import { AgentMarketCard } from "./components/AgentMarketCard"; -import { importAgent } from "@/services/agentConfigService"; import MarketAgentDetailModal from "./components/MarketAgentDetailModal"; +import AgentInstallModal from "./components/AgentInstallModal"; +import MarketErrorState from "./components/MarketErrorState"; interface MarketContentProps { /** Connection status */ @@ -66,7 +67,9 @@ export default function MarketContent({ const [currentPage, setCurrentPage] = useState(1); const [pageSize] = useState(20); const [totalAgents, setTotalAgents] = useState(0); - const [error, setError] = useState(null); + const [errorType, setErrorType] = useState< + "timeout" | "network" | "server" | "unknown" | null + >(null); // Detail modal state const [detailModalVisible, setDetailModalVisible] = useState(false); @@ -75,12 +78,19 @@ export default function MarketContent({ ); const [isLoadingDetail, setIsLoadingDetail] = useState(false); - // Load categories on mount + // Install modal state + const [installModalVisible, setInstallModalVisible] = useState(false); + const [installAgent, setInstallAgent] = useState( + null + ); + + // Load categories and initial agents on mount useEffect(() => { loadCategories(); + loadAgents(); // Auto-refresh on page load }, []); - // Load agents when category or page changes + // Load agents when category, page, or search changes (but not on initial mount) useEffect(() => { loadAgents(); }, [currentCategory, currentPage, searchKeyword]); @@ -90,13 +100,18 @@ export default function MarketContent({ */ const loadCategories = async () => { setIsLoadingCategories(true); - setError(null); + setErrorType(null); try { const data = await marketService.fetchMarketCategories(); setCategories(data); } catch (error) { log.error("Failed to load market categories:", error); - setError(t("market.error.loadCategories", "Failed to load categories")); + + if (error instanceof MarketApiError) { + setErrorType(error.type); + } else { + setErrorType("unknown"); + } } finally { setIsLoadingCategories(false); } @@ -107,7 +122,7 @@ export default function MarketContent({ */ const loadAgents = async () => { setIsLoadingAgents(true); - setError(null); + setErrorType(null); try { const params: MarketAgentListParams = { page: currentPage, @@ -127,7 +142,13 @@ export default function MarketContent({ setTotalAgents(data.pagination.total); } catch (error) { log.error("Failed to load market agents:", error); - setError(t("market.error.loadAgents", "Failed to load agents")); + + if (error instanceof MarketApiError) { + setErrorType(error.type); + } else { + setErrorType("unknown"); + } + setAgents([]); setTotalAgents(0); } finally { @@ -189,49 +210,47 @@ export default function MarketContent({ }; /** - * Handle agent download + * Handle agent download - Opens install wizard */ const handleDownload = async (agent: MarketAgentListItem) => { try { - message.loading({ - content: t("market.downloading", "Downloading agent..."), - key: "download", - duration: 0, - }); - - // Fetch full agent details including agent_json + setIsLoadingDetail(true); + // Fetch full agent details for installation const agentDetail = await marketService.fetchMarketAgentDetail( agent.agent_id ); - - // Import the agent using the agent_json - const result = await importAgent(agentDetail.agent_json); - - if (result.success) { - message.success({ - content: t( - "market.downloadSuccess", - "Agent downloaded successfully!" - ), - key: "download", - }); - } else { - message.error({ - content: - result.message || - t("market.downloadFailed", "Failed to download agent"), - key: "download", - }); - } + setInstallAgent(agentDetail); + setInstallModalVisible(true); } catch (error) { - log.error("Failed to download agent:", error); - message.error({ - content: t("market.downloadFailed", "Failed to download agent"), - key: "download", - }); + log.error("Failed to load agent details for installation:", error); + message.error( + t("market.error.fetchDetailFailed", "Failed to load agent details") + ); + } finally { + setIsLoadingDetail(false); } }; + /** + * Handle install complete + */ + const handleInstallComplete = () => { + setInstallModalVisible(false); + setInstallAgent(null); + // Optionally reload agents or show success message + message.success( + t("market.install.success", "Agent installed successfully!") + ); + }; + + /** + * Handle install cancel + */ + const handleInstallCancel = () => { + setInstallModalVisible(false); + setInstallAgent(null); + }; + /** * Render tab items */ @@ -308,124 +327,118 @@ export default function MarketContent({ - {/* Error message */} - {error && ( - - - - {error} - - - )} - - {/* Search bar */} - - } - value={searchKeyword} - onChange={(e) => handleSearch(e.target.value)} - allowClear - className="max-w-md" - /> - - - {/* Category tabs */} - - {isLoadingCategories ? ( -
- -
- ) : ( - - )} -
- - {/* Agents grid */} - - {isLoadingAgents ? ( -
- -
- ) : agents.length === 0 ? ( - + {/* Search bar */} + + } + value={searchKeyword} + onChange={(e) => handleSearch(e.target.value)} + allowClear + className="max-w-md" + /> + + {/* Category tabs */} + + {isLoadingCategories ? ( +
+ +
+ ) : ( + )} - className="py-16" - /> - ) : ( - <> -
- {agents.map((agent, index) => ( - - - - ))} -
+
- {/* Pagination */} - {totalAgents > pageSize && ( -
- - t("market.totalAgents", { - defaultValue: "Total {{total}} agents", - total, - }) - } - /> + {/* Agents grid */} + + {isLoadingAgents ? ( +
+
+ ) : agents.length === 0 ? ( + + ) : ( + <> +
+ {agents.map((agent, index) => ( + + + + ))} +
+ + {/* Pagination */} + {totalAgents > pageSize && ( +
+ + t("market.totalAgents", { + defaultValue: "Total {{total}} agents", + total, + }) + } + /> +
+ )} + )} - - )} -
+ + + ) : ( + /* Error state - only show when there's an error */ + !isLoadingAgents && + !isLoadingCategories && + )}
@@ -436,6 +449,14 @@ export default function MarketContent({ agentDetails={selectedAgent} loading={isLoadingDetail} /> + + {/* Agent Install Modal */} +
) : null} diff --git a/frontend/app/[locale]/market/components/AgentInstallModal.tsx b/frontend/app/[locale]/market/components/AgentInstallModal.tsx new file mode 100644 index 00000000..b0aafacc --- /dev/null +++ b/frontend/app/[locale]/market/components/AgentInstallModal.tsx @@ -0,0 +1,649 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Modal, Steps, Button, Select, Input, Form, Descriptions, Tag, Space, Spin, App } from "antd"; +import { CheckCircleOutlined, DownloadOutlined, CloseCircleOutlined, PlusOutlined } from "@ant-design/icons"; +import { useTranslation } from "react-i18next"; +import { MarketAgentDetail } from "@/types/market"; +import { ModelOption } from "@/types/modelConfig"; +import { modelService } from "@/services/modelService"; +import { getMcpServerList, addMcpServer } from "@/services/mcpService"; +import { McpServer } from "@/types/agentConfig"; +import { useAgentImport } from "@/hooks/useAgentImport"; +import log from "@/lib/logger"; + +interface AgentInstallModalProps { + visible: boolean; + onCancel: () => void; + agentDetails: MarketAgentDetail | null; + onInstallComplete?: () => void; +} + +interface ConfigField { + fieldPath: string; // e.g., "duty_prompt", "tools[0].params.api_key" + fieldLabel: string; // User-friendly label + promptHint?: string; // Hint from + currentValue: string; +} + +interface McpServerToInstall { + mcp_server_name: string; + mcp_url: string; + isInstalled: boolean; + isUrlEditable: boolean; // true if url is + editedUrl?: string; +} + +const needsConfig = (value: any): boolean => { + if (typeof value === "string") { + return value.trim() === "" || value.trim().startsWith(" { + if (typeof value !== "string") return undefined; + const match = value.trim().match(/^$/); + return match ? match[1] : undefined; +}; + +export default function AgentInstallModal({ + visible, + onCancel, + agentDetails, + onInstallComplete, +}: AgentInstallModalProps) { + const { t, i18n } = useTranslation("common"); + const isZh = i18n.language === "zh" || i18n.language === "zh-CN"; + const { message } = App.useApp(); + + // Use unified import hook + const { importFromData, isImporting: isInstallingAgent } = useAgentImport({ + onSuccess: () => { + onInstallComplete?.(); + }, + onError: (error) => { + message.error(error.message || t("market.install.error.installFailed", "Failed to install agent")); + }, + }); + + const [currentStep, setCurrentStep] = useState(0); + const [llmModels, setLlmModels] = useState([]); + const [loadingModels, setLoadingModels] = useState(false); + const [selectedModelId, setSelectedModelId] = useState(null); + const [selectedModelName, setSelectedModelName] = useState(""); + + const [configFields, setConfigFields] = useState([]); + const [configValues, setConfigValues] = useState>({}); + + const [mcpServers, setMcpServers] = useState([]); + const [existingMcpServers, setExistingMcpServers] = useState([]); + const [loadingMcpServers, setLoadingMcpServers] = useState(false); + const [installingMcp, setInstallingMcp] = useState>({}); + + // Load LLM models + useEffect(() => { + if (visible) { + loadLLMModels(); + } + }, [visible]); + + // Parse agent details for config fields and MCP servers + useEffect(() => { + if (visible && agentDetails) { + parseConfigFields(); + parseMcpServers(); + } + }, [visible, agentDetails]); + + const loadLLMModels = async () => { + setLoadingModels(true); + try { + const models = await modelService.getLLMModels(); + setLlmModels(models.filter(m => m.connect_status === "available")); + + // Auto-select first available model + if (models.length > 0 && models[0].connect_status === "available") { + setSelectedModelId(models[0].id); + setSelectedModelName(models[0].displayName); + } + } catch (error) { + log.error("Failed to load LLM models:", error); + message.error(t("market.install.error.loadModels", "Failed to load models")); + } finally { + setLoadingModels(false); + } + }; + + const parseConfigFields = () => { + if (!agentDetails) return; + + const fields: ConfigField[] = []; + + // Check basic fields (excluding MCP-related fields) + const basicFields: Array<{ key: keyof MarketAgentDetail; label: string }> = [ + { key: "description", label: t("market.detail.description", "Description") }, + { key: "business_description", label: t("market.detail.businessDescription", "Business Description") }, + { key: "duty_prompt", label: t("market.detail.dutyPrompt", "Duty Prompt") }, + { key: "constraint_prompt", label: t("market.detail.constraintPrompt", "Constraint Prompt") }, + { key: "few_shots_prompt", label: t("market.detail.fewShotsPrompt", "Few Shots Prompt") }, + ]; + + basicFields.forEach(({ key, label }) => { + const value = agentDetails[key]; + if (needsConfig(value)) { + fields.push({ + fieldPath: key, + fieldLabel: label, + promptHint: extractPromptHint(value as string), + currentValue: value as string, + }); + } + }); + + // Check tool params (excluding MCP server names/urls) + agentDetails.tools?.forEach((tool, toolIndex) => { + if (tool.params && typeof tool.params === "object") { + Object.entries(tool.params).forEach(([paramKey, paramValue]) => { + if (needsConfig(paramValue)) { + fields.push({ + fieldPath: `tools[${toolIndex}].params.${paramKey}`, + fieldLabel: `${tool.name || tool.class_name} - ${paramKey}`, + promptHint: extractPromptHint(paramValue as string), + currentValue: paramValue as string, + }); + } + }); + } + }); + + setConfigFields(fields); + + // Initialize config values + const initialValues: Record = {}; + fields.forEach(field => { + initialValues[field.fieldPath] = ""; + }); + setConfigValues(initialValues); + }; + + const parseMcpServers = async () => { + if (!agentDetails?.mcp_servers || agentDetails.mcp_servers.length === 0) { + setMcpServers([]); + return; + } + + setLoadingMcpServers(true); + try { + // Load existing MCP servers from system + const result = await getMcpServerList(); + const existing = result.success ? result.data : []; + setExistingMcpServers(existing); + + // Check each MCP server + const serversToInstall: McpServerToInstall[] = agentDetails.mcp_servers.map(mcp => { + const isUrlConfigNeeded = needsConfig(mcp.mcp_url); + + // Check if already installed (match by both name and url) + const isInstalled = !isUrlConfigNeeded && existing.some( + (existingMcp: McpServer) => + existingMcp.service_name === mcp.mcp_server_name && + existingMcp.mcp_url === mcp.mcp_url + ); + + return { + mcp_server_name: mcp.mcp_server_name, + mcp_url: mcp.mcp_url, + isInstalled, + isUrlEditable: isUrlConfigNeeded, + editedUrl: isUrlConfigNeeded ? "" : mcp.mcp_url, + }; + }); + + setMcpServers(serversToInstall); + } catch (error) { + log.error("Failed to check MCP servers:", error); + message.error(t("market.install.error.checkMcp", "Failed to check MCP servers")); + } finally { + setLoadingMcpServers(false); + } + }; + + const handleMcpUrlChange = (index: number, newUrl: string) => { + setMcpServers(prev => { + const updated = [...prev]; + updated[index].editedUrl = newUrl; + return updated; + }); + }; + + const handleInstallMcp = async (index: number) => { + const mcp = mcpServers[index]; + const urlToUse = mcp.editedUrl || mcp.mcp_url; + + if (!urlToUse || urlToUse.trim() === "") { + message.error(t("market.install.error.mcpUrlRequired", "MCP URL is required")); + return; + } + + const key = `${index}`; + setInstallingMcp(prev => ({ ...prev, [key]: true })); + + try { + const result = await addMcpServer(urlToUse, mcp.mcp_server_name); + if (result.success) { + message.success(t("market.install.success.mcpInstalled", "MCP server installed successfully")); + // Mark as installed - update state directly without re-fetching + setMcpServers(prev => { + const updated = [...prev]; + updated[index].isInstalled = true; + updated[index].editedUrl = urlToUse; + return updated; + }); + } else { + message.error(result.message || t("market.install.error.mcpInstall", "Failed to install MCP server")); + } + } catch (error) { + log.error("Failed to install MCP server:", error); + message.error(t("market.install.error.mcpInstall", "Failed to install MCP server")); + } finally { + setInstallingMcp(prev => ({ ...prev, [key]: false })); + } + }; + + const handleNext = () => { + if (currentStep === 0) { + // Step 1: Model selection validation + if (!selectedModelId || !selectedModelName) { + message.error(t("market.install.error.modelRequired", "Please select a model")); + return; + } + } else if (currentStep === 1) { + // Step 2: Config fields validation + const emptyFields = configFields.filter(field => !configValues[field.fieldPath]?.trim()); + if (emptyFields.length > 0) { + message.error(t("market.install.error.configRequired", "Please fill in all required fields")); + return; + } + } + + setCurrentStep(prev => prev + 1); + }; + + const handlePrevious = () => { + setCurrentStep(prev => prev - 1); + }; + + const handleInstall = async () => { + try { + // Prepare the data structure for import + const importData = prepareImportData(); + + if (!importData) { + message.error(t("market.install.error.invalidData", "Invalid agent data")); + return; + } + + log.info("Importing agent with data:", importData); + + // Import using unified hook + await importFromData(importData); + + // Success message will be shown by onSuccess callback + message.success(t("market.install.success", "Agent installed successfully!")); + } catch (error) { + // Error message will be shown by onError callback + log.error("Failed to install agent:", error); + } + }; + + const prepareImportData = () => { + if (!agentDetails) return null; + + // Clone agent_json structure + const agentJson = JSON.parse(JSON.stringify(agentDetails.agent_json)); + + // Update model information + const agentInfo = agentJson.agent_info[String(agentDetails.agent_id)]; + if (agentInfo) { + agentInfo.model_id = selectedModelId; + agentInfo.model_name = selectedModelName; + + // Clear business logic model fields + agentInfo.business_logic_model_id = null; + agentInfo.business_logic_model_name = null; + + // Update config fields + configFields.forEach(field => { + const value = configValues[field.fieldPath]; + if (field.fieldPath.includes("tools[")) { + // Handle tool params + const match = field.fieldPath.match(/tools\[(\d+)\]\.params\.(.+)/); + if (match && agentInfo.tools) { + const toolIndex = parseInt(match[1]); + const paramKey = match[2]; + if (agentInfo.tools[toolIndex]) { + agentInfo.tools[toolIndex].params[paramKey] = value; + } + } + } else { + // Handle basic fields + agentInfo[field.fieldPath] = value; + } + }); + + // Update MCP info + if (agentJson.mcp_info) { + agentJson.mcp_info = agentJson.mcp_info.map((mcp: any) => { + const matchingServer = mcpServers.find( + s => s.mcp_server_name === mcp.mcp_server_name + ); + if (matchingServer && matchingServer.editedUrl) { + return { + ...mcp, + mcp_url: matchingServer.editedUrl, + }; + } + return mcp; + }); + } + } + + return agentJson; + }; + + const handleCancel = () => { + // Reset state + setCurrentStep(0); + setSelectedModelId(null); + setSelectedModelName(""); + setConfigFields([]); + setConfigValues({}); + setMcpServers([]); + onCancel(); + }; + + // Filter only required steps for navigation + const steps = [ + { + key: "model", + title: t("market.install.step.model", "Select Model"), + }, + configFields.length > 0 && { + key: "config", + title: t("market.install.step.config", "Configure Fields"), + }, + mcpServers.length > 0 && { + key: "mcp", + title: t("market.install.step.mcp", "MCP Servers"), + }, + ].filter(Boolean) as Array<{ key: string; title: string }>; + + // Check if can proceed to next step + const canProceed = () => { + const currentStepKey = steps[currentStep]?.key; + + if (currentStepKey === "model") { + return selectedModelId !== null && selectedModelName !== ""; + } else if (currentStepKey === "config") { + return configFields.every(field => configValues[field.fieldPath]?.trim()); + } else if (currentStepKey === "mcp") { + // All non-editable MCPs should be installed or have edited URLs + return mcpServers.every(mcp => + mcp.isInstalled || + (mcp.isUrlEditable && mcp.editedUrl && mcp.editedUrl.trim() !== "") || + (!mcp.isUrlEditable && mcp.mcp_url && mcp.mcp_url.trim() !== "") + ); + } + + return true; + }; + + const renderStepContent = () => { + const currentStepKey = steps[currentStep]?.key; + + if (currentStepKey === "model") { + return ( +
+ {/* Agent Info - Title and Description Style */} + {agentDetails && ( +
+

+ {agentDetails.display_name} +

+

+ {agentDetails.description} +

+
+ )} + +
+

+ {t("market.install.model.description", "Select a model from your configured models to use for this agent.")} +

+ +
+ +
+ {loadingModels ? ( + + ) : ( + + )} +
+
+ + {llmModels.length === 0 && !loadingModels && ( +
+ {t("market.install.model.noModels", "No available models. Please configure models first.")} +
+ )} +
+
+ ); + } else if (currentStepKey === "config") { + return ( +
+

+ {t("market.install.config.description", "Please configure the following required fields for this agent.")} +

+ +
+ {configFields.map((field) => ( + + {field.fieldLabel} + * + + } + required={false} + > + { + setConfigValues(prev => ({ + ...prev, + [field.fieldPath]: e.target.value, + })); + }} + placeholder={field.promptHint || t("market.install.config.placeholder", "Enter configuration value")} + rows={3} + size="large" + /> + + ))} +
+
+ ); + } else if (currentStepKey === "mcp") { + return ( +
+

+ {t("market.install.mcp.description", "This agent requires the following MCP servers. Please install or configure them.")} +

+ + {loadingMcpServers ? ( +
+ +
+ ) : ( +
+ {mcpServers.map((mcp, index) => ( +
+
+
+
+ + {mcp.mcp_server_name} + + {mcp.isInstalled ? ( + } color="success" className="text-sm"> + {t("market.install.mcp.installed", "Installed")} + + ) : ( + } color="default" className="text-sm"> + {t("market.install.mcp.notInstalled", "Not Installed")} + + )} +
+ +
+ + MCP URL: + + {(mcp.isUrlEditable || !mcp.isInstalled) ? ( + handleMcpUrlChange(index, e.target.value)} + placeholder={mcp.isUrlEditable + ? t("market.install.mcp.urlPlaceholder", "Enter MCP server URL") + : mcp.mcp_url + } + size="middle" + disabled={mcp.isInstalled} + style={{ maxWidth: "400px" }} + /> + ) : ( + + {mcp.editedUrl || mcp.mcp_url} + + )} +
+
+ + {!mcp.isInstalled && ( + + )} +
+
+ ))} +
+ )} +
+ ); + } + + return null; + }; + + const isLastStep = currentStep === steps.length - 1; + + return ( + + + {t("market.install.title", "Install Agent")} + + } + open={visible} + onCancel={handleCancel} + width={800} + footer={ +
+ + + {currentStep > 0 && ( + + )} + {!isLastStep && ( + + )} + {isLastStep && ( + + )} + +
+ } + > +
+ ({ + title: step.title, + }))} + className="mb-6" + /> + +
+ {renderStepContent()} +
+
+
+ ); +} + diff --git a/frontend/app/[locale]/market/components/AgentMarketCard.tsx b/frontend/app/[locale]/market/components/AgentMarketCard.tsx index 5ea4371a..317f5bec 100644 --- a/frontend/app/[locale]/market/components/AgentMarketCard.tsx +++ b/frontend/app/[locale]/market/components/AgentMarketCard.tsx @@ -5,6 +5,7 @@ import { motion } from "framer-motion"; import { Download, Tag, Wrench } from "lucide-react"; import { MarketAgentListItem } from "@/types/market"; import { useTranslation } from "react-i18next"; +import { getGenericLabel } from "@/lib/agentLabelMapper"; interface AgentMarketCardProps { agent: MarketAgentListItem; @@ -51,9 +52,7 @@ export function AgentMarketCard({ ? isZh ? agent.category.display_name_zh : agent.category.display_name - : isZh - ? "其他" - : "Other"} + : t("market.category.other", "Other")}
@@ -83,7 +82,7 @@ export function AgentMarketCard({ className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs bg-slate-100 dark:bg-slate-700 text-slate-600 dark:text-slate-300" > - {tag.display_name} + {getGenericLabel(tag.display_name, t)} ))} {agent.tags.length > 3 && ( diff --git a/frontend/app/[locale]/market/components/MarketAgentDetailModal.tsx b/frontend/app/[locale]/market/components/MarketAgentDetailModal.tsx index 4eb8adbf..e21846ec 100644 --- a/frontend/app/[locale]/market/components/MarketAgentDetailModal.tsx +++ b/frontend/app/[locale]/market/components/MarketAgentDetailModal.tsx @@ -4,8 +4,6 @@ import React from "react"; import { Modal, Tabs, Tag, Descriptions, Empty } from "antd"; import { useTranslation } from "react-i18next"; import { - CheckCircle, - XCircle, Bot, Settings, FileText, @@ -14,6 +12,7 @@ import { Sparkles, } from "lucide-react"; import { MarketAgentDetail } from "@/types/market"; +import { getToolSourceLabel, getGenericLabel } from "@/lib/agentLabelMapper"; interface MarketAgentDetailModalProps { visible: boolean; @@ -63,6 +62,7 @@ export default function MarketAgentDetailModal({ return value || "-"; }; + const items = [ { key: "basic", @@ -116,7 +116,7 @@ export default function MarketAgentDetailModal({
{agentDetails.tags.map((tag) => ( - {tag.display_name} + {getGenericLabel(tag.display_name, t)} ))}
@@ -168,12 +168,9 @@ export default function MarketAgentDetailModal({ {agentDetails?.max_steps || 0} - {renderFieldValue(agentDetails?.business_logic_model_name)} + {renderFieldValue(agentDetails?.model_name)} {tool.source && ( - {t("market.detail.toolSource", "Source")}: {tool.source} + {t("common.source", "Source")}: {getToolSourceLabel(tool.source, t)} )} {tool.usage && ( - {t("market.detail.toolUsage", "Usage")}: {tool.usage} + {t("common.usage", "Usage")}: {tool.usage} )} {tool.output_type && ( - Output: {tool.output_type} + + {t("common.output", "Output")}: {tool.output_type} + )}
diff --git a/frontend/app/[locale]/market/components/MarketErrorState.tsx b/frontend/app/[locale]/market/components/MarketErrorState.tsx new file mode 100644 index 00000000..6a837d81 --- /dev/null +++ b/frontend/app/[locale]/market/components/MarketErrorState.tsx @@ -0,0 +1,84 @@ +"use client"; + +import React from "react"; +import { Empty } from "antd"; +import { useTranslation } from "react-i18next"; +import { + ServerCrash, + WifiOff, + Clock, + AlertTriangle +} from "lucide-react"; + +interface MarketErrorStateProps { + type: "timeout" | "network" | "server" | "unknown"; +} + +/** + * Market Error State Component + * Displays error states for market API failures + * Style matches MarketContent design + */ +export default function MarketErrorState({ type }: MarketErrorStateProps) { + const { t } = useTranslation("common"); + + const errorConfig = { + timeout: { + icon: Clock, + title: t("market.error.timeout.title", "Request Timeout"), + description: t( + "market.error.timeout.description", + "The market server is taking too long to respond. Please check your network connection and try again." + ), + }, + network: { + icon: WifiOff, + title: t("market.error.network.title", "Network Error"), + description: t( + "market.error.network.description", + "Unable to connect to the market server. Please check your internet connection." + ), + }, + server: { + icon: ServerCrash, + title: t("market.error.server.title", "Server Error"), + description: t( + "market.error.server.description", + "The market server encountered an error. Please try again later." + ), + }, + unknown: { + icon: AlertTriangle, + title: t("market.error.unknown.title", "Something Went Wrong"), + description: t( + "market.error.unknown.description", + "An unexpected error occurred. Please try again." + ), + }, + }; + + const config = errorConfig[type]; + const Icon = config.icon; + + return ( +
+
+ {/* Icon and Title Row */} +
+
+ +
+
+ {config.title} +
+
+ + {/* Description */} +
+ {config.description} +
+
+
+ ); +} + diff --git a/frontend/app/[locale]/page.tsx b/frontend/app/[locale]/page.tsx index cea3159f..8d717413 100644 --- a/frontend/app/[locale]/page.tsx +++ b/frontend/app/[locale]/page.tsx @@ -20,7 +20,8 @@ import ModelsContent from "./models/ModelsContent"; import AgentsContent from "./agents/AgentsContent"; import KnowledgesContent from "./knowledges/KnowledgesContent"; import { SpaceContent } from "./space/components/SpaceContent"; -import { fetchAgentList, importAgent } from "@/services/agentConfigService"; +import { fetchAgentList } from "@/services/agentConfigService"; +import { useAgentImport } from "@/hooks/useAgentImport"; import SetupLayout from "./setup/SetupLayout"; import { ChatContent } from "./chat/internal/ChatContent"; import { ChatTopNavContent } from "./chat/internal/ChatTopNavContent"; @@ -149,8 +150,8 @@ export default function Home() { } // Load data for specific views - if (viewType === "space" && agents.length === 0) { - loadAgents(); + if (viewType === "space") { + loadAgents(); // Always refresh agents when entering space } }; @@ -186,6 +187,20 @@ export default function Home() { } }; + // Use unified import hook for space view + const { importFromFile: importAgentFile } = useAgentImport({ + onSuccess: () => { + message.success(t("businessLogic.config.error.agentImportSuccess")); + loadAgents(); + setIsImporting(false); + }, + onError: (error) => { + log.error(t("agentConfig.agents.importFailed"), error); + message.error(t("businessLogic.config.error.agentImportFailed")); + setIsImporting(false); + }, + }); + // Handle import agent for space view const handleImportAgent = () => { const fileInput = document.createElement("input"); @@ -202,32 +217,9 @@ export default function Home() { setIsImporting(true); try { - const fileContent = await file.text(); - let agentInfo; - - try { - agentInfo = JSON.parse(fileContent); - } catch (parseError) { - message.error(t("businessLogic.config.error.invalidFileType")); - setIsImporting(false); - return; - } - - const result = await importAgent(agentInfo); - - if (result.success) { - message.success(t("businessLogic.config.error.agentImportSuccess")); - loadAgents(); - } else { - message.error( - result.message || t("businessLogic.config.error.agentImportFailed") - ); - } + await importAgentFile(file); } catch (error) { - log.error(t("agentConfig.agents.importFailed"), error); - message.error(t("businessLogic.config.error.agentImportFailed")); - } finally { - setIsImporting(false); + // Error already handled by hook's onError callback } }; diff --git a/frontend/app/[locale]/space/components/AgentDetailModal.tsx b/frontend/app/[locale]/space/components/AgentDetailModal.tsx index 12e4d7f4..f7c7e3bd 100644 --- a/frontend/app/[locale]/space/components/AgentDetailModal.tsx +++ b/frontend/app/[locale]/space/components/AgentDetailModal.tsx @@ -15,6 +15,7 @@ import { } from "lucide-react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { generateAvatarFromName } from "@/lib/avatar"; +import { getToolSourceLabel, getCategoryLabel } from "@/lib/agentLabelMapper"; interface AgentDetailModalProps { visible: boolean; @@ -206,17 +207,17 @@ export default function AgentDetailModal({
{tool.source && ( - {t("space.detail.source", "Source")}: {tool.source} + {t("common.source", "Source")}: {getToolSourceLabel(tool.source, t)} )} {tool.category && ( - {t("space.detail.category", "Category")}: {tool.category} + {t("common.category", "Category")}: {getCategoryLabel(tool.category, t)} )} {tool.usage && ( - {t("space.detail.usage", "Usage")}: {tool.usage} + {t("common.usage", "Usage")}: {tool.usage} )}
diff --git a/frontend/hooks/useAgentImport.md b/frontend/hooks/useAgentImport.md new file mode 100644 index 00000000..52b14aa7 --- /dev/null +++ b/frontend/hooks/useAgentImport.md @@ -0,0 +1,245 @@ +# useAgentImport Hook + +Unified agent import hook for handling agent imports across the application. + +## Overview + +This hook provides a consistent interface for importing agents from different sources: +- File upload (used in Agent Development and Agent Space) +- Direct data (used in Agent Market) + +All import operations ultimately call the same backend `/agent/import` endpoint. + +## Usage + +### Basic Import + +```typescript +import { useAgentImport } from "@/hooks/useAgentImport"; + +function MyComponent() { + const { isImporting, importFromFile, importFromData, error } = useAgentImport({ + onSuccess: () => { + console.log("Import successful!"); + }, + onError: (error) => { + console.error("Import failed:", error); + }, + }); + + // ... +} +``` + +### Import from File (SubAgentPool, SpaceContent) + +```typescript +const handleFileImport = async (file: File) => { + try { + await importFromFile(file); + // Success handled by onSuccess callback + } catch (error) { + // Error handled by onError callback + } +}; + +// In file input handler + { + const file = e.target.files?.[0]; + if (file) { + handleFileImport(file); + } + }} +/> +``` + +### Import from Data (Market) + +```typescript +const handleMarketImport = async (agentDetails: MarketAgentDetail) => { + // Prepare import data from agent details + const importData = { + agent_id: agentDetails.agent_id, + agent_info: agentDetails.agent_json.agent_info, + mcp_info: agentDetails.agent_json.mcp_info, + }; + + try { + await importFromData(importData); + // Success handled by onSuccess callback + } catch (error) { + // Error handled by onError callback + } +}; +``` + +## Integration Examples + +### 1. SubAgentPool Component + +```typescript +// In SubAgentPool.tsx +import { useAgentImport } from "@/hooks/useAgentImport"; + +export default function SubAgentPool({ onImportSuccess }: Props) { + const { isImporting, importFromFile } = useAgentImport({ + onSuccess: () => { + message.success(t("agent.import.success")); + onImportSuccess?.(); + }, + onError: (error) => { + message.error(error.message); + }, + }); + + const handleImportClick = () => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json"; + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + await importFromFile(file); + } + }; + input.click(); + }; + + return ( + + ); +} +``` + +### 2. SpaceContent Component + +```typescript +// In SpaceContent.tsx +import { useAgentImport } from "@/hooks/useAgentImport"; + +export function SpaceContent({ onRefresh }: Props) { + const { isImporting, importFromFile } = useAgentImport({ + onSuccess: () => { + message.success(t("space.import.success")); + onRefresh(); // Reload agent list + }, + }); + + const handleImportAgent = () => { + const input = document.createElement("input"); + input.type = "file"; + input.accept = ".json"; + input.onchange = async (e) => { + const file = (e.target as HTMLInputElement).files?.[0]; + if (file) { + await importFromFile(file); + } + }; + input.click(); + }; + + return ( + + ); +} +``` + +### 3. AgentInstallModal (Market) + +```typescript +// In AgentInstallModal.tsx +import { useAgentImport } from "@/hooks/useAgentImport"; + +export default function AgentInstallModal({ + agentDetails, + onComplete +}: Props) { + const { isImporting, importFromData } = useAgentImport({ + onSuccess: () => { + message.success(t("market.install.success")); + onComplete(); + }, + }); + + const handleInstall = async () => { + // Prepare configured data + const importData = prepareImportData(agentDetails, userConfig); + await importFromData(importData); + }; + + return ( + + ); +} +``` + +## API Reference + +### Parameters + +```typescript +interface UseAgentImportOptions { + onSuccess?: () => void; // Called on successful import + onError?: (error: Error) => void; // Called on import error + forceImport?: boolean; // Force import even if duplicate names exist +} +``` + +### Return Value + +```typescript +interface UseAgentImportResult { + isImporting: boolean; // Import in progress + importFromFile: (file: File) => Promise; // Import from file + importFromData: (data: ImportAgentData) => Promise; // Import from data + error: Error | null; // Last error (if any) +} +``` + +### Data Structure + +```typescript +interface ImportAgentData { + agent_id: number; + agent_info: Record; + mcp_info?: Array<{ + mcp_server_name: string; + mcp_url: string; + }>; +} +``` + +## Error Handling + +The hook handles errors in two ways: + +1. **Via onError callback** - Preferred method for user-facing error messages +2. **Via thrown exceptions** - For custom error handling in specific cases + +Both approaches are supported to allow flexibility in different use cases. + +## Implementation Notes + +- File content is read as text and parsed as JSON +- Data structure validation is performed before calling the backend +- The backend `/agent/import` endpoint is called with the prepared data +- All logging uses the centralized `log` utility from `@/lib/logger` + diff --git a/frontend/hooks/useAgentImport.ts b/frontend/hooks/useAgentImport.ts new file mode 100644 index 00000000..f0f33add --- /dev/null +++ b/frontend/hooks/useAgentImport.ts @@ -0,0 +1,152 @@ +import { useState } from "react"; +import { importAgent } from "@/services/agentConfigService"; +import log from "@/lib/logger"; + +export interface ImportAgentData { + agent_id: number; + agent_info: Record; + mcp_info?: Array<{ + mcp_server_name: string; + mcp_url: string; + }>; +} + +export interface UseAgentImportOptions { + onSuccess?: () => void; + onError?: (error: Error) => void; + forceImport?: boolean; +} + +export interface UseAgentImportResult { + isImporting: boolean; + importFromFile: (file: File) => Promise; + importFromData: (data: ImportAgentData) => Promise; + error: Error | null; +} + +/** + * Unified agent import hook + * Handles agent import from both file upload and direct data + * Used in: + * - Agent development (SubAgentPool) + * - Agent space (SpaceContent) + * - Agent market (MarketContent) + */ +export function useAgentImport( + options: UseAgentImportOptions = {} +): UseAgentImportResult { + const { onSuccess, onError, forceImport = false } = options; + + const [isImporting, setIsImporting] = useState(false); + const [error, setError] = useState(null); + + /** + * Import agent from uploaded file + */ + const importFromFile = async (file: File): Promise => { + setIsImporting(true); + setError(null); + + try { + // Read file content + const fileContent = await readFileAsText(file); + + // Parse JSON + let agentData: ImportAgentData; + try { + agentData = JSON.parse(fileContent); + } catch (parseError) { + throw new Error("Invalid JSON file format"); + } + + // Validate structure + if (!agentData.agent_id || !agentData.agent_info) { + throw new Error("Invalid agent data structure"); + } + + // Import using unified logic + await importAgentData(agentData); + + onSuccess?.(); + } catch (err) { + const error = err instanceof Error ? err : new Error("Unknown error"); + log.error("Failed to import agent from file:", error); + setError(error); + onError?.(error); + throw error; + } finally { + setIsImporting(false); + } + }; + + /** + * Import agent from data object (e.g., from market) + */ + const importFromData = async (data: ImportAgentData): Promise => { + setIsImporting(true); + setError(null); + + try { + // Validate structure + if (!data.agent_id || !data.agent_info) { + throw new Error("Invalid agent data structure"); + } + + // Import using unified logic + await importAgentData(data); + + onSuccess?.(); + } catch (err) { + const error = err instanceof Error ? err : new Error("Unknown error"); + log.error("Failed to import agent from data:", error); + setError(error); + onError?.(error); + throw error; + } finally { + setIsImporting(false); + } + }; + + /** + * Core import logic - calls backend API + */ + const importAgentData = async (data: ImportAgentData): Promise => { + const result = await importAgent(data, { forceImport }); + + if (!result.success) { + throw new Error(result.message || "Failed to import agent"); + } + }; + + /** + * Helper: Read file as text + */ + const readFileAsText = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (e) => { + const content = e.target?.result; + if (typeof content === "string") { + resolve(content); + } else { + reject(new Error("Failed to read file content")); + } + }; + + reader.onerror = () => { + reject(new Error("Failed to read file")); + }; + + reader.readAsText(file); + }); + }; + + return { + isImporting, + importFromFile, + importFromData, + error, + }; +} + diff --git a/frontend/lib/agentLabelMapper.ts b/frontend/lib/agentLabelMapper.ts new file mode 100644 index 00000000..a95e9df0 --- /dev/null +++ b/frontend/lib/agentLabelMapper.ts @@ -0,0 +1,84 @@ +/** + * Agent Label Mapper Utility + * Provides unified label mapping for tool sources, agent types, and other labels + * across the application with i18n support + */ + +import { TFunction } from "i18next"; + +/** + * Map tool source to localized label + * @param source - Tool source (local, mcp, langchain, etc.) + * @param t - Translation function from i18next + * @returns Localized tool source label + */ +export function getToolSourceLabel(source: string, t: TFunction): string { + const sourceLower = source?.toLowerCase() || ""; + + switch (sourceLower) { + case "local": + return t("common.toolSource.local", "Local Tool"); + case "mcp": + return t("common.toolSource.mcp", "MCP Tool"); + case "langchain": + return t("common.toolSource.langchain", "LangChain Tool"); + default: + return source; + } +} + +/** + * Map agent type to localized label + * @param type - Agent type (single agent, multi agent, etc.) + * @param t - Translation function from i18next + * @returns Localized agent type label + */ +export function getAgentTypeLabel(type: string, t: TFunction): string { + const typeLower = type?.toLowerCase() || ""; + + switch (typeLower) { + case "single agent": + return t("common.agentType.single", "Single Agent"); + case "multi agent": + return t("common.agentType.multi", "Multi Agent"); + default: + return type; + } +} + +/** + * Map generic tag/label to localized label + * Handles both tool sources and agent types + * @param label - Tag or label name + * @param t - Translation function from i18next + * @returns Localized label + */ +export function getGenericLabel(label: string, t: TFunction): string { + const labelLower = label?.toLowerCase() || ""; + + // Check tool sources first + if (["local", "mcp", "langchain"].includes(labelLower)) { + return getToolSourceLabel(label, t); + } + + // Check agent types + if (["single agent", "multi agent"].includes(labelLower)) { + return getAgentTypeLabel(label, t); + } + + // Return original if no mapping found + return label; +} + +/** + * Map category to localized label (for tool categories) + * @param category - Category name + * @param t - Translation function from i18next + * @returns Localized category label + */ +export function getCategoryLabel(category: string, t: TFunction): string { + // For now, category mapping is the same as agent type mapping + // Can be extended if different mappings are needed + return getAgentTypeLabel(category, t); +} + diff --git a/frontend/public/locales/en/common.json b/frontend/public/locales/en/common.json index d2bb2f6e..e06e3c40 100644 --- a/frontend/public/locales/en/common.json +++ b/frontend/public/locales/en/common.json @@ -1078,7 +1078,7 @@ "space.detail.subAgentId": "Sub Agent ID", "space.detail.source": "Source", "space.detail.category": "Category", - "space.detail.usage": "Usage", + "space.detail.usage": "MCP Server", "space.detail.parameters": "Parameters", "sidebar.homePage": "Home Page", @@ -1110,6 +1110,7 @@ "market.description": "Discover and download pre-built intelligent agents", "market.searchPlaceholder": "Search agents by name or description...", "market.category.all": "All", + "market.category.other": "Other", "market.download": "Download", "market.downloading": "Downloading agent...", "market.downloadSuccess": "Agent downloaded successfully!", @@ -1141,6 +1142,7 @@ "market.detail.model": "Model Name", "market.detail.modelId": "Model ID", "market.detail.maxSteps": "Max Steps", + "market.detail.recommendedModel": "Recommended Model", "market.detail.businessLogicModel": "Business Logic Model", "market.detail.businessLogicModelId": "Business Logic Model ID", "market.detail.provideRunSummary": "Provide Run Summary", @@ -1152,9 +1154,64 @@ "market.detail.toolName": "Tool Name", "market.detail.toolDescription": "Description", "market.detail.toolSource": "Source", - "market.detail.toolUsage": "Usage", + "market.detail.toolUsage": "MCP Server", "market.detail.toolParams": "Parameters", "market.detail.mcpServerName": "Server Name", "market.detail.mcpServerUrl": "Server URL", - "market.detail.viewDetails": "View Details" + "market.detail.viewDetails": "View Details", + + "market.install.title": "Install Agent", + "market.install.step.model": "Select Model", + "market.install.step.config": "Configure Fields", + "market.install.step.mcp": "MCP Servers", + "market.install.step.optional": "(No Config Required)", + "market.install.button.previous": "Previous", + "market.install.button.next": "Next", + "market.install.button.install": "Install", + "market.install.button.installing": "Installing...", + "market.install.model.description": "Select a model from your configured models to use for this agent.", + "market.install.model.label": "Model", + "market.install.model.placeholder": "Select a model", + "market.install.model.noModels": "No available models. Please configure models first.", + "market.install.config.description": "Please configure the following required fields for this agent.", + "market.install.config.placeholder": "Enter configuration value", + "market.install.mcp.description": "This agent requires the following MCP servers. Please install or configure them.", + "market.install.mcp.installed": "Installed", + "market.install.mcp.notInstalled": "Not Installed", + "market.install.mcp.urlPlaceholder": "Enter MCP server URL", + "market.install.mcp.install": "Install", + "market.install.error.modelRequired": "Please select a model", + "market.install.error.configRequired": "Please fill in all required fields", + "market.install.error.mcpUrlRequired": "MCP URL is required", + "market.install.error.loadModels": "Failed to load models", + "market.install.error.checkMcp": "Failed to check MCP servers", + "market.install.error.mcpInstall": "Failed to install MCP server", + "market.install.error.invalidData": "Invalid agent data", + "market.install.error.installFailed": "Failed to install agent", + "market.install.success.mcpInstalled": "MCP server installed successfully", + "market.install.info.notImplemented": "Installation will be implemented in next phase", + "market.install.success": "Agent installed successfully!", + "market.error.fetchDetailFailed": "Failed to load agent details", + "market.error.retry": "Retry", + "market.error.timeout.title": "Request Timeout", + "market.error.timeout.description": "The market server is taking too long to respond. Please check your network connection and try again.", + "market.error.timeout.help": "If the problem persists, the market server may be experiencing high traffic.", + "market.error.network.title": "Network Error", + "market.error.network.description": "Unable to connect to the market server. Please check your internet connection.", + "market.error.network.help": "Check your firewall settings or contact your network administrator.", + "market.error.server.title": "Server Error", + "market.error.server.description": "The market server encountered an error. Our team has been notified. Please try again later.", + "market.error.unknown.title": "Something Went Wrong", + "market.error.unknown.description": "An unexpected error occurred. Please try again.", + + "common.toBeConfigured": "To Be Configured", + "common.source": "Source", + "common.category": "Category", + "common.usage": "Usage", + "common.output": "Output", + "common.toolSource.local": "Local Tool", + "common.toolSource.mcp": "MCP Tool", + "common.toolSource.langchain": "LangChain Tool", + "common.agentType.single": "Single Agent", + "common.agentType.multi": "Multi Agent" } diff --git a/frontend/public/locales/zh/common.json b/frontend/public/locales/zh/common.json index d4e0586c..07d5058d 100644 --- a/frontend/public/locales/zh/common.json +++ b/frontend/public/locales/zh/common.json @@ -1078,7 +1078,7 @@ "space.detail.subAgentId": "子智能体 ID", "space.detail.source": "来源", "space.detail.category": "分类", - "space.detail.usage": "用途", + "space.detail.usage": "MCP服务器", "space.detail.parameters": "参数", "sidebar.homePage": "首页", @@ -1103,6 +1103,7 @@ "market.description": "发现并下载预构建的智能体", "market.searchPlaceholder": "按名称或描述搜索智能体...", "market.category.all": "全部", + "market.category.other": "其他", "market.download": "下载", "market.downloading": "正在下载智能体...", "market.downloadSuccess": "智能体下载成功!", @@ -1134,6 +1135,7 @@ "market.detail.model": "模型名称", "market.detail.modelId": "模型 ID", "market.detail.maxSteps": "最大步数", + "market.detail.recommendedModel": "建议模型", "market.detail.businessLogicModel": "业务逻辑模型", "market.detail.businessLogicModelId": "业务逻辑模型 ID", "market.detail.provideRunSummary": "提供运行摘要", @@ -1145,16 +1147,71 @@ "market.detail.toolName": "工具名称", "market.detail.toolDescription": "描述", "market.detail.toolSource": "来源", - "market.detail.toolUsage": "用途", + "market.detail.toolUsage": "MCP 服务器", "market.detail.toolParams": "参数", "market.detail.mcpServerName": "服务器名称", "market.detail.mcpServerUrl": "服务器地址", "market.detail.viewDetails": "查看详情", + "market.install.title": "安装智能体", + "market.install.step.model": "选择模型", + "market.install.step.config": "配置字段", + "market.install.step.mcp": "MCP 服务器", + "market.install.step.optional": "(无需配置)", + "market.install.button.previous": "上一步", + "market.install.button.next": "下一步", + "market.install.button.install": "安装", + "market.install.button.installing": "正在安装...", + "market.install.model.description": "从已配置的模型中选择一个模型用于该智能体。", + "market.install.model.label": "模型", + "market.install.model.placeholder": "选择一个模型", + "market.install.model.noModels": "暂无可用模型。请先配置模型。", + "market.install.config.description": "请为该智能体配置以下必填字段。", + "market.install.config.placeholder": "输入配置值", + "market.install.mcp.description": "该智能体需要以下 MCP 服务器。请安装或配置它们。", + "market.install.mcp.installed": "已安装", + "market.install.mcp.notInstalled": "未安装", + "market.install.mcp.urlPlaceholder": "输入 MCP 服务器地址", + "market.install.mcp.install": "安装", + "market.install.error.modelRequired": "请选择一个模型", + "market.install.error.configRequired": "请填写所有必填字段", + "market.install.error.mcpUrlRequired": "MCP 地址为必填项", + "market.install.error.loadModels": "加载模型失败", + "market.install.error.checkMcp": "检查 MCP 服务器失败", + "market.install.error.mcpInstall": "安装 MCP 服务器失败", + "market.install.error.invalidData": "无效的智能体数据", + "market.install.error.installFailed": "安装智能体失败", + "market.install.success.mcpInstalled": "MCP 服务器安装成功", + "market.install.info.notImplemented": "安装功能将在下一阶段实现", + "market.install.success": "智能体安装成功!", + "market.error.fetchDetailFailed": "加载智能体详情失败", + "market.error.retry": "重试", + "market.error.timeout.title": "请求超时", + "market.error.timeout.description": "市场服务器响应时间过长。请检查您的网络连接并重试。", + "market.error.timeout.help": "如果问题持续存在,市场服务器可能正在经历高流量。", + "market.error.network.title": "网络错误", + "market.error.network.description": "无法连接到市场服务器。请检查您的互联网连接。", + "market.error.network.help": "检查您的防火墙设置或联系您的网络管理员。", + "market.error.server.title": "服务器错误", + "market.error.server.description": "市场服务器遇到错误。我们的团队已收到通知。请稍后重试。", + "market.error.unknown.title": "出现问题", + "market.error.unknown.description": "发生意外错误。请重试。", + "users.comingSoon.title": "用户管理即将推出", "users.comingSoon.description": "为管理员提供全面的用户管理系统。控制组织内的访问权限、角色和权限。", "users.comingSoon.feature1": "管理用户账户和角色", "users.comingSoon.feature2": "配置精细化权限", "users.comingSoon.feature3": "监控用户活动和使用情况", - "users.comingSoon.badge": "即将推出" + "users.comingSoon.badge": "即将推出", + + "common.toBeConfigured": "待配置", + "common.source": "来源", + "common.category": "分类", + "common.usage": "用途", + "common.output": "输出", + "common.toolSource.local": "本地工具", + "common.toolSource.mcp": "MCP工具", + "common.toolSource.langchain": "LangChain工具", + "common.agentType.single": "单智能体", + "common.agentType.multi": "多智能体" } diff --git a/frontend/services/marketService.ts b/frontend/services/marketService.ts index 5f525502..65abdb16 100644 --- a/frontend/services/marketService.ts +++ b/frontend/services/marketService.ts @@ -13,6 +13,70 @@ import { MarketAgentListParams, } from '@/types/market'; +// Market API timeout in milliseconds (5 seconds) +const MARKET_API_TIMEOUT = 5000; + +/** + * Custom error class for market API errors + */ +export class MarketApiError extends Error { + constructor( + message: string, + public type: 'timeout' | 'network' | 'server' | 'unknown' = 'unknown', + public statusCode?: number + ) { + super(message); + this.name = 'MarketApiError'; + } +} + +/** + * Fetch with timeout support + * @param url - Request URL + * @param options - Fetch options + * @param timeout - Timeout in milliseconds + * @returns Promise + * @throws MarketApiError on timeout or network error + */ +async function fetchWithTimeout( + url: string, + options: RequestInit = {}, + timeout: number = MARKET_API_TIMEOUT +): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal, + }); + clearTimeout(timeoutId); + return response; + } catch (error: any) { + clearTimeout(timeoutId); + + if (error.name === 'AbortError') { + throw new MarketApiError( + 'Request timeout - market server is not responding', + 'timeout' + ); + } + + if (error instanceof TypeError && error.message === 'Failed to fetch') { + throw new MarketApiError( + 'Network error - unable to connect to market server', + 'network' + ); + } + + throw new MarketApiError( + error.message || 'Unknown error occurred', + 'unknown' + ); + } +} + /** * Fetch agent list from market with pagination and filters */ @@ -21,7 +85,7 @@ export async function fetchMarketAgentList( ): Promise { try { const url = API_ENDPOINTS.market.agents(params); - const response = await fetch(url, { + const response = await fetchWithTimeout(url, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -29,7 +93,11 @@ export async function fetchMarketAgentList( }); if (!response.ok) { - throw new Error(`Failed to fetch market agents: ${response.statusText}`); + throw new MarketApiError( + `Failed to fetch market agents: ${response.statusText}`, + 'server', + response.status + ); } const data = await response.json(); @@ -48,7 +116,7 @@ export async function fetchMarketAgentDetail( ): Promise { try { const url = API_ENDPOINTS.market.agentDetail(agentId); - const response = await fetch(url, { + const response = await fetchWithTimeout(url, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -56,8 +124,10 @@ export async function fetchMarketAgentDetail( }); if (!response.ok) { - throw new Error( - `Failed to fetch market agent detail: ${response.statusText}` + throw new MarketApiError( + `Failed to fetch market agent detail: ${response.statusText}`, + 'server', + response.status ); } @@ -75,7 +145,7 @@ export async function fetchMarketAgentDetail( export async function fetchMarketCategories(): Promise { try { const url = API_ENDPOINTS.market.categories; - const response = await fetch(url, { + const response = await fetchWithTimeout(url, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -83,8 +153,10 @@ export async function fetchMarketCategories(): Promise { }); if (!response.ok) { - throw new Error( - `Failed to fetch market categories: ${response.statusText}` + throw new MarketApiError( + `Failed to fetch market categories: ${response.statusText}`, + 'server', + response.status ); } @@ -102,7 +174,7 @@ export async function fetchMarketCategories(): Promise { export async function fetchMarketTags(): Promise { try { const url = API_ENDPOINTS.market.tags; - const response = await fetch(url, { + const response = await fetchWithTimeout(url, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -110,7 +182,11 @@ export async function fetchMarketTags(): Promise { }); if (!response.ok) { - throw new Error(`Failed to fetch market tags: ${response.statusText}`); + throw new MarketApiError( + `Failed to fetch market tags: ${response.statusText}`, + 'server', + response.status + ); } const data = await response.json(); @@ -129,7 +205,7 @@ export async function fetchMarketAgentMcpServers( ): Promise { try { const url = API_ENDPOINTS.market.mcpServers(agentId); - const response = await fetch(url, { + const response = await fetchWithTimeout(url, { method: 'GET', headers: { 'Content-Type': 'application/json', @@ -137,8 +213,10 @@ export async function fetchMarketAgentMcpServers( }); if (!response.ok) { - throw new Error( - `Failed to fetch agent MCP servers: ${response.statusText}` + throw new MarketApiError( + `Failed to fetch agent MCP servers: ${response.statusText}`, + 'server', + response.status ); }