From 357b0d1c5c85a1e975f1ddaf6a1b93ddec51300f Mon Sep 17 00:00:00 2001 From: xuyaqist Date: Thu, 13 Aug 2026 11:18:44 +0800 Subject: [PATCH 01/63] refactor new agents --- .../[locale]/newagents/AgentVersionCard.tsx | 609 +++++ .../[locale]/newagents/AgentVersionManage.tsx | 184 ++ .../app/[locale]/newagents/agent-config.tsx | 293 ++ .../components/a2a/A2AAgentDiscoveryModal.tsx | 837 ++++++ .../newagents/components/a2a/A2AChatModal.tsx | 322 +++ .../components/a2a/A2AServerSettingsPanel.tsx | 176 ++ .../components/a2a/NacosDiscoveryPanel.tsx | 623 +++++ .../newagents/components/agent-capability.tsx | 218 ++ .../newagents/components/agent-deployment.tsx | 112 + .../newagents/components/agent-guide.tsx | 77 + .../newagents/components/agent-info.tsx | 100 + .../newagents/components/agent-prompt.tsx | 114 + .../newagents/components/agent-run-policy.tsx | 107 + .../components/agent-selector-header.tsx | 853 ++++++ .../components/agentConfig/McpConfigModal.tsx | 1373 ++++++++++ .../agentConfig/SelectedSkillManagement.tsx | 315 +++ .../agentConfig/SkillBuildModal.tsx | 1409 ++++++++++ .../agentConfig/SkillDetailModal.tsx | 228 ++ .../agentConfig/SkillDraftPanel.tsx | 558 ++++ .../agentConfig/SkillManagement.tsx | 417 +++ .../components/agentConfig/ToolManagement.tsx | 312 +++ .../agentConfig/skill/SelectSkillsDialog.tsx | 506 ++++ .../agentConfig/skill/SkillConfigModal.tsx | 668 +++++ .../agentConfig/skill/SkillRowContent.tsx | 41 + .../skill/SkillTagManagementModal.tsx | 134 + .../components/agentConfig/skill/utils.ts | 50 + .../agentConfig/tool/LabelManagementModal.tsx | 160 ++ .../agentConfig/tool/SelectToolsDialog.tsx | 672 +++++ .../agentConfig/tool/ToolConfigModal.tsx | 2357 +++++++++++++++++ .../agentConfig/tool/ToolTestPanel.tsx | 1018 +++++++ .../components/agentConfig/tool/utils.ts | 24 + .../agentInfo/AgentGenerateDetail.tsx | 1459 ++++++++++ .../components/agentInfo/DebugConfig.tsx | 1102 ++++++++ .../components/agentInfo/DebugMessageList.tsx | 165 ++ .../agentInfo/DebugOptimizeModal.tsx | 221 ++ .../agentInfo/DebugPromptCompareModal.tsx | 76 + .../components/agentInfo/ExpandEditModal.tsx | 96 + .../agentInfo/GuardrailConfigContent.tsx | 1197 +++++++++ .../agentInfo/PromptOptimizeModal.tsx | 397 +++ .../agentInfo/PromptTemplateManagerModal.tsx | 517 ++++ .../components/agentInfo/useCompareStream.ts | 841 ++++++ .../components/agentManage/AgentList.tsx | 671 +++++ .../components/collaborative-agent.tsx | 331 +++ .../components/knowledge-base-config.tsx | 371 +++ frontend/app/[locale]/newagents/page.tsx | 112 + .../versions/AgentVersionCompareModal.tsx | 504 ++++ .../versions/AgentVersionPubulishModal.tsx | 283 ++ .../components/agent/CreateAgentModal.tsx | 103 + 48 files changed, 23313 insertions(+) create mode 100644 frontend/app/[locale]/newagents/AgentVersionCard.tsx create mode 100644 frontend/app/[locale]/newagents/AgentVersionManage.tsx create mode 100644 frontend/app/[locale]/newagents/agent-config.tsx create mode 100644 frontend/app/[locale]/newagents/components/a2a/A2AAgentDiscoveryModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/a2a/A2AChatModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/a2a/A2AServerSettingsPanel.tsx create mode 100644 frontend/app/[locale]/newagents/components/a2a/NacosDiscoveryPanel.tsx create mode 100644 frontend/app/[locale]/newagents/components/agent-capability.tsx create mode 100644 frontend/app/[locale]/newagents/components/agent-deployment.tsx create mode 100644 frontend/app/[locale]/newagents/components/agent-guide.tsx create mode 100644 frontend/app/[locale]/newagents/components/agent-info.tsx create mode 100644 frontend/app/[locale]/newagents/components/agent-prompt.tsx create mode 100644 frontend/app/[locale]/newagents/components/agent-run-policy.tsx create mode 100644 frontend/app/[locale]/newagents/components/agent-selector-header.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/McpConfigModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/SelectedSkillManagement.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/SkillBuildModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/SkillDetailModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/SkillDraftPanel.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/SkillManagement.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/ToolManagement.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/skill/SelectSkillsDialog.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/skill/SkillConfigModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/skill/SkillRowContent.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/skill/SkillTagManagementModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/skill/utils.ts create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/tool/LabelManagementModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/tool/SelectToolsDialog.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/tool/ToolConfigModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/tool/ToolTestPanel.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentConfig/tool/utils.ts create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/AgentGenerateDetail.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/DebugConfig.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/DebugMessageList.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/DebugOptimizeModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/DebugPromptCompareModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/ExpandEditModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/GuardrailConfigContent.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/PromptOptimizeModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/PromptTemplateManagerModal.tsx create mode 100644 frontend/app/[locale]/newagents/components/agentInfo/useCompareStream.ts create mode 100644 frontend/app/[locale]/newagents/components/agentManage/AgentList.tsx create mode 100644 frontend/app/[locale]/newagents/components/collaborative-agent.tsx create mode 100644 frontend/app/[locale]/newagents/components/knowledge-base-config.tsx create mode 100644 frontend/app/[locale]/newagents/page.tsx create mode 100644 frontend/app/[locale]/newagents/versions/AgentVersionCompareModal.tsx create mode 100644 frontend/app/[locale]/newagents/versions/AgentVersionPubulishModal.tsx create mode 100644 frontend/components/agent/CreateAgentModal.tsx diff --git a/frontend/app/[locale]/newagents/AgentVersionCard.tsx b/frontend/app/[locale]/newagents/AgentVersionCard.tsx new file mode 100644 index 000000000..dca4e2dbc --- /dev/null +++ b/frontend/app/[locale]/newagents/AgentVersionCard.tsx @@ -0,0 +1,609 @@ +"use client"; +import { useMemo, useState } from "react"; +import { + CheckCircle, + Archive, + Clock, + ChevronDown, + ChevronRight, + Rocket, + RotateCcw, + Eye, + Wrench, + Network, + AlertTriangle, + EllipsisVertical, + Trash2, + ArchiveRestore, + Edit +} from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { + Flex, + Button, + Tag, + Typography, + Card, + Descriptions, + DescriptionsProps, + Modal, + Dropdown, + Tooltip, + theme +} from "antd"; +import { ExclamationCircleFilled } from '@ant-design/icons'; + +const { useToken } = theme; +import type { AgentVersion, Agent as AgentVersionAgent, ToolInstance, AgentVersionDetail, VersionCompareResponse } from "@/services/agentVersionService"; +import type { Agent, Tool } from "@/types/agentConfig"; +import { useToolList } from "@/hooks/agent/useToolList"; +import { useAgentList } from "@/hooks/agent/useAgentList"; +import { useAgentVersionList } from "@/hooks/agent/useAgentVersionList"; +import { useAgentVersionDetail } from "@/hooks/agent/useAgentVersionDetail"; +import { rollbackVersion, compareVersions, deleteVersion } from "@/services/agentVersionService"; +import { searchAgentInfo } from "@/services/agentConfigService"; +import { useAgentConfigStore } from "@/stores/agentConfigStore"; +import { useAuthorizationContext } from "@/components/providers/AuthorizationProvider"; +import log from "@/lib/logger"; +import { resolveAgentListTenantKey } from "@/lib/agentListTenant"; +import { message } from "antd"; +import { useQueryClient } from "@tanstack/react-query"; +import AgentVersionCompareModal from "./versions/AgentVersionCompareModal"; +import AgentVersionPubulishModal from "./versions/AgentVersionPubulishModal"; + +const { Text } = Typography; + +const formatter = new Intl.DateTimeFormat("zh-CN", { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, +}); + +/** + * Format UTC time string from backend to local time string based on user timezone + */ +function formatUtcToLocal(dateTimeStr?: string | null) { + if (!dateTimeStr) { + return ""; + } + + // Detect whether the string already contains timezone information + const hasTimezone = /[zZ]|[+\-]\d{2}:?\d{2}$/.test(dateTimeStr); + + let date: Date; + if (hasTimezone) { + // If timezone exists, use as is + date = new Date(dateTimeStr); + } else { + // Treat as UTC time from database, convert to local time + // Normalize space-separated format like "2025-02-25 08:00:00" + const normalized = dateTimeStr.replace(" ", "T"); + date = new Date(`${normalized}Z`); + } + + return formatter.format(date); +} + +/** + * Get status configuration based on isCurrentVersion flag + */ +function getStatusConfig(isCurrentVersion: boolean) { + if (isCurrentVersion) { + return { + color: "green", + icon: ( +
+ +
+ ), + labelKey: "agent.version.currentVersion", + }; + } + + return { + color: "default", + icon: ( +
+ +
+ ), + labelKey: "", + }; +} + +/** + * Version card item component + */ +export function VersionCardItem({ + version, + agentId, + currentVersionNo, +}: { + version: AgentVersion; + agentId: number; + currentVersionNo?: number; +}) { + // Calculate isCurrentVersion based on version.version_no and currentVersionNo + const isCurrentVersion = currentVersionNo === version.version_no; + const statusConfig = getStatusConfig(isCurrentVersion); + const { t } = useTranslation("common"); + + // Local expanded state for this version card + const [isExpanded, setIsExpanded] = useState(false); + + // Get user context for tenantId + const { user } = useAuthorizationContext(); + const queryClient = useQueryClient(); + + // Get invalidate functions for refreshing data + const { agentVersionList, invalidate: invalidateAgentVersionList } = useAgentVersionList(agentId); + + // Fetch version detail when expanded + const { agentVersionDetail } = useAgentVersionDetail( + agentId, + isExpanded ? version.version_no : null + ); + + const { tools: toolList } = useToolList(); + const { agents: agentList } = useAgentList(""); + + // Get current agent's permission from agent list + const currentAgent = useMemo(() => { + return agentList.find((a: Agent) => a.id === String(agentId)); + }, [agentList, agentId]); + + const isReadOnly = currentAgent?.permission === "READ_ONLY"; + + // Modal state + const [compareModalOpen, setCompareModalOpen] = useState(false); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [editModalOpen, setEditModalOpen] = useState(false); + const [loading, setLoading] = useState(false); + const [rollbackLoading, setRollbackLoading] = useState(false); + const [deleteLoading, setDeleteLoading] = useState(false); + const [compareData, setCompareData] = useState(null); + const [selectedVersionNoA, setSelectedVersionNoA] = useState(null); + const [selectedVersionNoB, setSelectedVersionNoB] = useState(null); + + // Get theme token for styling + const { token } = theme.useToken(); + + // Generate display date from version data (convert from UTC to local time) + const displayDate = useMemo(() => { + return formatUtcToLocal(version.create_time); + }, [version.create_time]); + + /** + * Handle rollback button click - show comparison modal + */ + const handleRollbackClick = async () => { + if (!agentId || agentId === 0) { + message.error(t("agent.error.agentNotFound")); + return; + } + const versionNoA = currentVersionNo || 0; + const versionNoB = version.version_no; + setSelectedVersionNoA(versionNoA); + setSelectedVersionNoB(versionNoB); + setCompareModalOpen(true); + await loadComparison(versionNoA, versionNoB); + }; + + /** + * Load version comparison data between current version and selected version + */ + const loadComparison = async (versionNoA: number, versionNoB: number) => { + setLoading(true); + try { + const result = await compareVersions(agentId, versionNoA, versionNoB); + setCompareData(result); + } catch (error) { + log.error("Failed to load version comparison:", error); + message.error(t("agent.version.compareError")); + } finally { + setLoading(false); + } + }; + + const handleChangeVersionA = async (value: number) => { + setSelectedVersionNoA(value); + if (!selectedVersionNoB) { + return; + } + if (value === selectedVersionNoB) { + message.warning(t("agent.version.selectDifferentVersions")); + return; + } + await loadComparison(value, selectedVersionNoB); + }; + + const handleChangeVersionB = async (value: number) => { + setSelectedVersionNoB(value); + if (!selectedVersionNoA) { + return; + } + if (value === selectedVersionNoA) { + message.warning(t("agent.version.selectDifferentVersions")); + return; + } + await loadComparison(selectedVersionNoA, value); + }; + + /** + * Handle rollback confirmation + * Rollback updates current_version_no to point to the target version + * The user can then click publish to create an actual new version + */ + const handleRollbackConfirm = async () => { + setRollbackLoading(true); + try { + const result = await rollbackVersion(agentId, version.version_no); + + if (result.success) { + message.success(t("agent.version.rollbackSuccess")); + setCompareModalOpen(false); + invalidateAgentVersionList?.(); + queryClient.invalidateQueries({ queryKey: ["agentInfo", agentId] }); + queryClient.invalidateQueries({ queryKey: ["agents"] }); + + // Refresh agent detail and sync to Zustand store + const store = useAgentConfigStore.getState(); + if (store.currentAgentId === agentId) { + const agentResult = await searchAgentInfo(agentId); + if (agentResult.success && agentResult.data) { + store.setCurrentAgent(agentResult.data); + store.triggerForceRefresh(); + } + } + } else { + message.error(result.message || t("agent.version.rollbackError")); + } + } catch (error) { + log.error("Failed to rollback version:", error); + message.error(t("agent.version.rollbackError")); + } finally { + setRollbackLoading(false); + } + }; + + /** + * Handle delete version button click - show confirmation modal + */ + const handleDeleteClick = () => { + if (!agentId || agentId === 0) { + message.error(t("agent.error.agentNotFound")); + return; + } + setDeleteModalOpen(true); + }; + + /** + * Handle delete confirmation - actually delete the version + */ + const handleDeleteConfirm = async () => { + setDeleteLoading(true); + try { + const result = await deleteVersion(agentId, version.version_no); + + if (result.success) { + message.success(t("agent.version.deleteSuccess")); + setDeleteModalOpen(false); + invalidateAgentVersionList?.(); + queryClient.invalidateQueries({ queryKey: ["agentInfo", agentId] }); + queryClient.invalidateQueries({ queryKey: ["agents"] }); + } else { + message.error(result.message || t("agent.version.deleteError")); + } + } catch (error) { + log.error("Failed to delete version:", error); + message.error(t("agent.version.deleteError")); + } finally { + setDeleteLoading(false); + } + }; + + const agentConfigurationItems: DescriptionsProps['items'] = [ + { + key: '1', + label: t("agent.version.field.name"), + children: {agentVersionDetail?.name}, + }, + { + key: '2', + label: t("agent.version.field.modelName"), + children: ( + + {Array.isArray((agentVersionDetail as any)?.model_names) && + (agentVersionDetail as any).model_names.length > 0 + ? (agentVersionDetail as any).model_names.join(", ") + : (agentVersionDetail as any)?.model_name || "-"} + + ), + }, + ]; + + return ( +
+ + + {/* Left: Status icon with timeline */} + + + {statusConfig.icon} + +
+ + + {/* Middle: Version info */} + + + + {version.version_name || `V${version.version_no}`} + + + {t(statusConfig.labelKey)} + + + + + + + + {displayDate} + + + + + + {version.release_note && ( + + {version.release_note} + + )} + + + {/* Right: Actions */} + + , + , + ]} + centered + > + +
+ +
+
+
+ {t("agent.version.deleteConfirmContent", { versionName: version.version_name || `V${version.version_no}` })} +
+
+ {t("agent.version.deleteWarning")} +
+
+
+ + + {/* Edit Version Modal */} + setEditModalOpen(false)} + agentId={agentId} + versionNo={version.version_no} + isEdit={true} + initialValues={{ + version_name: version.version_name, + release_note: version.release_note, + is_a2a: version.is_a2a, + }} + onUpdated={() => { + // Refresh version list using the proper invalidate function + invalidateAgentVersionList(); + }} + /> +
+ ); +} diff --git a/frontend/app/[locale]/newagents/AgentVersionManage.tsx b/frontend/app/[locale]/newagents/AgentVersionManage.tsx new file mode 100644 index 000000000..edf26576c --- /dev/null +++ b/frontend/app/[locale]/newagents/AgentVersionManage.tsx @@ -0,0 +1,184 @@ +"use client"; +import { useState } from "react"; +import { GitBranch, GitCompare, Rocket, X } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import { Card, Flex, Button, Tag, Empty, Spin, message } from "antd"; +import { useAgentVersionList } from "@/hooks/agent/useAgentVersionList"; +import { useAgentInfo } from "@/hooks/agent/useAgentInfo"; +import { useAgentConfigStore } from "@/stores/agentConfigStore"; +import { VersionCardItem } from "./AgentVersionCard"; +import log from "@/lib/logger"; +import AgentVersionCompareModal from "./versions/AgentVersionCompareModal"; +import { compareVersions, type VersionCompareResponse } from "@/services/agentVersionService"; + +interface AgentVersionManageProps { + onClose?: () => void; +} + +export default function AgentVersionManage({ onClose }: AgentVersionManageProps) { + const { t } = useTranslation("common"); + const currentAgentId = useAgentConfigStore((state) => state.currentAgentId); + + const { agentVersionList, total, isLoading, invalidate: invalidateAgentVersionList } = useAgentVersionList(currentAgentId); + const { agentInfo, invalidate: invalidateAgentInfo } = useAgentInfo(currentAgentId); + + const [compareModalOpen, setCompareModalOpen] = useState(false); + const [compareLoading, setCompareLoading] = useState(false); + const [compareData, setCompareData] = useState(null); + const [selectedVersionA, setSelectedVersionA] = useState(null); + const [selectedVersionB, setSelectedVersionB] = useState(null); + + + const loadComparison = async (agentId: number, versionNoA: number, versionNoB: number) => { + try { + setCompareLoading(true); + const result = await compareVersions(agentId, versionNoA, versionNoB); + setCompareData(result); + } catch (error) { + log.error("Failed to compare versions:", error); + message.error(t("agent.version.compareError")); + } finally { + setCompareLoading(false); + } + }; + + const handleOpenCompareModal = async () => { + if (!currentAgentId) { + message.error(t("agent.error.agentNotFound")); + return; + } + if (agentVersionList.length < 2) { + message.warning(t("agent.version.needTwoVersions")); + return; + } + + // Use the last two versions by version_no as default comparison + const sorted = [...agentVersionList].sort((a, b) => a.version_no - b.version_no); + const defaultVersionA = sorted[sorted.length - 2]?.version_no; + const defaultVersionB = sorted[sorted.length - 1]?.version_no; + + if (!defaultVersionA || !defaultVersionB) { + message.warning(t("agent.version.needTwoVersions")); + return; + } + + setSelectedVersionA(defaultVersionA); + setSelectedVersionB(defaultVersionB); + setCompareModalOpen(true); + await loadComparison(currentAgentId, defaultVersionA, defaultVersionB); + }; + + const handleChangeVersionA = async (value: number) => { + setSelectedVersionA(value); + if (!currentAgentId || !selectedVersionB) { + return; + } + if (value === selectedVersionB) { + message.warning(t("agent.version.selectDifferentVersions")); + return; + } + await loadComparison(currentAgentId, value, selectedVersionB); + }; + + const handleChangeVersionB = async (value: number) => { + setSelectedVersionB(value); + if (!currentAgentId || !selectedVersionA) { + return; + } + if (value === selectedVersionA) { + message.warning(t("agent.version.selectDifferentVersions")); + return; + } + await loadComparison(currentAgentId, selectedVersionA, value); + }; + + const footer = [ + + + {t("agent.version.totalVersions", { count: total })} + + + , + ]; + + return ( + <> + + + {t("agent.version.manage")} +
+ } + actions={[ + ...footer, + onClose ? ( + +
+ + + setCustomHeaders(e.target.value)} + /> + + + + + + {loading && ( +
+ + {t("a2a.discovery.discovering")} + +
+ )} + + {selectedAgent && !loading && ( + handleAddToLocalAgent(selectedAgent) : undefined + } + /> + )} + + ), + }, + // Nacos Discovery Tab + { + key: "nacos", + label: ( + + + {t("a2a.discovery.tab.nacos")} + + ), + disabled: false, + children: ( + + ), + }, + // List Tab + { + key: "list", + label: ( + + + {t("a2a.discovery.tab.list")} + + ), + children: ( +
+
+ +
+ + + + ), + }, + ]} + /> + + + + ); +} + +// Agent Detail Card Component +interface AgentDetailCardProps { + agent: A2AExternalAgent; + onAddToLocalAgent?: () => void; +} + +function AgentDetailCard({ agent, onAddToLocalAgent }: Readonly) { + const { t } = useTranslation("common"); + + return ( +
+
+
+
+ + {agent.name} + + {agent.is_available + ? t("a2a.status.available") + : t("a2a.status.unavailable")} + +
+ + {agent.description && ( + + {agent.description} + + )} + +
+ {agent.supported_interfaces && agent.supported_interfaces.length > 0 && ( +
+ {t("a2a.agent.supportedInterfaces")}: +
+ {agent.supported_interfaces.map((iface: any, idx: number) => ( +
+ {iface.protocolBinding} + {iface.url} + v{iface.protocolVersion} +
+ ))} +
+
+ )} +
+
+ + {onAddToLocalAgent && ( + + )} +
+
+ ); +} diff --git a/frontend/app/[locale]/newagents/components/a2a/A2AChatModal.tsx b/frontend/app/[locale]/newagents/components/a2a/A2AChatModal.tsx new file mode 100644 index 000000000..0b40f9d7c --- /dev/null +++ b/frontend/app/[locale]/newagents/components/a2a/A2AChatModal.tsx @@ -0,0 +1,322 @@ +"use client"; + +import { useState, useRef, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { Modal, Button, Input, Tag, Typography } from "antd"; +import { Globe, Send, User, Bot, Loader2 } from "lucide-react"; +import { A2AExternalAgent, a2aClientService } from "@/services/a2aService"; +import log from "@/lib/logger"; + +const { Text } = Typography; + +export interface ChatMessage { + id: string; + role: "user" | "agent"; + content: string; + timestamp: Date; +} + +interface A2AChatModalProps { + open: boolean; + onClose: () => void; + agent: A2AExternalAgent; +} + +export default function A2AChatModal({ + open, + onClose, + agent, +}: Readonly) { + const { t } = useTranslation("common"); + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(""); + const [sending, setSending] = useState(false); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + /** + * Extract text content from A2A SendMessage response. + * Supports multiple response formats: + * 1. Direct message: { message: { parts: [...] } } + * 2. Task wrapper: { task: { status: { message: { parts: [...] } } } } + * 3. Fallback: any response with message text + */ + const extractAgentContent = (data: any): string => { + if (!data) return t("a2a.chat.noResponse"); + + // Format 1: Direct message with parts + if (data.message?.parts) { + const parts = data.message.parts; + if (Array.isArray(parts) && parts.length > 0) { + return parts[0].text || t("a2a.chat.noResponse"); + } + } + + // Format 1 fallback: Direct message as string + if (typeof data.message === "string") { + return data.message; + } + + // Format 2: Task wrapper (A2A SendMessage standard) + if (data.task?.status?.message?.parts) { + const parts = data.task.status.message.parts; + if (Array.isArray(parts) && parts.length > 0) { + return parts[0].text || t("a2a.chat.noResponse"); + } + } + + // Format 2 fallback: Task status message as string + if (typeof data.task?.status?.message === "string") { + return data.task.status.message; + } + + // Format 3: Look for text in common locations + if (data.text) return data.text; + if (data.content) return typeof data.content === "string" ? data.content : data.content?.text; + if (data.result?.text) return data.result.text; + if (data.result?.message?.text) return data.result.message.text; + + // Last resort: stringify meaningful data + if (data.message) return JSON.stringify(data.message); + + return t("a2a.chat.noResponse"); + }; + + useEffect(() => { + if (open) { + setMessages([]); + setInputValue(""); + } + }, [open]); + + useEffect(() => { + scrollToBottom(); + }, [messages]); + + const handleSend = async () => { + if (!inputValue.trim() || sending) { + return; + } + + const userMessage: ChatMessage = { + id: `user-${Date.now()}`, + role: "user", + content: inputValue.trim(), + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInputValue(""); + setSending(true); + + try { + const result = await a2aClientService.sendChatMessage( + String(agent.id), + userMessage.content + ); + + if (result.success) { + // Support both direct message format and Task wrapper format (A2A SendMessage standard) + // Format 1: { message: { parts: [...] } } + // Format 2: { task: { status: { message: { parts: [...] } } } } + const agentContent = extractAgentContent(result.data); + + const agentMessage: ChatMessage = { + id: `agent-${Date.now()}`, + role: "agent", + content: agentContent, + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, agentMessage]); + } else { + const errorMessage: ChatMessage = { + id: `error-${Date.now()}`, + role: "agent", + content: result.message || t("a2a.chat.sendFailed"), + timestamp: new Date(), + }; + setMessages((prev) => [...prev, errorMessage]); + } + } catch (error) { + log.error("Failed to send chat message:", error); + const errorMessage: ChatMessage = { + id: `error-${Date.now()}`, + role: "agent", + content: t("a2a.chat.sendFailed"), + timestamp: new Date(), + }; + setMessages((prev) => [...prev, errorMessage]); + } + + setSending(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const formatTime = (date: Date) => { + return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + }; + + const renderMessageContent = (content: string) => { + return content.split("\n").map((line, index) => ( + + {line} + {index < content.split("\n").length - 1 &&
} +
+ )); + }; + + return ( + + + {t("a2a.chat.title")} + + {agent.is_available + ? t("a2a.status.available") + : t("a2a.status.unavailable")} + + + } + open={open} + onCancel={onClose} + footer={null} + width={600} + destroyOnHidden + > +
+ {/* Agent Info Header */} +
+ + {agent.name} + {agent.description && ( + + {agent.description} + + )} +
+ + {/* Messages Container */} +
+ {messages.length === 0 && ( +
+ + {t("a2a.chat.emptyHistory")} +
+ )} + + {messages.map((message) => ( +
+
+
+ {message.role === "user" ? ( + + ) : ( + + )} +
+
+
+
+ {renderMessageContent(message.content)} +
+
+ + {formatTime(message.timestamp)} + +
+
+
+ ))} + + {sending && ( +
+
+
+ +
+
+ +
+
+
+ )} + +
+
+ + {/* Input Area */} +
+
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + rows={2} + autoSize={{ minRows: 2, maxRows: 4 }} + disabled={sending || !agent.is_available} + /> + +
+ {!agent.is_available && ( + + {t("a2a.chat.agentUnavailable")} + + )} +
+
+ + ); +} diff --git a/frontend/app/[locale]/newagents/components/a2a/A2AServerSettingsPanel.tsx b/frontend/app/[locale]/newagents/components/a2a/A2AServerSettingsPanel.tsx new file mode 100644 index 000000000..e15ff34ed --- /dev/null +++ b/frontend/app/[locale]/newagents/components/a2a/A2AServerSettingsPanel.tsx @@ -0,0 +1,176 @@ +"use client"; + +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + Button, + Tag, + message, +} from "antd"; +import { Copy, CheckCircle, Info } from "lucide-react"; + +interface A2AServerSettingsPanelProps { + agentId: number; + agentName: string; + endpointId?: string; + versionName?: string; + description?: string; + modelName?: string; + a2aAgentCard?: { + endpoint_id: string; + name: string; + description?: string; + version?: string; + streaming?: boolean; + agent_card_url: string | null; + rest_endpoints: { + message_send: string; + message_stream: string; + tasks_get: string; + }; + jsonrpc_url: string; + jsonrpc_methods: string[]; + }; +} + +export default function A2AServerSettingsPanel({ + agentId, + agentName, + endpointId, + versionName, + description, + modelName, + a2aAgentCard, +}: A2AServerSettingsPanelProps) { + const { t } = useTranslation("common"); + const [messageApi, contextHolder] = message.useMessage(); + + const [copiedField, setCopiedField] = useState(null); + + // Build preview data from backend response (relative paths) + const previewData = a2aAgentCard ? { + endpointId: a2aAgentCard.endpoint_id, + // Backend returns relative paths like /nb/a2a/{endpoint_id}/... + agentCardUrl: a2aAgentCard.agent_card_url || "", + restEndpoints: a2aAgentCard.rest_endpoints, + jsonrpcUrl: a2aAgentCard.jsonrpc_url, + jsonrpcMethods: a2aAgentCard.jsonrpc_methods, + } : null; + + const handleCopy = (text: string, field: string) => { + navigator.clipboard.writeText(text); + setCopiedField(field); + messageApi.success(t("common.copied")); + setTimeout(() => setCopiedField(null), 2000); + }; + + const CopyButton = ({ + text, + field, + }: { + text: string; + field: string; + }) => ( + + + +
+ + {/* Add/Edit Nacos Config Form - Toggleable */} + {showAddNacosForm && ( + +
+ + + setNacosConfig({ ...nacosConfig, name: e.target.value }) + } + disabled={savingNacosConfig} + /> + + + + + setNacosConfig({ ...nacosConfig, nacos_addr: e.target.value }) + } + disabled={savingNacosConfig} + /> + + + + + setNacosConfig({ ...nacosConfig, namespace_id: e.target.value }) + } + disabled={savingNacosConfig} + /> + + + + + setNacosConfig({ ...nacosConfig, username: e.target.value }) + } + disabled={savingNacosConfig} + /> + + + + + setNacosConfig({ ...nacosConfig, password: e.target.value }) + } + disabled={savingNacosConfig} + /> + + +
+ + + +
+ +
+ )} + +
+ record.config_id === selectedNacosConfigId ? "bg-blue-50" : "" + } + onRow={(record) => ({ + onClick: () => setSelectedNacosConfigId(record.config_id), + style: { cursor: "pointer" }, + })} + /> + + + {/* Scan Section - Only show when config is selected */} + {selectedNacosConfigId && ( + +
+ + updateAgent({ group_ids: vals })} + allowClear + /> + + + + {/* In-group Permission */} +
+ + { + const questions = [...exampleQuestions]; + questions[index] = event.target.value; + updateAgentConfig({ example_questions: questions }); + }} + /> + + + + + updateAgent({ display_name: e.target.value })} + /> + + + + + updateAgent({ name: e.target.value })} + /> + + + + + + + + updateAgent({ author: e.target.value })} + /> + + + + + + updateAgent({ description: e.target.value })} + /> + + + + {/* Right: icon upload */} + + +
+ {t("agent.field.icon")} +
+
+ + {editedAgent.display_name?.[0] ?? "A"} + +
+ +
+
+
+ {t("agent.field.iconHint")} +
+
+ + + + ); +} diff --git a/frontend/app/[locale]/newagents/components/agent-prompt.tsx b/frontend/app/[locale]/newagents/components/agent-prompt.tsx new file mode 100644 index 000000000..395646235 --- /dev/null +++ b/frontend/app/[locale]/newagents/components/agent-prompt.tsx @@ -0,0 +1,114 @@ +"use client"; + +import { useTranslation } from "react-i18next"; +import { Form, Input, Select, Row, Col } from "antd"; + +import { useAgentConfigStore } from "@/stores/agentConfigStore"; +import { useModelList } from "@/hooks/model/useModelList"; +import { canManageModels } from "@/lib/auth"; +import { useAuthorizationContext } from "@/components/providers/AuthorizationProvider"; +import { useDeployment } from "@/components/providers/deploymentProvider"; +import { useMemo } from "react"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; + +const { TextArea } = Input; + +export default function AgentPrompt() { + const { t } = useTranslation("common"); + const { user } = useAuthorizationContext(); + const { models } = useModelList(); + const { isSpeedMode } = useDeployment(); + const editedAgent = useAgentConfigStore((state) => state.editedAgent); + const updateAgent = useAgentConfigStore((state) => state.updateAgentConfig); + const defaultLlmConfig = useAgentConfigStore((state) => state.defaultLlmConfig); + + const modelOptions = useMemo(() => { + return (models ?? []).map((m: any) => ({ + value: m.id, + label: m.display_name ?? m.name, + model_name: m.name, + })); + }, [models]); + + const canManage = canManageModels(user?.role ?? ""); + + return ( +
+ {/* Model Selection */} + +
+ +