From 8732d1463aa10a3b1fd96816c297ab61b23ad569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=9B=90=E7=B2=92=20Yanli?= Date: Thu, 18 Jun 2026 23:34:51 +0800 Subject: [PATCH] chore(agent-v2): sync nightly updates to main (2026-06-18) (#37610) Co-authored-by: Joel Co-authored-by: yyh --- .../__tests__/menu-dialog.spec.tsx | 1 + .../header/account-setting/menu-dialog.tsx | 1 - .../hooks/__tests__/use-workflow.spec.ts | 63 ++- .../workflow/hooks/use-checklist.ts | 9 +- .../workflow/hooks/use-nodes-interactions.ts | 7 +- .../workflow/hooks/use-nodes-meta-data.ts | 4 +- .../components/workflow/hooks/use-workflow.ts | 11 +- .../change-block-menu-trigger.tsx | 4 +- .../nodes/_base/components/next-step/add.tsx | 3 +- .../_base/components/next-step/operator.tsx | 6 +- .../nodes/_base/components/node-handle.tsx | 5 +- .../_base/components/workflow-panel/index.tsx | 6 +- .../nodes/agent-v2/__tests__/panel.spec.tsx | 1 + .../agent-orchestrate-drawer-panel.tsx | 3 + .../components/agent-roster-field.tsx | 5 +- .../workflow/nodes/agent-v2/panel.tsx | 2 + .../workflow/utils/__tests__/node.spec.ts | 27 ++ web/app/components/workflow/utils/node.ts | 6 + .../agent-composer/__tests__/store.spec.ts | 30 +- .../agent-v2/agent-composer/conversions.ts | 12 +- .../agent-v2/agent-composer/form-state.ts | 1 + .../use-agent-configure-sync.spec.tsx | 14 + .../__tests__/empty-sections.spec.tsx | 2 +- .../__tests__/publish-bar.spec.tsx | 173 ++++++-- .../orchestrate/common/configurable-item.tsx | 8 +- .../files/__tests__/file-icon.spec.ts | 34 ++ .../files/__tests__/index.spec.tsx | 366 ++++++++++++++++- .../orchestrate/files/api-context.ts | 7 + .../components/orchestrate/files/file-icon.ts | 81 ++++ .../components/orchestrate/files/index.tsx | 260 ++++++++++-- .../orchestrate/files/upload-dialog.tsx | 123 +++--- .../components/orchestrate/index.tsx | 18 +- .../knowledge/__tests__/index.spec.tsx | 26 ++ .../orchestrate/knowledge/dialog.tsx | 2 +- .../orchestrate/prompt-editor/index.tsx | 6 +- .../orchestrate/publish-bar/index.tsx | 63 ++- .../publish-bar/publish-impact-popover.tsx | 148 ++++--- .../skills/__tests__/index.spec.tsx | 76 +++- .../orchestrate/skills/detail-dialog.tsx | 86 +++- .../components/orchestrate/skills/item.tsx | 91 ++++- .../agent-v2/agent-detail/configure/page.tsx | 1 + .../configure/use-agent-configure-sync.ts | 12 + .../logs/components/logs-table.tsx | 303 ++++++++++++++ .../logs/components/source-cell.tsx | 28 ++ .../logs/components/source-icon.tsx | 33 ++ .../logs/components/source-picker.tsx | 184 +++++++++ .../agent-v2/agent-detail/logs/mock-data.ts | 376 ------------------ .../agent-v2/agent-detail/logs/page.tsx | 318 ++++----------- web/features/agent-v2/agent-detail/page.tsx | 2 +- .../__tests__/agent-roster-list.spec.tsx | 88 +++- .../__tests__/create-agent-dialog.spec.tsx | 3 +- .../__tests__/edit-agent-dialog.spec.tsx | 9 +- .../roster/components/agent-roster-list.tsx | 46 ++- .../roster/components/create-agent-dialog.tsx | 3 - .../roster/components/edit-agent-dialog.tsx | 3 - web/i18n/ar-TN/agent-v-2.json | 14 +- web/i18n/de-DE/agent-v-2.json | 14 +- web/i18n/en-US/agent-v-2.json | 14 +- web/i18n/es-ES/agent-v-2.json | 14 +- web/i18n/fa-IR/agent-v-2.json | 14 +- web/i18n/fr-FR/agent-v-2.json | 14 +- web/i18n/hi-IN/agent-v-2.json | 14 +- web/i18n/id-ID/agent-v-2.json | 14 +- web/i18n/it-IT/agent-v-2.json | 14 +- web/i18n/ja-JP/agent-v-2.json | 14 +- web/i18n/ko-KR/agent-v-2.json | 14 +- web/i18n/nl-NL/agent-v-2.json | 14 +- web/i18n/pl-PL/agent-v-2.json | 14 +- web/i18n/pt-BR/agent-v-2.json | 14 +- web/i18n/ro-RO/agent-v-2.json | 14 +- web/i18n/ru-RU/agent-v-2.json | 14 +- web/i18n/sl-SI/agent-v-2.json | 14 +- web/i18n/th-TH/agent-v-2.json | 14 +- web/i18n/tr-TR/agent-v-2.json | 14 +- web/i18n/uk-UA/agent-v-2.json | 14 +- web/i18n/vi-VN/agent-v-2.json | 14 +- web/i18n/zh-Hans/agent-v-2.json | 14 +- web/i18n/zh-Hant/agent-v-2.json | 14 +- web/service/client.spec.ts | 34 ++ web/service/client.ts | 24 ++ 80 files changed, 2618 insertions(+), 962 deletions(-) create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/file-icon.spec.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/files/api-context.ts create mode 100644 web/features/agent-v2/agent-detail/configure/components/orchestrate/files/file-icon.ts create mode 100644 web/features/agent-v2/agent-detail/logs/components/logs-table.tsx create mode 100644 web/features/agent-v2/agent-detail/logs/components/source-cell.tsx create mode 100644 web/features/agent-v2/agent-detail/logs/components/source-icon.tsx create mode 100644 web/features/agent-v2/agent-detail/logs/components/source-picker.tsx delete mode 100644 web/features/agent-v2/agent-detail/logs/mock-data.ts diff --git a/web/app/components/header/account-setting/__tests__/menu-dialog.spec.tsx b/web/app/components/header/account-setting/__tests__/menu-dialog.spec.tsx index b69b9e89b2c..d202ee7964a 100644 --- a/web/app/components/header/account-setting/__tests__/menu-dialog.spec.tsx +++ b/web/app/components/header/account-setting/__tests__/menu-dialog.spec.tsx @@ -17,6 +17,7 @@ describe('MenuDialog', () => { // Assert expect(screen.getByTestId('dialog-content')).toBeInTheDocument() + expect(screen.getByRole('dialog').children).toHaveLength(1) }) it('should not render children when show is false', () => { diff --git a/web/app/components/header/account-setting/menu-dialog.tsx b/web/app/components/header/account-setting/menu-dialog.tsx index 9c9ec81db19..e0aef1ef484 100644 --- a/web/app/components/header/account-setting/menu-dialog.tsx +++ b/web/app/components/header/account-setting/menu-dialog.tsx @@ -35,7 +35,6 @@ const MenuDialog = ({ className, )} > -
{children} diff --git a/web/app/components/workflow/hooks/__tests__/use-workflow.spec.ts b/web/app/components/workflow/hooks/__tests__/use-workflow.spec.ts index 2ae8bec6aea..d9ed2437126 100644 --- a/web/app/components/workflow/hooks/__tests__/use-workflow.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-workflow.spec.ts @@ -1,12 +1,15 @@ +import type { NodeDefault } from '../../types' import { act, renderHook } from '@testing-library/react' import { createNode } from '../../__tests__/fixtures' import { baseRunningData, renderWorkflowFlowHook, renderWorkflowHook } from '../../__tests__/workflow-test-env' -import { WorkflowRunningStatus } from '../../types' +import { BlockClassificationEnum } from '../../block-selector/types' +import { BlockEnum, WorkflowRunningStatus } from '../../types' import { useIsChatMode, useIsNodeInIteration, useIsNodeInLoop, useNodesReadOnly, + useWorkflow, useWorkflowReadOnly, } from '../use-workflow' @@ -20,6 +23,20 @@ beforeEach(() => { mockAppMode = 'workflow' }) +function createNodeDefault(type: BlockEnum): NodeDefault { + return { + metaData: { + classification: BlockClassificationEnum.Default, + sort: 0, + type, + title: type, + author: 'test', + }, + defaultValue: {}, + checkValid: () => ({ isValid: true }), + } +} + // --------------------------------------------------------------------------- // useIsChatMode // --------------------------------------------------------------------------- @@ -169,6 +186,50 @@ describe('useNodesReadOnly', () => { }) }) +// --------------------------------------------------------------------------- +// useWorkflow connection validation +// --------------------------------------------------------------------------- + +describe('useWorkflow connection validation', () => { + it('should validate Agent V2 graph nodes against the Agent V2 catalog type', () => { + const agentNode = createNode({ + id: 'agent-v2', + data: { + type: BlockEnum.Agent, + title: 'Agent', + desc: '', + agent_node_kind: 'dify_agent', + version: '2', + }, + }) + const codeNode = createNode({ + id: 'code', + data: { + type: BlockEnum.Code, + title: 'Code', + desc: '', + }, + }) + + const { result } = renderWorkflowFlowHook(() => useWorkflow(), { + nodes: [agentNode, codeNode], + edges: [], + hooksStoreProps: { + availableNodesMetaData: { + nodes: [createNodeDefault(BlockEnum.AgentV2), createNodeDefault(BlockEnum.Code)], + }, + }, + }) + + expect(result.current.isValidConnection({ + source: 'agent-v2', + sourceHandle: 'source', + target: 'code', + targetHandle: 'target', + })).toBe(true) + }) +}) + // --------------------------------------------------------------------------- // useIsNodeInIteration // --------------------------------------------------------------------------- diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 643823aab6a..299fa4794b0 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -66,6 +66,7 @@ import { import { BlockEnum } from '../types' import { getDataSourceCheckParams, + getNodeCatalogType, getToolCheckParams, getValidTreeNodes, } from '../utils' @@ -99,10 +100,6 @@ const withFlowType = (moreDataForCheckValid: CheckValidExtraData, flowType?: Flo } } -const getNodeMetaType = (data: CommonNodeType) => { - return isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type -} - const START_NODE_TYPES: BlockEnum[] = [ BlockEnum.Start, BlockEnum.TriggerSchedule, @@ -272,7 +269,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType? if (node!.type === CUSTOM_NODE) { const checkData = getCheckData(node!.data) - const validator = nodesExtraData?.[getNodeMetaType(node!.data) as BlockEnum]?.checkValid + const validator = nodesExtraData?.[getNodeCatalogType(node!.data)]?.checkValid const isPluginMissing = isNodePluginMissing(node!.data, { builtInTools: buildInTools, customTools, workflowTools, mcpTools, triggerPlugins, dataSourceList }) const errorMessages: string[] = [] @@ -556,7 +553,7 @@ export const useChecklistBeforePublish = () => { } const checkData = getCheckData(node!.data, datasets, embeddingProviderModelMap) - const { errorMessage } = nodesExtraData![getNodeMetaType(node!.data) as BlockEnum].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType)) + const { errorMessage } = nodesExtraData![getNodeCatalogType(node!.data)].checkValid(checkData, t, withFlowType(moreDataForCheckValid, flowType)) if (errorMessage) { toast.error(`[${node!.data.title}] ${errorMessage}`) diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 4ef36285824..538c270692c 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -50,6 +50,7 @@ import { generateNewNode, genNewNodeTitleFromOld, getNestedNodePosition, + getNodeCatalogType, getNodeCustomTypeByNodeDataType, getNodesConnectedSourceOrTargetHandleIdsMap, getNodesWithSameDefaultDataType, @@ -747,7 +748,7 @@ export const useNodesInteractions = () => { return if ( - nodesMetaDataMap?.[currentNode.data.type as BlockEnum]?.metaData + nodesMetaDataMap?.[getNodeCatalogType(currentNode.data)]?.metaData .isUndeletable ) { return @@ -1791,7 +1792,7 @@ export const useNodesInteractions = () => { if (node.type === CUSTOM_NOTE_NODE) return true - const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum] + const nodeMeta = nodesMetaDataMap?.[getNodeCatalogType(node.data)] if (!nodeMeta) return false @@ -1803,7 +1804,7 @@ export const useNodesInteractions = () => { if (node.type === CUSTOM_NOTE_NODE) return {} - const nodeMeta = nodesMetaDataMap?.[isAgentV2NodeData(node.data) ? BlockEnum.AgentV2 : node.data.type as BlockEnum] + const nodeMeta = nodesMetaDataMap?.[getNodeCatalogType(node.data)] return nodeMeta?.defaultValue }, [nodesMetaDataMap]) diff --git a/web/app/components/workflow/hooks/use-nodes-meta-data.ts b/web/app/components/workflow/hooks/use-nodes-meta-data.ts index bad25b87078..37ad7986571 100644 --- a/web/app/components/workflow/hooks/use-nodes-meta-data.ts +++ b/web/app/components/workflow/hooks/use-nodes-meta-data.ts @@ -3,9 +3,9 @@ import type { Node } from '@/app/components/workflow/types' import { useMemo } from 'react' import { CollectionType } from '@/app/components/tools/types' import { useHooksStore } from '@/app/components/workflow/hooks-store' -import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/types' import { useStore } from '@/app/components/workflow/store' import { BlockEnum } from '@/app/components/workflow/types' +import { getNodeCatalogType } from '@/app/components/workflow/utils/node' import { useGetLanguage } from '@/context/i18n' import { useAllBuiltInTools, @@ -33,7 +33,7 @@ export const useNodeMetaData = (node: Node) => { const dataSourceList = useStore(s => s.dataSourceList) const availableNodesMetaData = useNodesMetaData() const { data } = node - const nodeMetaData = availableNodesMetaData.nodesMap?.[isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type] + const nodeMetaData = availableNodesMetaData.nodesMap?.[getNodeCatalogType(data)] const author = useMemo(() => { if (data.type === BlockEnum.DataSource) return dataSourceList?.find(dataSource => dataSource.plugin_id === data.plugin_id)?.author diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 2e2a5318d8e..bf568f0fb6c 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -36,6 +36,7 @@ import { import { WorkflowRunningStatus, } from '../types' +import { getNodeCatalogType } from '../utils' import { getWorkflowEntryNode, isWorkflowEntryNode, @@ -360,13 +361,15 @@ export const useWorkflow = () => { return false if (sourceNode && targetNode) { - const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNode.data.type, !!sourceNode.parentId).availableNextBlocks - const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNode.data.type, !!targetNode.parentId).availablePrevBlocks + const sourceNodeCatalogType = getNodeCatalogType(sourceNode.data) + const targetNodeCatalogType = getNodeCatalogType(targetNode.data) + const sourceNodeAvailableNextNodes = getAvailableBlocks(sourceNodeCatalogType, !!sourceNode.parentId).availableNextBlocks + const targetNodeAvailablePrevNodes = getAvailableBlocks(targetNodeCatalogType, !!targetNode.parentId).availablePrevBlocks - if (!sourceNodeAvailableNextNodes.includes(targetNode.data.type)) + if (!sourceNodeAvailableNextNodes.includes(targetNodeCatalogType)) return false - if (!targetNodeAvailablePrevNodes.includes(sourceNode.data.type)) + if (!targetNodeAvailablePrevNodes.includes(sourceNodeCatalogType)) return false } diff --git a/web/app/components/workflow/node-actions-menu/change-block-menu-trigger.tsx b/web/app/components/workflow/node-actions-menu/change-block-menu-trigger.tsx index 183d515cf41..2a6c6864c93 100644 --- a/web/app/components/workflow/node-actions-menu/change-block-menu-trigger.tsx +++ b/web/app/components/workflow/node-actions-menu/change-block-menu-trigger.tsx @@ -15,6 +15,7 @@ import { import { useHooksStore } from '@/app/components/workflow/hooks-store' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { BlockEnum, isTriggerNode } from '@/app/components/workflow/types' +import { getNodeCatalogType } from '@/app/components/workflow/utils' import { FlowType } from '@/types/common' type ChangeBlockMenuTriggerProps = { @@ -30,10 +31,11 @@ export function ChangeBlockMenuTrigger({ }: ChangeBlockMenuTriggerProps) { const { t } = useTranslation() const { handleNodeChange } = useNodesInteractions() + const nodeCatalogType = getNodeCatalogType(nodeData) const { availablePrevBlocks, availableNextBlocks, - } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop) + } = useAvailableBlocks(nodeCatalogType, nodeData.isInIteration || nodeData.isInLoop) const isChatMode = useIsChatMode() const flowType = useHooksStore(s => s.configsMap?.flowType) const nodes = useNodes() diff --git a/web/app/components/workflow/nodes/_base/components/next-step/add.tsx b/web/app/components/workflow/nodes/_base/components/next-step/add.tsx index c890aab83de..4d85aa31de6 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/add.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/add.tsx @@ -18,6 +18,7 @@ import { useNodesInteractions, useNodesReadOnly, } from '@/app/components/workflow/hooks' +import { getNodeCatalogType } from '@/app/components/workflow/utils' type AddProps = { nodeId: string @@ -37,7 +38,7 @@ const Add = ({ const [open, setOpen] = useState(false) const { handleNodeAdd } = useNodesInteractions() const { nodesReadOnly } = useNodesReadOnly() - const { availableNextBlocks } = useAvailableBlocks(nodeData.type, nodeData.isInIteration || nodeData.isInLoop) + const { availableNextBlocks } = useAvailableBlocks(getNodeCatalogType(nodeData), nodeData.isInIteration || nodeData.isInLoop) const handleSelect = useCallback((type, pluginDefaultValue) => { handleNodeAdd( diff --git a/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx b/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx index c24d7aa8f4f..2bf9ad4330b 100644 --- a/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx +++ b/web/app/components/workflow/nodes/_base/components/next-step/operator.tsx @@ -18,6 +18,7 @@ import { useAvailableBlocks, useNodesInteractions, } from '@/app/components/workflow/hooks' +import { getNodeCatalogType } from '@/app/components/workflow/utils' type ChangeItemProps = { data: CommonNodeType @@ -32,10 +33,11 @@ const ChangeItem = ({ const { t } = useTranslation() const { handleNodeChange } = useNodesInteractions() + const nodeCatalogType = getNodeCatalogType(data) const { availablePrevBlocks, availableNextBlocks, - } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop) + } = useAvailableBlocks(nodeCatalogType, data.isInIteration || data.isInLoop) const handleSelect = useCallback((type, pluginDefaultValue) => { handleNodeChange(nodeId, type, sourceHandle, pluginDefaultValue) @@ -59,7 +61,7 @@ const ChangeItem = ({ }} trigger={renderTrigger} popupClassName="w-[328px]!" - availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== data.type)} + availableBlocksTypes={intersection(availablePrevBlocks, availableNextBlocks).filter(item => item !== nodeCatalogType)} /> ) } diff --git a/web/app/components/workflow/nodes/_base/components/node-handle.tsx b/web/app/components/workflow/nodes/_base/components/node-handle.tsx index c0c7e8388d2..896e56f2b10 100644 --- a/web/app/components/workflow/nodes/_base/components/node-handle.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-handle.tsx @@ -28,6 +28,7 @@ import { BlockEnum, NodeRunningStatus, } from '../../../types' +import { getNodeCatalogType } from '../../../utils' type NodeHandleProps = { handleId: string @@ -57,7 +58,7 @@ export const NodeTargetHandle = memo(({ const { handleNodeAdd } = useNodesInteractions() const { getNodesReadOnly } = useNodesReadOnly() const connected = data._connectedTargetHandleIds?.includes(handleId) - const { availablePrevBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop) + const { availablePrevBlocks } = useAvailableBlocks(getNodeCatalogType(data), data.isInIteration || data.isInLoop) const isConnectable = !!availablePrevBlocks.length const handleOpenChange = useCallback((v: boolean) => { @@ -147,7 +148,7 @@ export const NodeSourceHandle = memo(({ const workflowStoreApi = useWorkflowStore() const { handleNodeAdd } = useNodesInteractions() const { getNodesReadOnly } = useNodesReadOnly() - const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop) + const { availableNextBlocks } = useAvailableBlocks(getNodeCatalogType(data), data.isInIteration || data.isInLoop) const isConnectable = !!availableNextBlocks.length const isChatMode = useIsChatMode() const shouldAutoOpen = shouldAutoOpenStartNodeSelector && canAutoOpenStartNodeSelector(data.type, isChatMode) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 92103974a62..51a7756e460 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -58,13 +58,13 @@ import { useHooksStore } from '@/app/components/workflow/hooks-store' import useInspectVarsCrud from '@/app/components/workflow/hooks/use-inspect-vars-crud' import { NodeActionsDropdown } from '@/app/components/workflow/node-actions-menu' import Split from '@/app/components/workflow/nodes/_base/components/split' -import { isAgentV2NodeData } from '@/app/components/workflow/nodes/agent-v2/types' import { useLogs } from '@/app/components/workflow/run/hooks' import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' import { useStore } from '@/app/components/workflow/store' import { BlockEnum, NodeRunningStatus } from '@/app/components/workflow/types' import { canRunBySingle, + getNodeCatalogType, hasErrorHandleNode, hasRetryNode, isSupportCustomRunForm, @@ -210,7 +210,7 @@ const BasePanel: FC = ({ const { handleNodeSelect } = useNodesInteractions() const { nodesReadOnly } = useNodesReadOnly() - const { availableNextBlocks } = useAvailableBlocks(data.type, data.isInIteration || data.isInLoop) + const { availableNextBlocks } = useAvailableBlocks(getNodeCatalogType(data), data.isInIteration || data.isInLoop) const toolIcon = useToolIcon(data) const { saveStateToHistory } = useWorkflowHistory() @@ -230,7 +230,7 @@ const BasePanel: FC = ({ }, [handleNodeDataUpdateWithSyncDraft, id, saveStateToHistory]) const isChildNode = !!(data.isInIteration || data.isInLoop) - const nodeMetaType = isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type + const nodeMetaType = getNodeCatalogType(data) const isSupportSingleRun = canRunBySingle(data.type, isChildNode) const appDetail = useAppStore(state => state.appDetail) diff --git a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx index 6212068fe96..522e53235f1 100644 --- a/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx +++ b/web/app/components/workflow/nodes/agent-v2/__tests__/panel.spec.tsx @@ -136,6 +136,7 @@ vi.mock('../hooks', () => ({ vi.mock('../components/agent-orchestrate-drawer-panel', () => ({ AgentOrchestrateDrawerPanel: (props: { agentId: string + appId?: string inlineComposerState?: unknown isInline: boolean nodeId: string diff --git a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx index bae800df611..f04899116b0 100644 --- a/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx +++ b/web/app/components/workflow/nodes/agent-v2/components/agent-orchestrate-drawer-panel.tsx @@ -16,6 +16,7 @@ import { useWorkflowInlineAgentConfigureSync } from '../agent-soul-config' type AgentOrchestrateDrawerPanelProps = { agentId: string + appId?: string inlineComposerState?: WorkflowAgentComposerResponse isInline: boolean nodeId: string @@ -32,6 +33,7 @@ export function AgentOrchestrateDrawerPanel(props: AgentOrchestrateDrawerPanelPr function AgentOrchestrateDrawerPanelContent({ agentId, + appId, inlineComposerState, isInline, nodeId, @@ -76,6 +78,7 @@ function AgentOrchestrateDrawerPanelContent({ return ( + showAccessIcon?: boolean showConsoleLink?: boolean showDetailActions?: boolean onClose: () => void @@ -144,7 +146,7 @@ function AgentRosterDrawer({ {title} - {!isSetup && } + {!isSetup && showAccessIcon && }
{description && (

@@ -341,6 +343,7 @@ export function AgentRosterField({ mode={panelMode} open={panelOpen} portalContainerRef={portalContainerRef} + showAccessIcon={!isInlineSetup} showDetailActions={showPanelDetailActions} onClose={() => setPanelOpen(false)} > diff --git a/web/app/components/workflow/nodes/agent-v2/panel.tsx b/web/app/components/workflow/nodes/agent-v2/panel.tsx index 371b2d847ee..c01c46fe513 100644 --- a/web/app/components/workflow/nodes/agent-v2/panel.tsx +++ b/web/app/components/workflow/nodes/agent-v2/panel.tsx @@ -33,6 +33,7 @@ export function AgentV2Panel({ const { handleNodeDataUpdate, handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() const openInlineAgentPanelNodeId = useStore(state => state.openInlineAgentPanelNodeId) const setOpenInlineAgentPanelNodeId = useStore(state => state.setOpenInlineAgentPanelNodeId) + const appId = useStore(state => state.appId) const drawerPortalContainerRef = useRef(null) const declaredOutputs = getAgentV2DeclaredOutputs(inputs) const rosterAgentId = inputs.agent_binding?.binding_type === 'roster_agent' ? inputs.agent_binding.agent_id : undefined @@ -175,6 +176,7 @@ export function AgentV2Panel({ ? ( { + it('should use Agent V2 catalog type for graph agent nodes with the Agent V2 discriminator', () => { + expect(getNodeCatalogType({ + title: 'Agent', + desc: '', + type: BlockEnum.Agent, + agent_node_kind: 'dify_agent', + version: '2', + } as CommonNodeType)).toBe(BlockEnum.AgentV2) + }) + + it('should keep the graph node type for regular nodes and legacy Agent nodes', () => { + expect(getNodeCatalogType({ + title: 'Code', + desc: '', + type: BlockEnum.Code, + })).toBe(BlockEnum.Code) + + expect(getNodeCatalogType({ + title: 'Agent', + desc: '', + type: BlockEnum.Agent, + })).toBe(BlockEnum.Agent) + }) +}) + describe('generateNewNode', () => { it('should create a basic node with default CUSTOM_NODE type', () => { const { newNode } = generateNewNode({ diff --git a/web/app/components/workflow/utils/node.ts b/web/app/components/workflow/utils/node.ts index 7432591f899..d319b020058 100644 --- a/web/app/components/workflow/utils/node.ts +++ b/web/app/components/workflow/utils/node.ts @@ -1,6 +1,7 @@ import type { IterationNodeType } from '../nodes/iteration/types' import type { LoopNodeType } from '../nodes/loop/types' import type { + CommonNodeType, Node, } from '../types' import { @@ -14,12 +15,17 @@ import { LOOP_CHILDREN_Z_INDEX, LOOP_NODE_Z_INDEX, } from '../constants' +import { isAgentV2NodeData } from '../nodes/agent-v2/types' import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants' import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants' import { BlockEnum, } from '../types' +export function getNodeCatalogType(data: CommonNodeType): BlockEnum { + return isAgentV2NodeData(data) ? BlockEnum.AgentV2 : data.type +} + export function generateNewNode({ data, position, id, zIndex, type, ...rest }: Omit & { id?: string }): { newNode: Node newIterationStartNode?: Node diff --git a/web/features/agent-v2/agent-composer/__tests__/store.spec.ts b/web/features/agent-v2/agent-composer/__tests__/store.spec.ts index 623a00f474d..302d04c2888 100644 --- a/web/features/agent-v2/agent-composer/__tests__/store.spec.ts +++ b/web/features/agent-v2/agent-composer/__tests__/store.spec.ts @@ -22,6 +22,7 @@ describe('agent composer store conversions', () => { id: 'secret-1', key: 'OPENAI_API_KEY', ref: 'credential-1', + value: 'credential-1', }, ], }, @@ -155,6 +156,7 @@ describe('agent composer store conversions', () => { key: 'OPENAI_API_KEY', masked: true, scope: 'secret', + value: 'credential-1', }), ], }) @@ -229,12 +231,36 @@ describe('agent composer store conversions', () => { secret_refs: [ expect.objectContaining({ key: 'OPENAI_API_KEY', - ref: 'secret-1', + value: 'credential-1', }), ], }) }) + it('should hydrate legacy secret refs from ref when value is absent', () => { + const formState = agentSoulConfigToFormState({ + env: { + secret_refs: [ + { + id: 'secret-1', + key: 'OPENAI_API_KEY', + ref: 'credential-legacy', + }, + ], + }, + }) + + expect(formState.envVariables).toEqual([ + { + id: 'secret-1', + key: 'OPENAI_API_KEY', + masked: true, + scope: 'secret', + value: 'credential-legacy', + }, + ]) + }) + it('should keep unauthorized credential type when no-auth tool settings change', () => { const publishConfig = formStateToAgentSoulConfig({ formState: { @@ -419,7 +445,7 @@ describe('agent composer store conversions', () => { id: 'valid-secret', key: 'OPENAI_API_KEY', name: 'OPENAI_API_KEY', - ref: 'valid-secret', + value: '********', variable: 'OPENAI_API_KEY', }, ], diff --git a/web/features/agent-v2/agent-composer/conversions.ts b/web/features/agent-v2/agent-composer/conversions.ts index d194b63af3f..3411bd6556c 100644 --- a/web/features/agent-v2/agent-composer/conversions.ts +++ b/web/features/agent-v2/agent-composer/conversions.ts @@ -78,10 +78,12 @@ const toFileFormState = (config?: AgentSoulConfig): AgentFileNode[] => ( id, name: file.name ?? id, icon: toFileIcon(file), + driveKey: file.drive_key ?? undefined, }] }) const toFileRefs = (files: AgentFileNode[]) => flattenFileNodes(files).map(file => ({ + ...(file.driveKey ? { drive_key: file.driveKey } : {}), id: file.id, name: file.name, type: file.icon, @@ -289,10 +291,11 @@ const toCliEnvVariables = (tool: AgentSoulCliToolConfig): EnvVariable[] => [ }), ...(tool.env?.secret_refs ?? []).map((secret): EnvVariable => { const key = secret.key ?? secret.name ?? secret.variable ?? secret.env_name ?? '' + const value = secret.value ?? secret.ref ?? secret.credential_id ?? '' return { id: secret.id ?? secret.ref ?? secret.credential_id ?? key, key, - value: '••••••••••••', + value, scope: 'secret', masked: true, } @@ -352,7 +355,7 @@ const toCliToolConfigs = (tools: AgentTool[]) => tools.flatMap((tool) => { id: variable.id, key: variable.key.trim(), name: variable.key.trim(), - ref: variable.id, + value: variable.value, variable: variable.key.trim(), })), }, @@ -376,10 +379,11 @@ const toEnvVariableFormState = (config?: AgentSoulConfig): EnvVariable[] => [ }), ...(config?.env?.secret_refs ?? []).map((secret): EnvVariable => { const key = secret.key ?? secret.name ?? secret.variable ?? secret.env_name ?? '' + const value = secret.value ?? secret.ref ?? secret.credential_id ?? '' return { id: secret.id ?? secret.ref ?? secret.credential_id ?? key, key, - value: '••••••••••••', + value, scope: 'secret', masked: true, } @@ -402,7 +406,7 @@ const toEnvConfig = (variables: EnvVariable[]): AgentSoulConfig['env'] => ({ id: variable.id, key: variable.key.trim(), name: variable.key.trim(), - ref: variable.id, + value: variable.value, variable: variable.key.trim(), })), }) diff --git a/web/features/agent-v2/agent-composer/form-state.ts b/web/features/agent-v2/agent-composer/form-state.ts index 26ad4b39501..e75c3491f91 100644 --- a/web/features/agent-v2/agent-composer/form-state.ts +++ b/web/features/agent-v2/agent-composer/form-state.ts @@ -39,6 +39,7 @@ export type AgentFileNode = { id: string name: string icon: FileTreeIconType + driveKey?: string children?: AgentFileNode[] } diff --git a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx index 787120dd3cf..cde113dda3d 100644 --- a/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/__tests__/use-agent-configure-sync.spec.tsx @@ -38,6 +38,12 @@ vi.mock('@/service/client', () => ({ consoleQuery: { agent: { byAgentId: { + get: { + queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ + 'agent-detail', + input.params.agent_id, + ], + }, composer: { get: { queryKey: ({ input }: { input: { params: { agent_id: string } } }) => [ @@ -165,6 +171,10 @@ describe('useAgentConfigureSync', () => { it('should publish only when publishDraft is called explicitly', async () => { const { queryClient, result } = renderUseAgentConfigureSync() + queryClient.setQueryData(['agent-detail', 'agent-1'], { + active_config_is_published: false, + name: 'Agent', + }) await act(async () => { await result.current.publishDraft({ @@ -198,5 +208,9 @@ describe('useAgentConfigureSync', () => { }, }, }) + expect(queryClient.getQueryData(['agent-detail', 'agent-1'])).toEqual({ + active_config_is_published: true, + name: 'Agent', + }) }) }) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/empty-sections.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/empty-sections.spec.tsx index 2660d6d3aec..ef583288a07 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/empty-sections.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/empty-sections.spec.tsx @@ -23,7 +23,7 @@ function renderEmptySections() { }} > - + diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx index d5f3a4dec13..5e4fe175882 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/__tests__/publish-bar.spec.tsx @@ -1,7 +1,8 @@ -import type { AgentConfigSnapshotSummaryResponse, AgentPublishedReferenceResponse } from '@dify/contracts/api/console/agent/types.gen' +import type { AgentConfigSnapshotSummaryResponse, AgentReferencingWorkflowResponse } from '@dify/contracts/api/console/agent/types.gen' import type { ComponentProps } from 'react' import type { Mock } from 'vitest' -import { fireEvent, render, screen, within } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { createStore, Provider as JotaiProvider } from 'jotai' import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' import { agentComposerDraftAtom, agentComposerOriginalDraftAtom, agentComposerPublishedDraftAtom } from '@/features/agent-v2/agent-composer/store' @@ -18,6 +19,9 @@ const hotkeyRegistrations = vi.hoisted(() => new Map vi.fn((hotkey: string) => `display:${hotkey}`)) const mockFormatTimeFromNow = vi.hoisted(() => vi.fn(() => 'just now')) +const workflowReferences = vi.hoisted(() => ({ + data: [] as AgentReferencingWorkflowResponse[], +})) vi.mock('@tanstack/react-hotkeys', async (importOriginal) => { const actual = await importOriginal() @@ -36,8 +40,28 @@ vi.mock('@/hooks/use-format-time-from-now', () => ({ }), })) +vi.mock('@/service/client', () => ({ + consoleQuery: { + agent: { + byAgentId: { + referencingWorkflows: { + get: { + queryOptions: ({ input }: { input: { params: { agent_id: string } } }) => ({ + queryKey: ['agent-referencing-workflows', input], + queryFn: async () => ({ + data: workflowReferences.data, + }), + }), + }, + }, + }, + }, + }, +})) + vi.mock('@langgenius/dify-ui/popover', async () => { const React = await import('react') + const ReactDOM = await import('react-dom') const PopoverContext = React.createContext<{ open: boolean onOpenChange?: (open: boolean) => void @@ -68,7 +92,7 @@ vi.mock('@langgenius/dify-ui/popover', async () => { if (!context.open) return null - return

{children}
+ return ReactDOM.createPortal(
{children}
, document.body) }, PopoverTrigger: ({ render: trigger, @@ -96,60 +120,73 @@ const originalDraftWithFile = { ], } satisfies typeof defaultAgentSoulConfigFormState -const publishedReferences: AgentPublishedReferenceResponse[] = [ +const publishedReferences: AgentReferencingWorkflowResponse[] = [ { app_id: 'app-python', app_mode: 'workflow', app_name: 'Python bug fixer', workflow_id: 'workflow-python', workflow_version: '1', + node_ids: ['node-python'], }, { app_id: 'app-translation', + app_icon: 'T', + app_icon_background: '#E0F2FE', + app_icon_type: 'emoji', app_mode: 'workflow', app_name: 'Translation Workflow', workflow_id: 'workflow-translation', workflow_version: '1', + node_ids: ['node-translation'], }, ] function renderPublishBar({ + activeConfigIsPublished, activeConfigSnapshot, draftSavedAt, isPublishing, onPublish = vi.fn(), prompt = '', - publishedReferenceCount, - publishedReferences, setupStore, + usedByAppReferences = [], }: { + activeConfigIsPublished?: boolean activeConfigSnapshot?: AgentConfigSnapshotSummaryResponse | null draftSavedAt?: number isPublishing?: boolean onPublish?: PublishMock prompt?: string - publishedReferenceCount?: number - publishedReferences?: AgentPublishedReferenceResponse[] setupStore?: (store: ReturnType) => void + usedByAppReferences?: AgentReferencingWorkflowResponse[] } = {}) { + workflowReferences.data = usedByAppReferences + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) const store = createStore() store.set(agentComposerPromptAtom, prompt) setupStore?.(store) render( - - - , + + + + + , ) return { @@ -161,6 +198,7 @@ describe('AgentConfigurePublishBar', () => { beforeEach(() => { vi.clearAllMocks() hotkeyRegistrations.clear() + workflowReferences.data = [] vi.spyOn(console, 'log').mockImplementation(() => {}) }) @@ -191,7 +229,10 @@ describe('AgentConfigurePublishBar', () => { }) it('should render published state from the active snapshot and disable publish logic', () => { - const { onPublish } = renderPublishBar({ activeConfigSnapshot }) + const { onPublish } = renderPublishBar({ + activeConfigIsPublished: true, + activeConfigSnapshot, + }) expect(screen.getByText('agentV2.agentDetail.configure.publishBar.upToDate')).toBeInTheDocument() expect(screen.getByText(/agentV2\.agentDetail\.configure\.publishBar\.publishedAt/)).toBeInTheDocument() @@ -207,7 +248,28 @@ describe('AgentConfigurePublishBar', () => { expect(onPublish).not.toHaveBeenCalled() }) - it('should publish the current draft payload from the unpublished changes state', () => { + it('should initialize unpublished state when active config is not published', async () => { + const { onPublish } = renderPublishBar({ + activeConfigIsPublished: false, + activeConfigSnapshot, + }) + + expect(screen.getByText('agentV2.agentDetail.configure.publishBar.unpublishedChanges')).toBeInTheDocument() + expect(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ })).toBeInTheDocument() + expect(hotkeyRegistrations.get('Mod+Shift+P')?.options).toEqual( + expect.objectContaining({ enabled: true, ignoreInputs: false }), + ) + + fireEvent.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ })) + + await waitFor(() => { + expect(onPublish).toHaveBeenCalledWith(expect.objectContaining({ + agent_id: 'agent-1', + })) + }) + }) + + it('should publish the current draft payload from the unpublished changes state', async () => { const { onPublish } = renderPublishBar({ activeConfigSnapshot, prompt: 'Updated system prompt', @@ -217,14 +279,16 @@ describe('AgentConfigurePublishBar', () => { fireEvent.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ })) - expect(onPublish).toHaveBeenCalledWith(expect.objectContaining({ - agent_id: 'agent-1', - config_snapshot: expect.objectContaining({ - prompt: expect.objectContaining({ - system_prompt: 'Updated system prompt', + await waitFor(() => { + expect(onPublish).toHaveBeenCalledWith(expect.objectContaining({ + agent_id: 'agent-1', + config_snapshot: expect.objectContaining({ + prompt: expect.objectContaining({ + system_prompt: 'Updated system prompt', + }), }), - }), - })) + })) + }) }) it('should mark non-prompt draft changes as unpublished', () => { @@ -277,20 +341,27 @@ describe('AgentConfigurePublishBar', () => { ) }) - it('should show affected workflow references when clicking a publishable agent in use', () => { + it('should show affected workflow references when clicking a publishable agent in use', async () => { const { onPublish } = renderPublishBar({ activeConfigSnapshot, prompt: 'Updated system prompt', - publishedReferenceCount: 2, - publishedReferences, + usedByAppReferences: publishedReferences, }) expect(screen.queryByTestId('publish-impact-popover')).not.toBeInTheDocument() + const publishBar = screen.getByText('agentV2.agentDetail.configure.publishBar.unpublishedChanges').closest('[aria-hidden]') + expect(publishBar).toHaveAttribute('aria-hidden', 'false') fireEvent.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ })) expect(onPublish).not.toHaveBeenCalled() - expect(screen.getByTestId('publish-impact-popover')).toBeInTheDocument() + const impactPopover = await screen.findByTestId('publish-impact-popover') + expect(impactPopover).toBeInTheDocument() + expect(publishBar).toHaveAttribute('aria-hidden', 'false') + await waitFor(() => { + expect(publishBar).toHaveAttribute('aria-hidden', 'true') + expect(publishBar).toHaveClass('opacity-0') + }) expect(screen.getByText(/agentV2\.agentDetail\.configure\.publishImpact\.title/)).toBeInTheDocument() expect(screen.getByText(/agentV2\.agentDetail\.configure\.publishImpact\.descriptionPrefix/)).toBeInTheDocument() expect(screen.getByText(/agentV2\.agentDetail\.configure\.publishImpact\.workflowCount/)).toBeInTheDocument() @@ -298,18 +369,20 @@ describe('AgentConfigurePublishBar', () => { expect(screen.getByText('Translation Workflow')).toBeInTheDocument() expect(screen.getByRole('link', { name: 'Python bug fixer' })).toHaveAttribute('target', '_blank') expect(screen.getByRole('link', { name: 'Python bug fixer' })).toHaveAttribute('rel', 'noopener noreferrer') + expect(within(impactPopover).getByText('display:Mod')).toBeInTheDocument() + expect(within(impactPopover).getByText('display:Shift')).toBeInTheDocument() + expect(within(impactPopover).getByText('display:P')).toBeInTheDocument() }) - it('should publish from the affected workflow popover action', () => { + it('should publish from the affected workflow popover action', async () => { const { onPublish } = renderPublishBar({ activeConfigSnapshot, prompt: 'Updated system prompt', - publishedReferenceCount: 2, - publishedReferences, + usedByAppReferences: publishedReferences, }) fireEvent.click(screen.getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/ })) - fireEvent.click(within(screen.getByTestId('publish-impact-popover')).getByRole('button', { + fireEvent.click(within(await screen.findByTestId('publish-impact-popover')).getByRole('button', { name: /agentV2\.agentDetail\.configure\.publishBar\.publishUpdate/, })) @@ -317,4 +390,28 @@ describe('AgentConfigurePublishBar', () => { agent_id: 'agent-1', })) }) + + it('should open the affected workflow popover from the publish shortcut before publishing', async () => { + const { onPublish } = renderPublishBar({ + activeConfigSnapshot, + prompt: 'Updated system prompt', + usedByAppReferences: publishedReferences, + }) + const publishShortcut = hotkeyRegistrations.get('Mod+Shift+P') + + await act(async () => { + await publishShortcut?.callback({ preventDefault: vi.fn() }) + }) + + expect(onPublish).not.toHaveBeenCalled() + expect(await screen.findByTestId('publish-impact-popover')).toBeInTheDocument() + + await act(async () => { + await hotkeyRegistrations.get('Mod+Shift+P')?.callback({ preventDefault: vi.fn() }) + }) + + expect(onPublish).toHaveBeenCalledWith(expect.objectContaining({ + agent_id: 'agent-1', + })) + }) }) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/common/configurable-item.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/common/configurable-item.tsx index ee56d237cdd..337d7be8e32 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/common/configurable-item.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/common/configurable-item.tsx @@ -32,12 +32,12 @@ export function ConfigureSectionConfigurableItem({ {!readOnly && ( -
+
@@ -45,14 +45,14 @@ export function ConfigureSectionConfigurableItem({ type="button" aria-label={removeAriaLabel} onClick={onRemove} - className="flex size-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" + className="flex size-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive focus-visible:bg-state-destructive-hover focus-visible:text-text-destructive focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden" >
)} {hasBadge && ( - + {badge} )} diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/file-icon.spec.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/file-icon.spec.ts new file mode 100644 index 00000000000..28827f3c560 --- /dev/null +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/file-icon.spec.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest' +import { getDriveFileIconType, getFileIconType } from '../file-icon' + +describe('agent file icon helpers', () => { + it('should infer supported icons for uploaded drive file pointer kinds', () => { + expect(getDriveFileIconType({ + fileKind: 'upload_file', + fileName: 'report.md', + mimeType: 'text/markdown', + })).toBe('markdown') + expect(getDriveFileIconType({ + fileKind: 'tool_file', + fileName: 'image.png', + mimeType: 'image/png', + })).toBe('image') + }) + + it('should keep supported drive file kinds and normalize directories', () => { + expect(getDriveFileIconType({ + fileKind: 'directory', + fileName: 'files', + })).toBe('folder') + expect(getDriveFileIconType({ + fileKind: 'pdf', + fileName: 'guide', + })).toBe('pdf') + }) + + it('should infer icons from file extension when mime type is not enough', () => { + expect(getFileIconType('data.csv')).toBe('table') + expect(getFileIconType('archive.zip')).toBe('archive') + expect(getFileIconType('script.ts')).toBe('code') + }) +}) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx index 3476c890b3b..9961ba7d242 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/__tests__/index.spec.tsx @@ -1,5 +1,6 @@ +import type { AgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { fireEvent, render, screen, within } from '@testing-library/react' +import { fireEvent, render, screen, waitFor, within } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { defaultAgentSoulConfigFormState } from '@/features/agent-v2/agent-composer/form-state' import { AgentComposerProvider } from '@/features/agent-v2/agent-composer/provider' @@ -7,20 +8,87 @@ import { AgentOrchestrateReadOnlyContext } from '../../read-only-context' import { AgentFiles } from '../index' const mocks = vi.hoisted(() => ({ - filePreviewQueryOptions: vi.fn(), + agentDriveFilesQueryOptions: vi.fn(), + agentFileCommitMutationFn: vi.fn(), + agentFileDeleteMutationFn: vi.fn(), + agentFileDeleteMutationOptions: vi.fn(), + agentFileDownloadQueryOptions: vi.fn(), + agentFilePreviewQueryOptions: vi.fn(), + agentFileCommitMutationOptions: vi.fn(), + workflowAgentDriveFilesQueryOptions: vi.fn(), + workflowAgentFileCommitMutationFn: vi.fn(), + workflowAgentFileDeleteMutationFn: vi.fn(), + workflowAgentFileDeleteMutationOptions: vi.fn(), + workflowAgentFileDownloadQueryOptions: vi.fn(), + workflowAgentFilePreviewQueryOptions: vi.fn(), + workflowAgentFileCommitMutationOptions: vi.fn(), + uploadFileMutationFn: vi.fn(), uploadFileMutationOptions: vi.fn(), })) vi.mock('@/service/client', () => ({ consoleQuery: { - files: { - byFileId: { - preview: { - get: { - queryOptions: mocks.filePreviewQueryOptions, + agent: { + byAgentId: { + drive: { + files: { + get: { + queryOptions: mocks.agentDriveFilesQueryOptions, + }, + download: { + get: { + queryOptions: mocks.agentFileDownloadQueryOptions, + }, + }, + preview: { + get: { + queryOptions: mocks.agentFilePreviewQueryOptions, + }, + }, + }, + }, + files: { + delete: { + mutationOptions: mocks.agentFileDeleteMutationOptions, + }, + post: { + mutationOptions: mocks.agentFileCommitMutationOptions, }, }, }, + }, + apps: { + byAppId: { + agent: { + drive: { + files: { + get: { + queryOptions: mocks.workflowAgentDriveFilesQueryOptions, + }, + download: { + get: { + queryOptions: mocks.workflowAgentFileDownloadQueryOptions, + }, + }, + preview: { + get: { + queryOptions: mocks.workflowAgentFilePreviewQueryOptions, + }, + }, + }, + }, + files: { + delete: { + mutationOptions: mocks.workflowAgentFileDeleteMutationOptions, + }, + post: { + mutationOptions: mocks.workflowAgentFileCommitMutationOptions, + }, + }, + }, + }, + }, + files: { upload: { post: { mutationOptions: mocks.uploadFileMutationOptions, @@ -37,16 +105,36 @@ const agentFilesDraft = { id: 'preview-image', name: 'agent-roster-skill-detail-dialog-preview-image.png', icon: 'image', + driveKey: 'files/agent-roster-skill-detail-dialog-preview-image.png', }, { id: 'brief', name: 'brief.md', icon: 'markdown', + driveKey: 'files/brief.md', }, ], -} satisfies typeof defaultAgentSoulConfigFormState +} satisfies AgentSoulConfigFormState -function renderAgentFiles() { +const agentSkillFilesDraft = { + ...defaultAgentSoulConfigFormState, + files: [ + { + id: 'script', + name: 'run.py', + icon: 'file', + driveKey: 'files/run.py', + }, + { + id: 'skill-md', + name: 'SKILL.md', + icon: 'markdown', + driveKey: 'files/SKILL.md', + }, + ], +} satisfies AgentSoulConfigFormState + +function renderAgentFiles(initialDraft: AgentSoulConfigFormState = agentFilesDraft) { const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -57,8 +145,8 @@ function renderAgentFiles() { return render( - - + + , ) @@ -77,7 +165,7 @@ function renderReadonlyAgentFiles() { - + , @@ -87,38 +175,272 @@ function renderReadonlyAgentFiles() { describe('AgentFiles', () => { beforeEach(() => { vi.clearAllMocks() - mocks.filePreviewQueryOptions.mockImplementation(({ input }) => ({ - queryKey: ['file-preview', input], + mocks.agentDriveFilesQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['agent-drive-files', input], + queryFn: () => new Promise(() => {}), + })) + mocks.workflowAgentDriveFilesQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['workflow-agent-drive-files', input], + queryFn: () => new Promise(() => {}), + })) + mocks.agentFilePreviewQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['agent-file-preview', input], queryFn: async () => ({ - content: `Preview content for ${input.params.file_id}`, + binary: false, + text: `Preview content for ${input.query.key}`, + }), + })) + mocks.workflowAgentFilePreviewQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['workflow-agent-file-preview', input], + queryFn: async () => ({ + binary: false, + text: `Preview content for ${input.query.key}`, + }), + })) + mocks.agentFileDownloadQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['agent-file-download', input], + queryFn: async () => ({ + url: `https://signed.example/${input.query.key}`, + }), + })) + mocks.workflowAgentFileDownloadQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['workflow-agent-file-download', input], + queryFn: async () => ({ + url: `https://signed.example/${input.query.key}`, }), })) mocks.uploadFileMutationOptions.mockReturnValue({ - mutationFn: vi.fn(), + mutationFn: mocks.uploadFileMutationFn.mockResolvedValue({ + id: 'upload-file-1', + name: 'uploaded.md', + mime_type: 'text/markdown', + }), mutationKey: ['upload-file'], }) + mocks.agentFileCommitMutationOptions.mockReturnValue({ + mutationFn: mocks.agentFileCommitMutationFn.mockResolvedValue({ + file: { + drive_key: 'files/uploaded.md', + file_id: 'drive-file-1', + mime_type: 'text/markdown', + name: 'uploaded.md', + }, + }), + mutationKey: ['commit-agent-file'], + }) + mocks.workflowAgentFileCommitMutationOptions.mockReturnValue({ + mutationFn: mocks.workflowAgentFileCommitMutationFn.mockResolvedValue({ + file: { + drive_key: 'files/uploaded.md', + file_id: 'drive-file-1', + mime_type: 'text/markdown', + name: 'uploaded.md', + }, + }), + mutationKey: ['commit-workflow-agent-file'], + }) + mocks.agentFileDeleteMutationOptions.mockReturnValue({ + mutationFn: mocks.agentFileDeleteMutationFn.mockResolvedValue({ result: 'success' }), + mutationKey: ['delete-agent-file'], + }) + mocks.workflowAgentFileDeleteMutationOptions.mockReturnValue({ + mutationFn: mocks.workflowAgentFileDeleteMutationFn.mockResolvedValue({ result: 'success' }), + mutationKey: ['delete-workflow-agent-file'], + }) + }) + + it('should list Agent App drive files under the files prefix', () => { + renderAgentFiles() + + expect(mocks.agentDriveFilesQueryOptions).toHaveBeenCalledWith({ + input: { + params: { + agent_id: 'agent-1', + }, + query: { + prefix: 'files/', + }, + }, + }) + }) + + it('should keep the file preview trigger focus ring inside the row bounds', () => { + renderAgentFiles() + + expect(screen.getByRole('button', { name: 'brief.md' })).toHaveClass( + 'focus-visible:ring-2', + 'focus-visible:ring-state-accent-solid', + 'focus-visible:ring-inset', + ) }) it('should open the shared detail dialog with the full file tree when the file row is clicked', async () => { renderAgentFiles() fireEvent.click(screen.getByRole('button', { - name: 'agent-roster-skill-detail-dialog-preview-image.png', + name: 'brief.md', })) const dialog = screen.getByRole('dialog') expect(dialog).toBeInTheDocument() - expect(within(dialog).getAllByText('agent-roster-skill-detail-dialog-preview-image.png')).toHaveLength(2) - expect(within(dialog).getByText('brief.md')).toBeInTheDocument() - expect(mocks.filePreviewQueryOptions).toHaveBeenCalledWith({ + expect(within(dialog).getByText('agent-roster-skill-detail-dialog-preview-image.png')).toBeInTheDocument() + expect(within(dialog).getAllByText('brief.md')).toHaveLength(2) + expect(mocks.agentFilePreviewQueryOptions).toHaveBeenCalledWith({ input: { params: { - file_id: 'preview-image', + agent_id: 'agent-1', + }, + query: { + key: 'files/brief.md', }, }, }) - expect(await within(dialog).findByText('Preview content for preview-image')).toBeInTheDocument() + expect(await within(dialog).findByText('Preview content for files/brief.md')).toBeInTheDocument() + }) + + it('should preview the clicked file when SKILL.md also exists', async () => { + renderAgentFiles(agentSkillFilesDraft) + + fireEvent.click(screen.getByRole('button', { + name: 'run.py', + })) + + const dialog = screen.getByRole('dialog') + + expect(await within(dialog).findByText('Preview content for files/run.py')).toBeInTheDocument() + expect(mocks.agentFilePreviewQueryOptions).toHaveBeenCalledWith({ + input: { + params: { + agent_id: 'agent-1', + }, + query: { + key: 'files/run.py', + }, + }, + }) + }) + + it('should preview the selected file from the detail file tree', async () => { + renderAgentFiles(agentSkillFilesDraft) + + fireEvent.click(screen.getByRole('button', { + name: 'run.py', + })) + + const dialog = screen.getByRole('dialog') + const skillFile = within(dialog).getByRole('button', { name: 'SKILL.md' }) + fireEvent.click(skillFile) + + expect(await within(dialog).findByText('Preview content for files/SKILL.md')).toBeInTheDocument() + + const scriptFile = within(dialog).getAllByRole('button', { name: 'run.py' }).at(-1) + expect(scriptFile).toBeDefined() + fireEvent.click(scriptFile!) + + expect(await within(dialog).findByText('Preview content for files/run.py')).toBeInTheDocument() + expect(mocks.agentFilePreviewQueryOptions).toHaveBeenCalledWith({ + input: { + params: { + agent_id: 'agent-1', + }, + query: { + key: 'files/run.py', + }, + }, + }) + }) + + it('should render image files directly from the drive download URL without a download link', async () => { + mocks.agentFilePreviewQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['agent-file-preview', input], + queryFn: async () => ({ + binary: false, + key: input.query.key, + size: 12345, + text: 'image preview should not render as text', + truncated: false, + }), + })) + renderAgentFiles() + + fireEvent.click(screen.getByRole('button', { + name: 'agent-roster-skill-detail-dialog-preview-image.png', + })) + + const image = await screen.findByRole('img', { + name: 'agent-roster-skill-detail-dialog-preview-image.png', + }) + + expect(image).toHaveAttribute('src', 'https://signed.example/files/agent-roster-skill-detail-dialog-preview-image.png') + expect(screen.queryByRole('link', { name: /common\.operation\.download/ })).not.toBeInTheDocument() + expect(screen.queryByText('image preview should not render as text')).not.toBeInTheDocument() + expect(mocks.agentFileDownloadQueryOptions).toHaveBeenCalledWith({ + input: { + params: { + agent_id: 'agent-1', + }, + query: { + key: 'files/agent-roster-skill-detail-dialog-preview-image.png', + }, + }, + }) + }) + + it('should render a download link for binary non-image files', async () => { + mocks.agentFilePreviewQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['agent-file-preview', input], + queryFn: async () => ({ + binary: true, + key: input.query.key, + size: 12345, + text: null, + truncated: false, + }), + })) + renderAgentFiles() + + fireEvent.click(screen.getByRole('button', { + name: 'brief.md', + })) + + const link = await screen.findByRole('link', { name: 'common.operation.download' }) + + expect(screen.getByText('agentV2.agentDetail.configure.files.preview.unsupported')).toBeInTheDocument() + expect(link).toHaveAttribute('href', 'https://signed.example/files/brief.md') + expect(screen.queryByText('Preview content for files/brief.md')).not.toBeInTheDocument() + }) + + it('should commit an uploaded file to the Agent App drive before adding it to the composer draft', async () => { + renderAgentFiles({ + ...defaultAgentSoulConfigFormState, + files: [], + }) + + fireEvent.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.files.add' })) + const input = document.querySelector('input[type="file"]') + expect(input).toBeInstanceOf(HTMLInputElement) + + fireEvent.change(input!, { + target: { + files: [new File(['# Uploaded'], 'uploaded.md', { type: 'text/markdown' })], + }, + }) + fireEvent.click(screen.getByRole('button', { name: 'agentV2.agentDetail.configure.files.upload.action' })) + + await waitFor(() => { + expect(mocks.agentFileCommitMutationFn).toHaveBeenCalledWith( + { + params: { + agent_id: 'agent-1', + }, + body: { + upload_file_id: 'upload-file-1', + }, + }, + expect.anything(), + ) + }) }) // File rows expose a hover/focus remove action that updates the composer draft. diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/api-context.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/api-context.ts new file mode 100644 index 00000000000..886dd16841e --- /dev/null +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/api-context.ts @@ -0,0 +1,7 @@ +export type AgentFileApiContext = { + agentId: string + workflow?: { + appId: string + nodeId: string + } +} diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/file-icon.ts b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/file-icon.ts new file mode 100644 index 00000000000..cb462055058 --- /dev/null +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/file-icon.ts @@ -0,0 +1,81 @@ +import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree' + +const codeFileExtensions = new Set([ + 'css', + 'go', + 'html', + 'js', + 'jsx', + 'py', + 'rb', + 'rs', + 'scss', + 'sh', + 'ts', + 'tsx', + 'vue', + 'yaml', + 'yml', +]) +const tableFileExtensions = new Set(['csv', 'xls', 'xlsx']) +const archiveFileExtensions = new Set(['7z', 'gz', 'rar', 'tar', 'zip']) +const driveFileIconTypes = new Set([ + 'archive', + 'code', + 'database', + 'file', + 'folder', + 'image', + 'json', + 'markdown', + 'pdf', + 'table', + 'text', +]) + +function getFileExtension(fileName: string) { + return fileName.split('.').pop()?.toLowerCase() ?? '' +} + +export function getFileIconType(fileName: string, mimeType?: string | null): FileTreeIconType { + const extension = getFileExtension(fileName) + + if (mimeType?.startsWith('image/')) + return 'image' + if (mimeType === 'application/pdf' || extension === 'pdf') + return 'pdf' + if (extension === 'md' || extension === 'markdown' || extension === 'mdx') + return 'markdown' + if (extension === 'json') + return 'json' + if (tableFileExtensions.has(extension)) + return 'table' + if (archiveFileExtensions.has(extension)) + return 'archive' + if (codeFileExtensions.has(extension)) + return 'code' + if (mimeType?.startsWith('text/')) + return 'text' + + return 'file' +} + +export function getDriveFileIconType({ + fileKind, + fileName, + mimeType, +}: { + fileKind?: string | null + fileName: string + mimeType?: string | null +}): FileTreeIconType { + const normalizedFileKind = fileKind?.toLowerCase() + + if (normalizedFileKind === 'directory') + return 'folder' + + if (normalizedFileKind && driveFileIconTypes.has(normalizedFileKind as FileTreeIconType)) + return normalizedFileKind as FileTreeIconType + + return getFileIconType(fileName, mimeType) +} diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx index 7a0930bca87..4d61b51f9ec 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/files/index.tsx @@ -2,16 +2,18 @@ import type { ReactNode } from 'react' import type { AgentOrchestrateAddActionOptions } from '../add-actions-context' +import type { AgentFileApiContext } from './api-context' import type { AgentFileNode } from '@/features/agent-v2/agent-composer/form-state' import { Dialog, + DialogTrigger, } from '@langgenius/dify-ui/dialog' import { FileTreeGuide, } from '@langgenius/dify-ui/file-tree' -import { useQuery } from '@tanstack/react-query' +import { useMutation, useQuery } from '@tanstack/react-query' import { useAtom } from 'jotai' -import { useCallback, useRef, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { agentComposerFilesAtom } from '@/features/agent-v2/agent-composer/store-modules/files' import { consoleQuery } from '@/service/client' @@ -21,6 +23,7 @@ import { ConfigureSectionEmpty } from '../common/empty' import { ConfigureSection } from '../common/section' import { useAgentOrchestrateReadOnly } from '../read-only-context' import { AgentSkillDetailDialog } from '../skills/detail-dialog' +import { getDriveFileIconType } from './file-icon' import { AgentFileTree } from './tree' import { AgentFileUploadDialog } from './upload-dialog' @@ -31,11 +34,47 @@ const removeFileNode = (files: AgentFileNode[], fileId: string): AgentFileNode[] children: file.children ? removeFileNode(file.children, fileId) : undefined, })) +const FILES_DRIVE_PREFIX = 'files/' + +const getAgentFilePreviewKey = (file: AgentFileNode) => file.driveKey ?? file.id + +const findAgentFileNode = (files: AgentFileNode[], fileId: string): AgentFileNode | undefined => { + for (const file of files) { + if (file.id === fileId) + return file + + const child = file.children ? findAgentFileNode(file.children, fileId) : undefined + if (child) + return child + } +} + +const getAgentDriveFileName = (key: string) => { + const normalizedKey = key.endsWith('/') ? key.slice(0, -1) : key + return normalizedKey.split('/').pop() || normalizedKey +} + +const toAgentFileNodeFromDriveItem = (item: { + file_kind: string + key: string + mime_type?: string | null +}): AgentFileNode => ({ + id: item.key, + name: getAgentDriveFileName(item.key), + icon: getDriveFileIconType({ + fileKind: item.file_kind, + fileName: getAgentDriveFileName(item.key), + mimeType: item.mime_type, + }), + driveKey: item.key, +}) + function AgentFileItem({ children, depth, file, files, + apiContext, onRemove, selected, }: { @@ -43,35 +82,96 @@ function AgentFileItem({ depth: number file: AgentFileNode files: AgentFileNode[] + apiContext: AgentFileApiContext onRemove: (fileId: string) => void selected: boolean }) { const { t } = useTranslation('agentV2') const readOnly = useAgentOrchestrateReadOnly() const [isPreviewOpen, setIsPreviewOpen] = useState(false) - const previewQuery = useQuery({ - ...consoleQuery.files.byFileId.preview.get.queryOptions({ + const [selectedFileId, setSelectedFileId] = useState() + const selectedFile = selectedFileId ? findAgentFileNode(files, selectedFileId) : undefined + const previewFileId = getAgentFilePreviewKey(selectedFile ?? file) + const agentPreviewQuery = useQuery({ + ...consoleQuery.agent.byAgentId.drive.files.preview.get.queryOptions({ input: { params: { - file_id: file.id, + agent_id: apiContext.agentId, + }, + query: { + key: previewFileId ?? '', }, }, }), - enabled: isPreviewOpen, + enabled: isPreviewOpen && !!previewFileId && !apiContext.workflow, }) + const workflowPreviewQuery = useQuery({ + ...consoleQuery.apps.byAppId.agent.drive.files.preview.get.queryOptions({ + input: { + params: { + app_id: apiContext.workflow?.appId ?? '', + }, + query: { + key: previewFileId ?? '', + node_id: apiContext.workflow?.nodeId, + }, + }, + }), + enabled: isPreviewOpen && !!previewFileId && !!apiContext.workflow, + }) + const previewQuery = apiContext.workflow ? workflowPreviewQuery : agentPreviewQuery + const selectedPreviewFile = selectedFile ?? file + const isImagePreviewFile = selectedPreviewFile.icon === 'image' + const shouldDownloadPreviewFile = isPreviewOpen && !!previewFileId && (isImagePreviewFile || !!previewQuery.data?.binary) + const agentDownloadQuery = useQuery({ + ...consoleQuery.agent.byAgentId.drive.files.download.get.queryOptions({ + input: { + params: { + agent_id: apiContext.agentId, + }, + query: { + key: previewFileId ?? '', + }, + }, + }), + enabled: shouldDownloadPreviewFile && !apiContext.workflow, + }) + const workflowDownloadQuery = useQuery({ + ...consoleQuery.apps.byAppId.agent.drive.files.download.get.queryOptions({ + input: { + params: { + app_id: apiContext.workflow?.appId ?? '', + }, + query: { + key: previewFileId ?? '', + node_id: apiContext.workflow?.nodeId, + }, + }, + }), + enabled: shouldDownloadPreviewFile && !!apiContext.workflow, + }) + const downloadQuery = apiContext.workflow ? workflowDownloadQuery : agentDownloadQuery const handleRemove = useCallback(() => { onRemove(file.id) }, [file.id, onRemove]) + const handlePreviewOpenChange = useCallback((open: boolean) => { + if (open) + setSelectedFileId(file.id) + setIsPreviewOpen(open) + }, [file.id]) return (
  • - - - {isPreviewOpen && ( - - )} + + setSelectedFileId(selectedFile.id), + selectedFileId: selectedFileId ?? file.id, + sections: [], + }} + /> {!readOnly && ( : null} + hotkey={PUBLISH_AGENT_HOTKEY} + agentId={agentId} agentName={agentName} disabled={!canPublish} - publishedReferenceCount={publishedReferenceCount} - publishedReferences={publishedReferences} + onOpenChange={handleImpactPopoverOpenChange} onPublish={handlePublish} trigger={(
  • @@ -127,11 +174,12 @@ export function AgentPublishImpactPopover({ function ReferenceLink({ reference, - index, }: { - reference: AgentPublishedReferenceResponse - index: number + reference: AgentReferencingWorkflowResponse }) { + const imageUrl = (reference.app_icon_type === 'image' || reference.app_icon_type === 'link') ? reference.app_icon : undefined + const iconType = (imageUrl ? 'image' : reference.app_icon_type) as AgentIconType | null | undefined + return ( - - {getWorkflowReferenceInitial(reference.app_name)} + + {reference.app_name} diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx index eec25d009f2..c54d4c86d0f 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/__tests__/index.spec.tsx @@ -11,6 +11,8 @@ import { AgentSkills } from '../index' const mocks = vi.hoisted(() => ({ driveFilesQueryOptions: vi.fn(), + driveFileDownloadQueryOptions: vi.fn(), + driveFilePreviewQueryOptions: vi.fn(), uploadSkillMutationOptions: vi.fn(), })) @@ -30,6 +32,16 @@ vi.mock('@/service/client', () => ({ get: { queryOptions: mocks.driveFilesQueryOptions, }, + download: { + get: { + queryOptions: mocks.driveFileDownloadQueryOptions, + }, + }, + preview: { + get: { + queryOptions: mocks.driveFilePreviewQueryOptions, + }, + }, }, }, skills: { @@ -51,7 +63,7 @@ const agentSkillsDraft = { id: 'tender-analyzer', name: 'Tender Analyzer', description: 'Extracts tender requirements and scoring criteria.', - files: ['SKILL.md', 'schema.json'], + files: ['__MACOSX/._hatch-pet', 'SKILL.md', 'schema.json'], path: 'tender-analyzer', skillMdKey: 'tender-analyzer/SKILL.md', }, @@ -129,6 +141,18 @@ describe('AgentSkills', () => { ], }), })) + mocks.driveFilePreviewQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['agent-drive-file-preview', input], + queryFn: async () => ({ + text: `Preview content for ${input.query.key}`, + }), + })) + mocks.driveFileDownloadQueryOptions.mockImplementation(({ input }) => ({ + queryKey: ['agent-drive-file-download', input], + queryFn: async () => ({ + url: `https://example.com/${input.query.key}`, + }), + })) mocks.uploadSkillMutationOptions.mockReturnValue({ mutationFn: vi.fn(), mutationKey: ['upload-skill'], @@ -152,14 +176,60 @@ describe('AgentSkills', () => { agent_id: 'agent-1', }, query: { - prefix: 'tender-analyzer', + prefix: 'tender-analyzer/', }, }, }) expect(within(dialog).getByText('Tender Analyzer')).toBeInTheDocument() expect(within(dialog).getByText('Extracts tender requirements and scoring criteria.')).toBeInTheDocument() + expect(within(dialog).queryByText('__MACOSX/._hatch-pet')).not.toBeInTheDocument() expect(await within(dialog).findByText('scripts/extract.py')).toBeInTheDocument() expect(within(dialog).getByText('SKILL.md')).toBeInTheDocument() + expect(await within(dialog).findByText('Preview content for tender-analyzer/SKILL.md')).toBeInTheDocument() + expect(mocks.driveFilePreviewQueryOptions).toHaveBeenCalledWith({ + input: { + params: { + agent_id: 'agent-1', + }, + query: { + key: 'tender-analyzer/SKILL.md', + }, + }, + }) + expect(mocks.driveFilePreviewQueryOptions).not.toHaveBeenCalledWith({ + input: { + params: { + agent_id: 'agent-1', + }, + query: { + key: 'tender-analyzer/__MACOSX/._hatch-pet', + }, + }, + }) + }) + + it('should preview the selected skill file from the detail file tree', async () => { + renderAgentSkills() + + fireEvent.click(screen.getByRole('button', { + name: 'Tender Analyzer', + })) + + const dialog = screen.getByRole('dialog') + const scriptFile = await within(dialog).findByRole('button', { name: 'scripts/extract.py' }) + fireEvent.click(scriptFile) + + expect(await within(dialog).findByText('Preview content for tender-analyzer/scripts/extract.py')).toBeInTheDocument() + expect(mocks.driveFilePreviewQueryOptions).toHaveBeenCalledWith({ + input: { + params: { + agent_id: 'agent-1', + }, + query: { + key: 'tender-analyzer/scripts/extract.py', + }, + }, + }) }) // The hover/focus remove action updates the composer draft without opening preview. @@ -235,7 +305,7 @@ describe('AgentSkills', () => { agent_id: 'agent-1', }, query: { - prefix: 'invoice-helper', + prefix: 'invoice-helper/', }, }, }) diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/detail-dialog.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/detail-dialog.tsx index 169c1272e1d..c7fd8c3f6ff 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/detail-dialog.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/detail-dialog.tsx @@ -7,6 +7,7 @@ import { DialogDescription, DialogTitle, } from '@langgenius/dify-ui/dialog' +import { FileTreeFile } from '@langgenius/dify-ui/file-tree' import { ScrollArea } from '@langgenius/dify-ui/scroll-area' import { useTranslation } from 'react-i18next' import Loading from '@/app/components/base/loading' @@ -27,10 +28,17 @@ export type AgentSkillDetail = { fileCount?: number files: AgentSkillFileNode[] filePreview?: { + binary?: boolean content?: string + downloadUrl?: string + fileName?: string + isDownloadError?: boolean + isDownloadLoading?: boolean isError?: boolean + isImage?: boolean isLoading?: boolean } + onSelectFile?: (file: AgentSkillFileNode) => void selectedFileId?: string sections: AgentSkillDetailSection[] } @@ -38,10 +46,12 @@ export type AgentSkillDetail = { function AgentSkillFileList({ files, fileCount, + onSelectFile, selectedFileId, }: { files: AgentSkillFileNode[] fileCount: number + onSelectFile?: (file: AgentSkillFileNode) => void selectedFileId?: string }) { const { t } = useTranslation('agentV2') @@ -52,6 +62,13 @@ function AgentSkillFileList({ selectedFileId={selectedFileId} labelledBy="agent-skill-detail-files-heading" className="h-[258px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-1 shadow-xs shadow-shadow-shadow-3" + renderFile={onSelectFile + ? ({ file, selected, children }) => ( + onSelectFile(file)}> + {children} + + ) + : undefined} header={( <>

    @@ -96,17 +113,30 @@ function AgentSkillDetailSectionBlock({ } function AgentFilePreviewContent({ + binary, content, + downloadUrl, + fileName, + isDownloadError, + isDownloadLoading, isError, + isImage, isLoading, }: { + binary?: boolean content?: string + downloadUrl?: string + fileName?: string + isDownloadError?: boolean + isDownloadLoading?: boolean isError?: boolean + isImage?: boolean isLoading?: boolean }) { const { t } = useTranslation('agentV2') + const { t: tCommon } = useTranslation('common') - if (isLoading) { + if (isLoading || isDownloadLoading) { return (
    @@ -114,7 +144,7 @@ function AgentFilePreviewContent({ ) } - if (isError) { + if (isError || isDownloadError) { return (

    {t('agentDetail.configure.files.preview.failed')} @@ -122,6 +152,45 @@ function AgentFilePreviewContent({ ) } + if (isImage && downloadUrl) { + return ( +

    + {fileName +
    + ) + } + + if (binary) { + if (downloadUrl) { + return ( +
    + + {t('agentDetail.configure.files.preview.unsupported')} + + + + {tCommon('operation.download')} + +
    + ) + } + + return ( +

    + {t('agentDetail.configure.files.preview.empty')} +

    + ) + } + if (!content) { return (

    @@ -171,8 +240,14 @@ export function AgentSkillDetailDialog({ > {detail.filePreview && ( )} @@ -181,7 +256,12 @@ export function AgentSkillDetailDialog({ ))}

    - +
    diff --git a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/item.tsx b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/item.tsx index 67b93743fa9..d3160a470c6 100644 --- a/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/item.tsx +++ b/web/features/agent-v2/agent-detail/configure/components/orchestrate/skills/item.tsx @@ -1,7 +1,7 @@ 'use client' import type { AgentDriveItemResponse } from '@dify/contracts/api/console/agent/types.gen' -import type { AgentSkill } from '@/features/agent-v2/agent-composer/form-state' +import type { AgentFileNode, AgentSkill } from '@/features/agent-v2/agent-composer/form-state' import { Dialog, } from '@langgenius/dify-ui/dialog' @@ -9,6 +9,7 @@ import { useQuery } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { consoleQuery } from '@/service/client' +import { getDriveFileIconType } from '../files/file-icon' import { useAgentOrchestrateReadOnly } from '../read-only-context' import { AgentSkillDetailDialog } from './detail-dialog' @@ -22,11 +23,37 @@ const getSkillFileName = (key: string, skillDrivePath: string) => key.startsWith : key const toSkillFileNode = (item: AgentDriveItemResponse, skillDrivePath: string) => ({ - icon: item.file_kind === 'folder' ? 'folder' as const : 'file' as const, + icon: getDriveFileIconType({ + fileKind: item.file_kind, + fileName: getSkillFileName(item.key, skillDrivePath), + mimeType: item.mime_type, + }), id: item.key, name: getSkillFileName(item.key, skillDrivePath), }) +const getSkillMdFileId = (files: AgentFileNode[]): string | undefined => { + for (const file of files) { + if (file.icon !== 'folder' && file.name === 'SKILL.md') + return file.id + + const childFileId = file.children ? getSkillMdFileId(file.children) : undefined + if (childFileId) + return childFileId + } +} + +const getFirstSkillFileId = (files: AgentFileNode[]): string | undefined => { + for (const file of files) { + if (file.icon !== 'folder') + return file.id + + const childFileId = file.children ? getFirstSkillFileId(file.children) : undefined + if (childFileId) + return childFileId + } +} + export function AgentSkillItem({ agentId, skill, @@ -39,10 +66,14 @@ export function AgentSkillItem({ const { t } = useTranslation('agentV2') const readOnly = useAgentOrchestrateReadOnly() const [isPreviewOpen, setIsPreviewOpen] = useState(false) + const [selectedFileId, setSelectedFileId] = useState() const handleRemove = useCallback(() => { onRemove(skill.id) }, [onRemove, skill.id]) - const skillFiles = skill.files ?? [] + const handleOpenPreview = useCallback(() => { + setSelectedFileId(undefined) + setIsPreviewOpen(true) + }, []) const skillDrivePath = getSkillDrivePath(skill) const driveFilesQuery = useQuery({ ...consoleQuery.agent.byAgentId.drive.files.get.queryOptions({ @@ -51,7 +82,7 @@ export function AgentSkillItem({ agent_id: agentId, }, query: { - prefix: skillDrivePath, + prefix: `${skillDrivePath}/`, }, }, }), @@ -59,11 +90,38 @@ export function AgentSkillItem({ }) const detailFiles = driveFilesQuery.isSuccess ? (driveFilesQuery.data.items ?? []).map(item => toSkillFileNode(item, skillDrivePath)) - : skillFiles.map(file => ({ - icon: 'file' as const, - id: file, - name: file, - })) + : [] + const previewFileId = selectedFileId + ?? skill.skillMdKey + ?? (driveFilesQuery.isSuccess ? getSkillMdFileId(detailFiles) ?? getFirstSkillFileId(detailFiles) : undefined) + const previewQuery = useQuery({ + ...consoleQuery.agent.byAgentId.drive.files.preview.get.queryOptions({ + input: { + params: { + agent_id: agentId, + }, + query: { + key: previewFileId ?? '', + }, + }, + }), + enabled: isPreviewOpen && !!previewFileId, + }) + const selectedFile = detailFiles.find(file => file.id === previewFileId) + const isImagePreviewFile = selectedFile?.icon === 'image' + const downloadQuery = useQuery({ + ...consoleQuery.agent.byAgentId.drive.files.download.get.queryOptions({ + input: { + params: { + agent_id: agentId, + }, + query: { + key: previewFileId ?? '', + }, + }, + }), + enabled: isPreviewOpen && !!previewFileId && (isImagePreviewFile || !!previewQuery.data?.binary), + }) return ( @@ -71,7 +129,7 @@ export function AgentSkillItem({ + + + )} + {isSuccess && logs.length === 0 && ( + + {t('agentDetail.logs.empty')} + + )} + {isSuccess && logs.map(log => ( + + + + + + {log.title || notAvailable} + + + + + + {log.end_user_id || notAvailable} + + + {log.message_count} + + + {formatRate(log.user_rate, notAvailable)} + + + {formatRate(log.operation_rate, notAvailable)} + + + {formatLogTime(log.updated_at)} + + + {formatLogTime(log.created_at)} + + + ))} + + ) +} + +function formatRate(value: number | null | undefined, fallback: string) { + return value == null ? fallback : `${Math.round(value * 100)}%` +} + +type LogsTableHeaderLabels = { + unread: string + title: string + source: string + endUser: string + messageCount: string + userRate: string + operationRate: string + updatedTime: string + createdTime: string +} + +function LogsTableHeader({ + labels, + rowClassName, +}: { + labels: LogsTableHeaderLabels + rowClassName?: string +}) { + return ( + + + + {labels.unread} + + {labels.title} + {labels.source} + {labels.endUser} + {labels.messageCount} + {labels.userRate} + {labels.operationRate} + {labels.updatedTime} + {labels.createdTime} + + + ) +} + +function LogsTableColGroup() { + return ( + + + + + + + + + + + + ) +} + +function LogsStateRow({ + children, +}: { + children: ReactNode +}) { + return ( + + + {children} + + + ) +} + +function LogsSkeletonRows() { + return ( + <> + {Array.from({ length: 10 }, (_, index) => ( + + + + + +
    + + +
    +
    +
    +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + +
    + + + ))} + + ) +} + +function TableHead({ + className, + ...props +}: ThHTMLAttributes) { + return ( + + ) +} + +function TableCell({ + children, + className, + ...props +}: TdHTMLAttributes) { + return ( + +
    + {children} +
    + + ) +} diff --git a/web/features/agent-v2/agent-detail/logs/components/source-cell.tsx b/web/features/agent-v2/agent-detail/logs/components/source-cell.tsx new file mode 100644 index 00000000000..943dcc83578 --- /dev/null +++ b/web/features/agent-v2/agent-detail/logs/components/source-cell.tsx @@ -0,0 +1,28 @@ +import type { AgentLogConversationItemResponse } from '@dify/contracts/api/console/agent/types.gen' +import { useTranslation } from 'react-i18next' +import { LogSourceIcon } from './source-icon' + +export function LogSourceCell({ + source, +}: { + source?: AgentLogConversationItemResponse['source'] +}) { + const { t } = useTranslation('agentV2') + + if (!source) { + return ( +
    + {t('agentDetail.logs.notAvailable')} +
    + ) + } + + return ( +
    + +
    + {source.app_name} +
    +
    + ) +} diff --git a/web/features/agent-v2/agent-detail/logs/components/source-icon.tsx b/web/features/agent-v2/agent-detail/logs/components/source-icon.tsx new file mode 100644 index 00000000000..5af0c77ab70 --- /dev/null +++ b/web/features/agent-v2/agent-detail/logs/components/source-icon.tsx @@ -0,0 +1,33 @@ +import type { AgentIconType, AgentLogSourceResponse } from '@dify/contracts/api/console/agent/types.gen' +import AppIcon from '@/app/components/base/app-icon' + +const getLogSourceImageUrl = (source?: AgentLogSourceResponse | null) => + (source?.app_icon_type === 'image' || source?.app_icon_type === 'link') + ? source.app_icon + : undefined + +const getLogSourceIconType = (source?: AgentLogSourceResponse | null) => { + const imageUrl = getLogSourceImageUrl(source) + return (imageUrl ? 'image' : source?.app_icon_type) as AgentIconType | null | undefined +} + +export function LogSourceIcon({ + source, +}: { + source?: AgentLogSourceResponse | null +}) { + if (!source) + return + + return ( + + ) +} diff --git a/web/features/agent-v2/agent-detail/logs/components/source-picker.tsx b/web/features/agent-v2/agent-detail/logs/components/source-picker.tsx new file mode 100644 index 00000000000..6d04799ec87 --- /dev/null +++ b/web/features/agent-v2/agent-detail/logs/components/source-picker.tsx @@ -0,0 +1,184 @@ +import type { AgentLogSourceGroupResponse, AgentLogSourceResponse } from '@dify/contracts/api/console/agent/types.gen' +import type { TFunction } from 'i18next' +import type { ReactNode } from 'react' +import { Button } from '@langgenius/dify-ui/button' +import { cn } from '@langgenius/dify-ui/cn' +import { + Combobox, + ComboboxClear, + ComboboxCollection, + ComboboxContent, + ComboboxEmpty, + ComboboxGroup, + ComboboxGroupLabel, + ComboboxInput, + ComboboxInputGroup, + ComboboxItem, + ComboboxItemText, + ComboboxList, + ComboboxTrigger, + ComboboxValue, +} from '@langgenius/dify-ui/combobox' +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { LogSourceIcon } from './source-icon' + +export type SourceFilterValue = AgentLogSourceResponse['id'][] + +const getSourceGroupLabel = ( + group: AgentLogSourceGroupResponse, + t: TFunction<'agentV2'>, +) => { + if (group.type === 'webapp') + return t('agentDetail.logs.filters.source.webapp') + if (group.type === 'workflow') + return t('agentDetail.logs.filters.source.workflow') + return group.label +} + +const getSourceLabel = (source: AgentLogSourceResponse) => source.app_name + +export function AgentLogSourcePicker({ + value, + groups, + isLoading, + isError, + onRetry, + onChange, +}: { + value: SourceFilterValue + groups: AgentLogSourceGroupResponse[] + isLoading: boolean + isError: boolean + onRetry: () => void + onChange: (value: SourceFilterValue) => void +}) { + const { t } = useTranslation('agentV2') + const { t: tCommon } = useTranslation('common') + const [inputValue, setInputValue] = useState('') + const sources = groups.flatMap(group => group.sources ?? []) + const selectedSources = sources.filter(source => value.includes(source.id)) + + return ( + + multiple + items={groups} + value={selectedSources} + itemToStringLabel={getSourceLabel} + onValueChange={(nextSources) => { + setInputValue('') + onChange(nextSources.map(source => source.id)) + }} + inputValue={inputValue} + onInputValueChange={setInputValue} + > + + + {(selectedValue: AgentLogSourceResponse[]) => { + if (selectedValue.length === 0) + return t('agentDetail.logs.filters.source.all') + if (selectedValue.length === 1) + return selectedValue[0]!.app_name + return tCommon('dynamicSelect.selected', { count: selectedValue.length }) + }} + + + +
    + + + + + +
    + {isLoading && ( + + {t('agentDetail.logs.filters.source.loading')} + + )} + {isError && ( + + {t('agentDetail.logs.filters.source.loadFailed')} + + + )} + {!isLoading && !isError && ( + <> + + {groups.map(group => ( + + + {getSourceGroupLabel(group, t)} + + + {(source: AgentLogSourceResponse) => ( + + + + + + {source.app_name} + + + + )} + + + ))} + + + {t('agentDetail.logs.filters.source.empty')} + + + )} +
    + + ) +} + +function SourcePickerStatus({ + children, + className, +}: { + children: ReactNode + className?: string +}) { + return ( +
    + {children} +
    + ) +} + +function SourceCheckbox({ + checked, +}: { + checked: boolean +}) { + return ( + + {checked && } + + ) +} diff --git a/web/features/agent-v2/agent-detail/logs/mock-data.ts b/web/features/agent-v2/agent-detail/logs/mock-data.ts deleted file mode 100644 index b415a2d6c67..00000000000 --- a/web/features/agent-v2/agent-detail/logs/mock-data.ts +++ /dev/null @@ -1,376 +0,0 @@ -import type { I18nKeysWithPrefix } from '@/types/i18n' - -export type PeriodKey = 'last7days' | 'last30days' | 'allTime' -export type SourceKey = 'all' | 'webapp' | 'workflow' - -export type FilterOption = { - value: T - labelKey: I18nKeysWithPrefix<'agentV2', 'agentDetail.logs.'> -} - -type AgentLogRow = { - id: string - title: string - endUser: string - messageCount: number - userRate: string - operationRate: string - updatedTime: string - createdTime: string - source: Exclude - unread?: boolean -} - -export const periodOptions: Array> = [ - { value: 'last7days', labelKey: 'agentDetail.logs.filters.period.last7days' }, - { value: 'last30days', labelKey: 'agentDetail.logs.filters.period.last30days' }, - { value: 'allTime', labelKey: 'agentDetail.logs.filters.period.allTime' }, -] - -export const sourceOptions: Array> = [ - { value: 'all', labelKey: 'agentDetail.logs.filters.source.all' }, - { value: 'webapp', labelKey: 'agentDetail.logs.filters.source.webapp' }, - { value: 'workflow', labelKey: 'agentDetail.logs.filters.source.workflow' }, -] - -const LOG_ROW_COUNT = 5400 -const LOG_INTERVAL_MINUTES = 2 -const baseLogTime = new Date(2023, 2, 21, 10, 25) -const periodDays: Partial> = { - last7days: 7, - last30days: 30, -} - -const logTemplates: AgentLogRow[] = [ - { - id: 'log_001', - title: 'Asking about Dify agent orchestration best practices', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_002', - title: 'Alice, our user, talks about prompt orchestration techniques', - endUser: 'N/A', - messageCount: 3, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_003', - title: 'How to self-host a Dify chatbot for an internal team', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 5, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_004', - title: 'Requesting information about dataset retrieval settings', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 1, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_005', - title: 'Exploring options for connecting external knowledge bases', - endUser: 'N/A', - messageCount: 3, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_006', - title: 'What types of plugin tools can be used in workflows?', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_007', - title: 'Querying about Dify cloud deployment requirements', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_008', - title: 'Seeking assistance with YAML file setup', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_009', - title: 'Inquiring about compatibility with external APIs', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 5, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_010', - title: 'Can Dify integrate with my existing CRM?', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_011', - title: 'Exploring options for customizing chatbot responses', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_012', - title: 'Understanding data management and security in Dify', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_013', - title: 'Learning about available resources for getting started with Dify', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_014', - title: 'What are the best practices for optimizing prompts?', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_015', - title: 'How do I monitor the performance of my AI applications?', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_016', - title: 'Is there a free trial available for Dify?', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_017', - title: 'How can I improve user satisfaction metrics with Dify?', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, - { - id: 'log_018', - title: 'Are there any upcoming features or improvements in Dify?', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'workflow', - unread: true, - }, - { - id: 'log_019', - title: 'What are the recommended steps to deploy a Dify chatbot?', - endUser: '94924171-e6b0-4076-8f54-71d4370af8ef', - messageCount: 2, - userRate: 'N/A', - operationRate: 'N/A', - updatedTime: '2023-03-21 10:25', - createdTime: '2023-03-21 10:25', - source: 'webapp', - unread: true, - }, -] - -const formatDateTime = (date: Date) => { - const year = date.getFullYear() - const month = String(date.getMonth() + 1).padStart(2, '0') - const day = String(date.getDate()).padStart(2, '0') - const hour = String(date.getHours()).padStart(2, '0') - const minute = String(date.getMinutes()).padStart(2, '0') - - return `${year}-${month}-${day} ${hour}:${minute}` -} - -const formatLogTime = (index: number, offset: number) => { - const logTime = new Date(baseLogTime) - logTime.setMinutes(logTime.getMinutes() - index * LOG_INTERVAL_MINUTES - offset) - - return formatDateTime(logTime) -} - -const getPeriodThreshold = (period: PeriodKey) => { - const days = periodDays[period] - - if (!days) - return undefined - - const threshold = new Date(baseLogTime) - threshold.setDate(threshold.getDate() - days) - - return formatDateTime(threshold) -} - -const logRows: AgentLogRow[] = Array.from({ length: LOG_ROW_COUNT }, (_, index) => { - const template = logTemplates[index % logTemplates.length]! - - return { - ...template, - id: `${template.id}_${index + 1}`, - messageCount: template.messageCount + (index % 4), - updatedTime: formatLogTime(index, 0), - createdTime: formatLogTime(index, 3), - } -}) - -export function getOption(options: Array>, value: T) { - return options.find(option => option.value === value) ?? options[0]! -} - -export function getSortParts(sortBy: string) { - return { - sortOrder: sortBy.startsWith('-') ? '-' : '', - sortValue: sortBy.replace('-', '') || 'created_at', - } -} - -export function getAgentLogRowsView({ - period, - source, - keyword, - sortBy, - page, - limit, -}: { - period: PeriodKey - source: SourceKey - keyword: string - sortBy: string - page: number - limit: number -}) { - const normalizedKeyword = keyword.trim().toLowerCase() - const periodThreshold = getPeriodThreshold(period) - const filteredRows = logRows.filter((log) => { - const matchesPeriod = !periodThreshold || log.createdTime >= periodThreshold - const matchesSource = source === 'all' || log.source === source - const matchesKeyword = !normalizedKeyword || [ - log.title, - log.endUser, - String(log.messageCount), - log.updatedTime, - log.createdTime, - ].some(value => value.toLowerCase().includes(normalizedKeyword)) - - return matchesPeriod && matchesSource && matchesKeyword - }) - const { sortOrder, sortValue } = getSortParts(sortBy) - const sortField = sortValue === 'updated_at' ? 'updatedTime' : 'createdTime' - const sortDirection = sortOrder ? -1 : 1 - const sortedRows = [...filteredRows].sort((a, b) => { - const timeSort = a[sortField].localeCompare(b[sortField]) * sortDirection - - if (timeSort !== 0) - return timeSort - - return a.title.localeCompare(b.title) * sortDirection - }) - const totalPages = Math.max(Math.ceil(sortedRows.length / limit), 1) - const currentPage = Math.min(page, totalPages) - - return { - currentPage, - totalPages, - rows: sortedRows.slice((currentPage - 1) * limit, currentPage * limit), - } -} diff --git a/web/features/agent-v2/agent-detail/logs/page.tsx b/web/features/agent-v2/agent-detail/logs/page.tsx index 15d4b5f750d..bd3a31bfe96 100644 --- a/web/features/agent-v2/agent-detail/logs/page.tsx +++ b/web/features/agent-v2/agent-detail/logs/page.tsx @@ -1,73 +1,87 @@ 'use client' -import type { TdHTMLAttributes, ThHTMLAttributes } from 'react' -import type { - PeriodKey, - SourceKey, -} from './mock-data' -import { cn } from '@langgenius/dify-ui/cn' +import type { SourceFilterValue } from './components/source-picker' import { Pagination } from '@langgenius/dify-ui/pagination' -import { - ScrollAreaContent, - ScrollAreaRoot, - ScrollAreaScrollbar, - ScrollAreaThumb, - ScrollAreaViewport, -} from '@langgenius/dify-ui/scroll-area' -import { Select, SelectContent, SelectItem, SelectItemIndicator, SelectItemText, SelectTrigger } from '@langgenius/dify-ui/select' +import { useQuery } from '@tanstack/react-query' +import dayjs from 'dayjs' import { useState } from 'react' import { useTranslation } from 'react-i18next' import Chip from '@/app/components/base/chip' import { SearchInput } from '@/app/components/base/search-input' import Sort from '@/app/components/base/sort' import { useDocLink } from '@/context/i18n' -import { - getAgentLogRowsView, - getOption, - getSortParts, - periodOptions, - sourceOptions, -} from './mock-data' +import { consoleQuery } from '@/service/client' +import { AgentLogsTable } from './components/logs-table' +import { AgentLogSourcePicker } from './components/source-picker' -export function AgentLogsPage() { +type PeriodKey = 'last7days' | 'last30days' | 'allTime' + +type AgentLogsPageProps = { + agentId: string +} + +const queryDateFormat = 'YYYY-MM-DD HH:mm' + +const periodOptions: Array<{ + value: PeriodKey + labelKey: 'agentDetail.logs.filters.period.last7days' | 'agentDetail.logs.filters.period.last30days' | 'agentDetail.logs.filters.period.allTime' +}> = [ + { value: 'last7days', labelKey: 'agentDetail.logs.filters.period.last7days' }, + { value: 'last30days', labelKey: 'agentDetail.logs.filters.period.last30days' }, + { value: 'allTime', labelKey: 'agentDetail.logs.filters.period.allTime' }, +] + +const getPeriodQuery = (period: PeriodKey) => { + if (period === 'allTime') + return {} + + const days = period === 'last7days' ? 7 : 30 + return { + start: dayjs().subtract(days, 'day').format(queryDateFormat), + end: dayjs().add(1, 'minute').format(queryDateFormat), + } +} + +export function AgentLogsPage({ + agentId, +}: AgentLogsPageProps) { const { t } = useTranslation('agentV2') const { t: tCommon } = useTranslation('common') const docLink = useDocLink() const [period, setPeriod] = useState('last7days') - const [source, setSource] = useState('all') + const [source, setSource] = useState([]) const [keyword, setKeyword] = useState('') - const [sortBy, setSortBy] = useState('-created_at') - const [page, setPage] = useState(2) + const [page, setPage] = useState(1) const [limit, setLimit] = useState(25) - - const selectedSource = getOption(sourceOptions, source) - const { sortOrder, sortValue } = getSortParts(sortBy) - const tableHeaderLabels = { - unread: t('agentDetail.logs.table.unread'), - title: t('agentDetail.logs.table.title'), - endUser: t('agentDetail.logs.table.endUser'), - messageCount: t('agentDetail.logs.table.messageCount'), - userRate: t('agentDetail.logs.table.userRate'), - operationRate: t('agentDetail.logs.table.operationRate'), - updatedTime: t('agentDetail.logs.table.updatedTime'), - createdTime: t('agentDetail.logs.table.createdTime'), - } const periodItems = periodOptions.map(option => ({ value: option.value, name: t(option.labelKey), })) - const { - currentPage, - totalPages, - rows, - } = getAgentLogRowsView({ - period, - source, - keyword, - sortBy, - page, - limit, - }) + const logSourcesQuery = useQuery(consoleQuery.agent.byAgentId.logSources.get.queryOptions({ + input: { + params: { + agent_id: agentId, + }, + }, + })) + const logsQuery = useQuery(consoleQuery.agent.byAgentId.logs.get.queryOptions({ + input: { + params: { + agent_id: agentId, + }, + query: { + ...getPeriodQuery(period), + page, + limit, + keyword: keyword.trim() || undefined, + // TODO: Send multiple source ids after the backend contract supports multi-source filtering. + source: source.length === 1 ? source[0] : undefined, + }, + }, + })) + const logs = logsQuery.data?.data ?? [] + const totalPages = Math.max(Math.ceil((logsQuery.data?.total ?? 0) / limit), 1) + const currentPage = logsQuery.data?.page ?? page return (
    - + onChange={(nextSource) => { + setPage(1) + setSource(nextSource) + }} + /> { - setPage(1) - setSortBy(value) - }} + onSelect={() => {}} />
    -
    -
    - - - - -
    - - - - - - - - - {rows.length > 0 - ? rows.map(log => ( - - - - {log.title} - - - {log.endUser} - - - {log.messageCount} - - - {log.userRate} - - - {log.operationRate} - - - {log.updatedTime} - - - {log.createdTime} - - - )) - : ( - - - - )} - -
    - -
    - {t('agentDetail.logs.empty')} -
    -
    -
    - - - -
    -
    + { + void logsQuery.refetch() + }} + />
    ) } - -type LogsTableHeaderLabels = { - unread: string - title: string - endUser: string - messageCount: string - userRate: string - operationRate: string - updatedTime: string - createdTime: string -} - -function LogsTableHeader({ - labels, - rowClassName, -}: { - labels: LogsTableHeaderLabels - rowClassName?: string -}) { - return ( - - - - {labels.unread} - - {labels.title} - {labels.endUser} - {labels.messageCount} - {labels.userRate} - {labels.operationRate} - {labels.updatedTime} - {labels.createdTime} - - - ) -} - -function LogsTableColGroup() { - return ( - - - - - - - - - - - ) -} - -function TableHead({ - className, - ...props -}: ThHTMLAttributes) { - return ( - - ) -} - -function TableCell({ - children, - className, - ...props -}: TdHTMLAttributes) { - return ( - -
    - {children} -
    - - ) -} diff --git a/web/features/agent-v2/agent-detail/page.tsx b/web/features/agent-v2/agent-detail/page.tsx index 7b23ece3869..61fcf03381a 100644 --- a/web/features/agent-v2/agent-detail/page.tsx +++ b/web/features/agent-v2/agent-detail/page.tsx @@ -19,7 +19,7 @@ export function AgentDetailPage({ return if (section === 'logs') - return + return if (section === 'access') return diff --git a/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx b/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx index 85e0f0a9d79..687133f25e7 100644 --- a/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/agent-roster-list.spec.tsx @@ -1,16 +1,47 @@ import type { ComponentProps } from 'react' import type { AgentRosterListItem } from '../agent-roster-list' +import { toast } from '@langgenius/dify-ui/toast' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { AgentRosterList } from '../agent-roster-list' +const { duplicateAgentMutationFn } = vi.hoisted(() => ({ + duplicateAgentMutationFn: vi.fn(), +})) + vi.mock('@/hooks/use-timestamp', () => ({ default: () => ({ formatTime: () => '06/12/2026 12:00:00 PM', }), })) +vi.mock('@/service/client', () => ({ + consoleQuery: { + agent: { + byAgentId: { + copy: { + post: { + mutationOptions: () => ({ + mutationFn: duplicateAgentMutationFn, + }), + }, + }, + delete: { + mutationOptions: () => ({ + mutationFn: vi.fn(), + }), + }, + put: { + mutationOptions: () => ({ + mutationFn: vi.fn(), + }), + }, + }, + }, + }, +})) + const createAgent = (overrides: Partial = {}): AgentRosterListItem => ({ active_config_is_published: false, description: 'Find and summarize market materials.', @@ -51,6 +82,16 @@ const renderList = ( describe('AgentRosterList', () => { beforeEach(() => { vi.clearAllMocks() + vi.spyOn(toast, 'error').mockReturnValue('toast-id') + vi.spyOn(toast, 'success').mockReturnValue('toast-id') + duplicateAgentMutationFn.mockResolvedValue(createAgent({ + id: 'agent-copy', + name: 'Research Agent copy', + })) + }) + + afterEach(() => { + vi.restoreAllMocks() }) it('renders role under the card title instead of the agent mode', () => { @@ -68,6 +109,28 @@ describe('AgentRosterList', () => { expect(screen.getByText('agentV2.roster.usageStatus.draft')).toHaveClass('system-2xs-medium-uppercase') }) + it('draws the primary link focus ring above the draft corner label without z-index', () => { + renderList([createAgent()]) + + const configureLink = screen.getByRole('link', { name: 'Research Agent' }) + const draftLabel = screen.getByText('agentV2.roster.usageStatus.draft') + const draftCornerLabel = draftLabel.closest('.absolute') + + expect(configureLink).toHaveClass( + 'relative', + 'focus-visible:after:ring-2', + 'focus-visible:after:ring-state-accent-solid', + 'focus-visible:after:ring-inset', + ) + expect(configureLink).not.toHaveClass('peer/card-link') + expect(draftCornerLabel && configureLink.contains(draftCornerLabel)).toBe(true) + expect(draftCornerLabel).toHaveClass( + 'top-[-0.5px]', + 'right-0', + ) + expect(draftCornerLabel).not.toHaveClass('z-10', 'z-20') + }) + it('only renders the draft badge for unpublished agents', () => { renderList([ createAgent({ @@ -137,4 +200,27 @@ describe('AgentRosterList', () => { expect(workflowLink).toHaveAttribute('href', '/app/workflow-app-id/workflow') expect(screen.getByText(/agentV2\.roster\.references\.label/)).toBeInTheDocument() }) + + it('duplicates an agent from the card action menu', async () => { + const user = userEvent.setup() + renderList([createAgent()]) + + await user.click(screen.getByRole('button', { name: /agentV2\.roster\.moreActions/ })) + await user.click(screen.getByRole('menuitem', { name: /common\.operation\.duplicate/ })) + + expect(duplicateAgentMutationFn).toHaveBeenCalledWith( + { + params: { + agent_id: 'agent-1', + }, + body: {}, + }, + expect.objectContaining({ + client: expect.any(QueryClient), + }), + ) + await waitFor(() => { + expect(toast.success).toHaveBeenCalledWith('agentV2.roster.duplicateSuccess') + }) + }) }) diff --git a/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx b/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx index e340936678a..57fb616246f 100644 --- a/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/create-agent-dialog.spec.tsx @@ -61,9 +61,10 @@ describe('CreateAgentDialog', () => { icon_background: '#F5F3FF', }, }, expect.objectContaining({ - onError: expect.any(Function), onSuccess: expect.any(Function), })) + const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1] + expect(mutationOptions).not.toHaveProperty('onError') }) it('shows a field error when creating with an empty name', async () => { diff --git a/web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx b/web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx index cc69c2d1b13..be9d113c79f 100644 --- a/web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx +++ b/web/features/agent-v2/roster/components/__tests__/edit-agent-dialog.spec.tsx @@ -111,9 +111,10 @@ describe('EditAgentDialog', () => { icon_background: '#F5F3FF', }, }, expect.objectContaining({ - onError: expect.any(Function), onSuccess: expect.any(Function), })) + const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1] + expect(mutationOptions).not.toHaveProperty('onError') }) it('submits the full agent payload when only the role changes', async () => { @@ -138,9 +139,10 @@ describe('EditAgentDialog', () => { icon_background: '#F5F3FF', }, }, expect.objectContaining({ - onError: expect.any(Function), onSuccess: expect.any(Function), })) + const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1] + expect(mutationOptions).not.toHaveProperty('onError') }) it('submits selected icon fields when the roster icon changes', async () => { @@ -165,9 +167,10 @@ describe('EditAgentDialog', () => { icon_background: '#E0F2FE', }, }, expect.objectContaining({ - onError: expect.any(Function), onSuccess: expect.any(Function), })) + const mutationOptions = mutationMock.mutate.mock.calls[0]?.[1] + expect(mutationOptions).not.toHaveProperty('onError') }) it('shows a field error when saving with an empty name', async () => { diff --git a/web/features/agent-v2/roster/components/agent-roster-list.tsx b/web/features/agent-v2/roster/components/agent-roster-list.tsx index 9a1c473228c..f59632791e2 100644 --- a/web/features/agent-v2/roster/components/agent-roster-list.tsx +++ b/web/features/agent-v2/roster/components/agent-roster-list.tsx @@ -9,12 +9,15 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@langgenius/dify-ui/dropdown-menu' +import { toast } from '@langgenius/dify-ui/toast' +import { useMutation } from '@tanstack/react-query' import { useId, useState } from 'react' import { useTranslation } from 'react-i18next' import AppIcon from '@/app/components/base/app-icon' import { SkeletonRectangle } from '@/app/components/base/skeleton' import useTimestamp from '@/hooks/use-timestamp' import Link from '@/next/link' +import { consoleQuery } from '@/service/client' import { AgentWorkflowReferencesDropdown } from './agent-workflow-references-dropdown' import { DeleteAgentDialog } from './delete-agent-dialog' import { EditAgentDialog } from './edit-agent-dialog' @@ -102,6 +105,7 @@ function AgentRosterItem({ const descriptionId = useId() const [isEditOpen, setIsEditOpen] = useState(false) const [isDeleteOpen, setIsDeleteOpen] = useState(false) + const duplicateAgentMutation = useMutation(consoleQuery.agent.byAgentId.copy.post.mutationOptions()) const updatedAt = agent.updated_at != null ? formatTime(agent.updated_at, t('roster.dateTimeFormat')) : null @@ -111,6 +115,24 @@ function AgentRosterItem({ const isDraft = agent.active_config_is_published !== true const imageUrl = (agent.icon_type === 'image' || agent.icon_type === 'link') ? agent.icon : undefined const iconType = (imageUrl ? 'image' : agent.icon_type) as AgentIconType | null | undefined + const handleDuplicate = () => { + if (duplicateAgentMutation.isPending) + return + + duplicateAgentMutation.mutate({ + params: { + agent_id: agent.id, + }, + body: {}, + }, { + onSuccess: () => { + toast.success(t('roster.duplicateSuccess')) + }, + onError: () => { + toast.error(t('roster.duplicateFailed')) + }, + }) + } return (
    @@ -119,7 +141,7 @@ function AgentRosterItem({ href={`/roster/agent/${agent.id}/configure`} aria-labelledby={nameId} aria-describedby={agent.description ? descriptionId : undefined} - className="block shrink-0 cursor-pointer touch-manipulation rounded-xl outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset" + className="relative block shrink-0 cursor-pointer touch-manipulation rounded-xl outline-hidden after:pointer-events-none after:absolute after:inset-0 after:rounded-xl after:content-[''] focus-visible:after:ring-2 focus-visible:after:ring-state-accent-solid focus-visible:after:ring-inset" >
    @@ -146,6 +168,14 @@ function AgentRosterItem({ {agent.description}
    + {isDraft && ( +
    +
    +
    + {t('roster.usageStatus.draft')} +
    +
    + )}
    @@ -172,14 +202,6 @@ function AgentRosterItem({
    - {isDraft && ( -
    -
    -
    - {t('roster.usageStatus.draft')} -
    -
    - )}
    @@ -196,7 +218,11 @@ function AgentRosterItem({ {t('roster.editInfo')} - + {tCommon('operation.duplicate')} diff --git a/web/features/agent-v2/roster/components/create-agent-dialog.tsx b/web/features/agent-v2/roster/components/create-agent-dialog.tsx index de6b5f24ff2..abda2faa6af 100644 --- a/web/features/agent-v2/roster/components/create-agent-dialog.tsx +++ b/web/features/agent-v2/roster/components/create-agent-dialog.tsx @@ -70,9 +70,6 @@ export function CreateAgentDialog() { toast.success(t('roster.createSuccess')) handleOpenChange(false) }, - onError: () => { - toast.error(t('roster.createFailed')) - }, }) } diff --git a/web/features/agent-v2/roster/components/edit-agent-dialog.tsx b/web/features/agent-v2/roster/components/edit-agent-dialog.tsx index cdcfe0c6d3f..3688476972a 100644 --- a/web/features/agent-v2/roster/components/edit-agent-dialog.tsx +++ b/web/features/agent-v2/roster/components/edit-agent-dialog.tsx @@ -136,9 +136,6 @@ export function EditAgentDialog({ toast.success(t('roster.updateSuccess')) onOpenChange(false) }, - onError: () => { - toast.error(t('roster.updateFailed')) - }, }) } diff --git a/web/i18n/ar-TN/agent-v-2.json b/web/i18n/ar-TN/agent-v-2.json index 10c4ae688dc..0345d06d865 100644 --- a/web/i18n/ar-TN/agent-v-2.json +++ b/web/i18n/ar-TN/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "الملفات", "agentDetail.configure.files.preview.empty": "لا يوجد محتوى معاينة.", "agentDetail.configure.files.preview.failed": "فشل تحميل المعاينة.", + "agentDetail.configure.files.preview.unsupported": "هذا الملف لا يدعم المعاينة.", "agentDetail.configure.files.remove": "إزالة {{name}}", "agentDetail.configure.files.tip": "الملفات التي يمكن لهذا الوكيل استخدامها أثناء تنسيق المهام.", "agentDetail.configure.files.toggle": "تبديل الملفات", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "آخر وقت إنشاء", "agentDetail.logs.filters.sort.lastUpdatedTime": "آخر وقت تحديث", "agentDetail.logs.filters.source.all": "المصدر", + "agentDetail.logs.filters.source.empty": "لم يتم العثور على مصادر", "agentDetail.logs.filters.source.label": "مصدر السجل", + "agentDetail.logs.filters.source.loadFailed": "تعذر تحميل المصادر", + "agentDetail.logs.filters.source.loading": "جارٍ تحميل المصادر…", + "agentDetail.logs.filters.source.searchLabel": "البحث في المصادر", + "agentDetail.logs.filters.source.searchPlaceholder": "بحث", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "سير العمل", "agentDetail.logs.learnMore": "اعرف المزيد", + "agentDetail.logs.loadFailed": "تعذر تحميل السجلات", + "agentDetail.logs.loading": "جارٍ تحميل السجلات…", + "agentDetail.logs.notAvailable": "غير متاح", "agentDetail.logs.table.createdTime": "وقت الإنشاء", "agentDetail.logs.table.endUser": "المستخدم النهائي", "agentDetail.logs.table.messageCount": "عدد الرسائل", "agentDetail.logs.table.operationRate": "معدل العمليات", + "agentDetail.logs.table.source": "المصدر", "agentDetail.logs.table.title": "العنوان", "agentDetail.logs.table.unread": "غير مقروء", "agentDetail.logs.table.updatedTime": "وقت التحديث", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "خيارات إنشاء الوكيل", "roster.createDialog.description": "أنشئ وكيلاً قابلاً لإعادة الاستخدام في Roster مساحة العمل هذه.", "roster.createDialog.title": "إنشاء وكيل", - "roster.createFailed": "فشل إنشاء الوكيل.", "roster.createForm.changeIcon": "تغيير أيقونة الوكيل", "roster.createForm.descriptionLabel": "الوصف", "roster.createForm.descriptionOptional": "(اختياري)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "حذف {{name}}؟", "roster.deleteFailed": "فشل حذف الوكيل.", "roster.deleteSuccess": "تم حذف الوكيل.", + "roster.duplicateFailed": "فشل نسخ الوكيل.", + "roster.duplicateSuccess": "تم نسخ الوكيل.", "roster.editAgent": "تعديل {{name}}", "roster.editDialog.description": "حدّث اسم Roster والوصف والدور لهذا الوكيل.", "roster.editDialog.title": "تعديل الوكيل", @@ -376,7 +387,6 @@ "roster.tabs.agent": "وكيل", "roster.tabs.human": "إنسان", "roster.tabsLabel": "نوع Roster", - "roster.updateFailed": "فشل تحديث الوكيل. تحقق من الحقول وحاول مرة أخرى.", "roster.updateSuccess": "تم تحديث الوكيل.", "roster.usageStatus.draft": "مسودة" } diff --git a/web/i18n/de-DE/agent-v-2.json b/web/i18n/de-DE/agent-v-2.json index 5a17c14e1b2..6a346017576 100644 --- a/web/i18n/de-DE/agent-v-2.json +++ b/web/i18n/de-DE/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Dateien", "agentDetail.configure.files.preview.empty": "Kein Vorschauinhalt.", "agentDetail.configure.files.preview.failed": "Vorschau konnte nicht geladen werden.", + "agentDetail.configure.files.preview.unsupported": "Für diese Datei wird keine Vorschau unterstützt.", "agentDetail.configure.files.remove": "{{name}} entfernen", "agentDetail.configure.files.tip": "Dateien, die dieser Agent bei der Orchestrierung von Aufgaben verwenden kann.", "agentDetail.configure.files.toggle": "Dateien umschalten", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Zuletzt erstellt", "agentDetail.logs.filters.sort.lastUpdatedTime": "Zuletzt aktualisiert", "agentDetail.logs.filters.source.all": "Quelle", + "agentDetail.logs.filters.source.empty": "Keine Quellen gefunden", "agentDetail.logs.filters.source.label": "Log-Quelle", + "agentDetail.logs.filters.source.loadFailed": "Quellen konnten nicht geladen werden", + "agentDetail.logs.filters.source.loading": "Quellen werden geladen…", + "agentDetail.logs.filters.source.searchLabel": "Quellen suchen", + "agentDetail.logs.filters.source.searchPlaceholder": "Suchen", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Workflow", "agentDetail.logs.learnMore": "Mehr erfahren", + "agentDetail.logs.loadFailed": "Logs konnten nicht geladen werden", + "agentDetail.logs.loading": "Logs werden geladen…", + "agentDetail.logs.notAvailable": "Nicht verfügbar", "agentDetail.logs.table.createdTime": "Erstellungszeit", "agentDetail.logs.table.endUser": "Endbenutzer", "agentDetail.logs.table.messageCount": "Nachr. Anzahl", "agentDetail.logs.table.operationRate": "Operationsrate", + "agentDetail.logs.table.source": "Quelle", "agentDetail.logs.table.title": "Titel", "agentDetail.logs.table.unread": "Ungelesen", "agentDetail.logs.table.updatedTime": "Aktualisierungszeit", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Optionen zum Erstellen eines Agenten", "roster.createDialog.description": "Erstellen Sie einen wiederverwendbaren Agenten im Roster dieses Workspaces.", "roster.createDialog.title": "Agent erstellen", - "roster.createFailed": "Agent konnte nicht erstellt werden.", "roster.createForm.changeIcon": "Agenten-Symbol ändern", "roster.createForm.descriptionLabel": "Beschreibung", "roster.createForm.descriptionOptional": "(optional)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "{{name}} löschen?", "roster.deleteFailed": "Agent konnte nicht gelöscht werden.", "roster.deleteSuccess": "Agent gelöscht.", + "roster.duplicateFailed": "Agent konnte nicht dupliziert werden.", + "roster.duplicateSuccess": "Agent dupliziert.", "roster.editAgent": "{{name}} bearbeiten", "roster.editDialog.description": "Aktualisieren Sie Roster-Name, Beschreibung und Rolle dieses Agenten.", "roster.editDialog.title": "Agent bearbeiten", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agent", "roster.tabs.human": "Mensch", "roster.tabsLabel": "Roster-Typ", - "roster.updateFailed": "Agent konnte nicht aktualisiert werden. Bitte Felder überprüfen und erneut versuchen.", "roster.updateSuccess": "Agent aktualisiert.", "roster.usageStatus.draft": "Entwurf" } diff --git a/web/i18n/en-US/agent-v-2.json b/web/i18n/en-US/agent-v-2.json index f94d9197ad7..6ce81aa591e 100644 --- a/web/i18n/en-US/agent-v-2.json +++ b/web/i18n/en-US/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Files", "agentDetail.configure.files.preview.empty": "No preview content.", "agentDetail.configure.files.preview.failed": "Failed to load preview.", + "agentDetail.configure.files.preview.unsupported": "Preview is not supported for this file.", "agentDetail.configure.files.remove": "Remove {{name}}", "agentDetail.configure.files.tip": "Files this agent can use while orchestrating tasks.", "agentDetail.configure.files.toggle": "Toggle files", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Last Created time", "agentDetail.logs.filters.sort.lastUpdatedTime": "Last Updated time", "agentDetail.logs.filters.source.all": "Source", + "agentDetail.logs.filters.source.empty": "No sources found", "agentDetail.logs.filters.source.label": "Log source", + "agentDetail.logs.filters.source.loadFailed": "Failed to load sources", + "agentDetail.logs.filters.source.loading": "Loading sources…", + "agentDetail.logs.filters.source.searchLabel": "Search sources", + "agentDetail.logs.filters.source.searchPlaceholder": "Search", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Workflow", "agentDetail.logs.learnMore": "Learn more", + "agentDetail.logs.loadFailed": "Failed to load logs", + "agentDetail.logs.loading": "Loading logs…", + "agentDetail.logs.notAvailable": "N/A", "agentDetail.logs.table.createdTime": "Created Time", "agentDetail.logs.table.endUser": "End-user", "agentDetail.logs.table.messageCount": "Msg. Count", "agentDetail.logs.table.operationRate": "Op. Rate", + "agentDetail.logs.table.source": "Source", "agentDetail.logs.table.title": "Title", "agentDetail.logs.table.unread": "Unread", "agentDetail.logs.table.updatedTime": "Updated Time", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Create agent options", "roster.createDialog.description": "Create a reusable agent in this workspace roster.", "roster.createDialog.title": "Create agent", - "roster.createFailed": "Failed to create agent.", "roster.createForm.changeIcon": "Change agent icon", "roster.createForm.descriptionLabel": "Description", "roster.createForm.descriptionOptional": "(optional)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Delete {{name}}?", "roster.deleteFailed": "Failed to delete agent.", "roster.deleteSuccess": "Agent deleted.", + "roster.duplicateFailed": "Failed to duplicate agent.", + "roster.duplicateSuccess": "Agent duplicated.", "roster.editAgent": "Edit {{name}}", "roster.editDialog.description": "Update the roster name, description, and role for this agent.", "roster.editDialog.title": "Edit agent", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agent", "roster.tabs.human": "Human", "roster.tabsLabel": "Roster type", - "roster.updateFailed": "Failed to update agent. Check the fields and try again.", "roster.updateSuccess": "Agent updated.", "roster.usageStatus.draft": "Draft" } diff --git a/web/i18n/es-ES/agent-v-2.json b/web/i18n/es-ES/agent-v-2.json index 07a21f18850..fb23b2a523e 100644 --- a/web/i18n/es-ES/agent-v-2.json +++ b/web/i18n/es-ES/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Archivos", "agentDetail.configure.files.preview.empty": "Sin contenido de vista previa.", "agentDetail.configure.files.preview.failed": "Error al cargar la vista previa.", + "agentDetail.configure.files.preview.unsupported": "Este archivo no admite vista previa.", "agentDetail.configure.files.remove": "Eliminar {{name}}", "agentDetail.configure.files.tip": "Archivos que este agente puede usar al orquestar tareas.", "agentDetail.configure.files.toggle": "Alternar archivos", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Última fecha de creación", "agentDetail.logs.filters.sort.lastUpdatedTime": "Última fecha de actualización", "agentDetail.logs.filters.source.all": "Fuente", + "agentDetail.logs.filters.source.empty": "No se encontraron fuentes", "agentDetail.logs.filters.source.label": "Fuente del registro", + "agentDetail.logs.filters.source.loadFailed": "No se pudieron cargar las fuentes", + "agentDetail.logs.filters.source.loading": "Cargando fuentes…", + "agentDetail.logs.filters.source.searchLabel": "Buscar fuentes", + "agentDetail.logs.filters.source.searchPlaceholder": "Buscar", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Flujo de trabajo", "agentDetail.logs.learnMore": "Más información", + "agentDetail.logs.loadFailed": "No se pudieron cargar los registros", + "agentDetail.logs.loading": "Cargando registros…", + "agentDetail.logs.notAvailable": "N/D", "agentDetail.logs.table.createdTime": "Fecha de creación", "agentDetail.logs.table.endUser": "Usuario final", "agentDetail.logs.table.messageCount": "Cant. mens.", "agentDetail.logs.table.operationRate": "Tasa op.", + "agentDetail.logs.table.source": "Fuente", "agentDetail.logs.table.title": "Título", "agentDetail.logs.table.unread": "Sin leer", "agentDetail.logs.table.updatedTime": "Fecha de actualización", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Opciones para crear agente", "roster.createDialog.description": "Crea un agente reutilizable en el roster de este workspace.", "roster.createDialog.title": "Crear agente", - "roster.createFailed": "Error al crear el agente.", "roster.createForm.changeIcon": "Cambiar icono del agente", "roster.createForm.descriptionLabel": "Descripción", "roster.createForm.descriptionOptional": "(opcional)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "¿Eliminar {{name}}?", "roster.deleteFailed": "Error al eliminar el agente.", "roster.deleteSuccess": "Agente eliminado.", + "roster.duplicateFailed": "Error al duplicar el agente.", + "roster.duplicateSuccess": "Agente duplicado.", "roster.editAgent": "Editar {{name}}", "roster.editDialog.description": "Actualiza el nombre, la descripción y el rol del roster para este agente.", "roster.editDialog.title": "Editar agente", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agente", "roster.tabs.human": "Humano", "roster.tabsLabel": "Tipo de roster", - "roster.updateFailed": "Error al actualizar el agente. Comprueba los campos y vuelve a intentarlo.", "roster.updateSuccess": "Agente actualizado.", "roster.usageStatus.draft": "Borrador" } diff --git a/web/i18n/fa-IR/agent-v-2.json b/web/i18n/fa-IR/agent-v-2.json index 0a641be4ade..45b5891dc26 100644 --- a/web/i18n/fa-IR/agent-v-2.json +++ b/web/i18n/fa-IR/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "فایل‌ها", "agentDetail.configure.files.preview.empty": "محتوای پیش‌نمایش وجود ندارد.", "agentDetail.configure.files.preview.failed": "بارگذاری پیش‌نمایش ناموفق بود.", + "agentDetail.configure.files.preview.unsupported": "این فایل از پیش‌نمایش پشتیبانی نمی‌کند.", "agentDetail.configure.files.remove": "حذف {{name}}", "agentDetail.configure.files.tip": "فایل‌هایی که این عامل هنگام هماهنگی وظایف می‌تواند استفاده کند.", "agentDetail.configure.files.toggle": "تغییر فایل‌ها", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "آخرین زمان ایجاد", "agentDetail.logs.filters.sort.lastUpdatedTime": "آخرین زمان به‌روزرسانی", "agentDetail.logs.filters.source.all": "منبع", + "agentDetail.logs.filters.source.empty": "منبعی پیدا نشد", "agentDetail.logs.filters.source.label": "منبع گزارش", + "agentDetail.logs.filters.source.loadFailed": "بارگیری منابع ناموفق بود", + "agentDetail.logs.filters.source.loading": "در حال بارگیری منابع…", + "agentDetail.logs.filters.source.searchLabel": "جستجوی منابع", + "agentDetail.logs.filters.source.searchPlaceholder": "جستجو", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "گردش کار", "agentDetail.logs.learnMore": "اطلاعات بیشتر", + "agentDetail.logs.loadFailed": "بارگیری گزارش‌ها ناموفق بود", + "agentDetail.logs.loading": "در حال بارگیری گزارش‌ها…", + "agentDetail.logs.notAvailable": "ناموجود", "agentDetail.logs.table.createdTime": "زمان ایجاد", "agentDetail.logs.table.endUser": "کاربر نهایی", "agentDetail.logs.table.messageCount": "تعداد پیام", "agentDetail.logs.table.operationRate": "نرخ عملیات", + "agentDetail.logs.table.source": "منبع", "agentDetail.logs.table.title": "عنوان", "agentDetail.logs.table.unread": "خوانده‌نشده", "agentDetail.logs.table.updatedTime": "زمان به‌روزرسانی", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "گزینه‌های ایجاد عامل", "roster.createDialog.description": "یک عامل قابل استفاده مجدد در Roster این فضای کاری ایجاد کنید.", "roster.createDialog.title": "ایجاد عامل", - "roster.createFailed": "ایجاد عامل ناموفق بود.", "roster.createForm.changeIcon": "تغییر آیکون عامل", "roster.createForm.descriptionLabel": "توضیحات", "roster.createForm.descriptionOptional": "(اختیاری)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "حذف {{name}}؟", "roster.deleteFailed": "حذف عامل ناموفق بود.", "roster.deleteSuccess": "عامل حذف شد.", + "roster.duplicateFailed": "تکثیر عامل ناموفق بود.", + "roster.duplicateSuccess": "عامل تکثیر شد.", "roster.editAgent": "ویرایش {{name}}", "roster.editDialog.description": "نام، توضیحات و نقش Roster این عامل را به‌روزرسانی کنید.", "roster.editDialog.title": "ویرایش عامل", @@ -376,7 +387,6 @@ "roster.tabs.agent": "عامل", "roster.tabs.human": "انسان", "roster.tabsLabel": "نوع Roster", - "roster.updateFailed": "به‌روزرسانی عامل ناموفق بود. فیلدها را بررسی کنید و دوباره امتحان کنید.", "roster.updateSuccess": "عامل به‌روزرسانی شد.", "roster.usageStatus.draft": "پیش‌نویس" } diff --git a/web/i18n/fr-FR/agent-v-2.json b/web/i18n/fr-FR/agent-v-2.json index 1259541d8a1..804b2624f44 100644 --- a/web/i18n/fr-FR/agent-v-2.json +++ b/web/i18n/fr-FR/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Fichiers", "agentDetail.configure.files.preview.empty": "Aucun contenu d’aperçu.", "agentDetail.configure.files.preview.failed": "Échec du chargement de l’aperçu.", + "agentDetail.configure.files.preview.unsupported": "Ce fichier ne prend pas en charge l’aperçu.", "agentDetail.configure.files.remove": "Supprimer {{name}}", "agentDetail.configure.files.tip": "Fichiers que cet agent peut utiliser lors de l’orchestration des tâches.", "agentDetail.configure.files.toggle": "Basculer les fichiers", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Dernière date de création", "agentDetail.logs.filters.sort.lastUpdatedTime": "Dernière date de mise à jour", "agentDetail.logs.filters.source.all": "Source", + "agentDetail.logs.filters.source.empty": "Aucune source trouvée", "agentDetail.logs.filters.source.label": "Source du journal", + "agentDetail.logs.filters.source.loadFailed": "Échec du chargement des sources", + "agentDetail.logs.filters.source.loading": "Chargement des sources…", + "agentDetail.logs.filters.source.searchLabel": "Rechercher des sources", + "agentDetail.logs.filters.source.searchPlaceholder": "Rechercher", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Workflow", "agentDetail.logs.learnMore": "En savoir plus", + "agentDetail.logs.loadFailed": "Échec du chargement des journaux", + "agentDetail.logs.loading": "Chargement des journaux…", + "agentDetail.logs.notAvailable": "N/D", "agentDetail.logs.table.createdTime": "Date de création", "agentDetail.logs.table.endUser": "Utilisateur final", "agentDetail.logs.table.messageCount": "Nb. msg.", "agentDetail.logs.table.operationRate": "Taux d’op.", + "agentDetail.logs.table.source": "Source", "agentDetail.logs.table.title": "Titre", "agentDetail.logs.table.unread": "Non lu", "agentDetail.logs.table.updatedTime": "Date de mise à jour", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Options de création d’agent", "roster.createDialog.description": "Créez un agent réutilisable dans le roster de cet espace de travail.", "roster.createDialog.title": "Créer un agent", - "roster.createFailed": "Échec de la création de l’agent.", "roster.createForm.changeIcon": "Changer l’icône de l’agent", "roster.createForm.descriptionLabel": "Description", "roster.createForm.descriptionOptional": "(facultatif)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Supprimer {{name}} ?", "roster.deleteFailed": "Échec de la suppression de l’agent.", "roster.deleteSuccess": "Agent supprimé.", + "roster.duplicateFailed": "Échec de la duplication de l’agent.", + "roster.duplicateSuccess": "Agent dupliqué.", "roster.editAgent": "Modifier {{name}}", "roster.editDialog.description": "Mettez à jour le nom, la description et le rôle de cet agent dans le roster.", "roster.editDialog.title": "Modifier l’agent", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agent", "roster.tabs.human": "Humain", "roster.tabsLabel": "Type de roster", - "roster.updateFailed": "Échec de la mise à jour de l’agent. Vérifiez les champs et réessayez.", "roster.updateSuccess": "Agent mis à jour.", "roster.usageStatus.draft": "Brouillon" } diff --git a/web/i18n/hi-IN/agent-v-2.json b/web/i18n/hi-IN/agent-v-2.json index cb0af3bfeb1..ba5f7df6d58 100644 --- a/web/i18n/hi-IN/agent-v-2.json +++ b/web/i18n/hi-IN/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "फ़ाइलें", "agentDetail.configure.files.preview.empty": "कोई पूर्वावलोकन सामग्री नहीं।", "agentDetail.configure.files.preview.failed": "पूर्वावलोकन लोड करने में विफल।", + "agentDetail.configure.files.preview.unsupported": "यह फ़ाइल पूर्वावलोकन का समर्थन नहीं करती।", "agentDetail.configure.files.remove": "{{name}} हटाएँ", "agentDetail.configure.files.tip": "इस एजेंट द्वारा कार्यों को व्यवस्थित करते समय उपयोग की जा सकने वाली फ़ाइलें।", "agentDetail.configure.files.toggle": "फ़ाइलें टॉगल करें", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "अंतिम निर्मित समय", "agentDetail.logs.filters.sort.lastUpdatedTime": "अंतिम अद्यतन समय", "agentDetail.logs.filters.source.all": "स्रोत", + "agentDetail.logs.filters.source.empty": "कोई स्रोत नहीं मिला", "agentDetail.logs.filters.source.label": "लॉग स्रोत", + "agentDetail.logs.filters.source.loadFailed": "स्रोत लोड नहीं हो सके", + "agentDetail.logs.filters.source.loading": "स्रोत लोड हो रहे हैं…", + "agentDetail.logs.filters.source.searchLabel": "स्रोत खोजें", + "agentDetail.logs.filters.source.searchPlaceholder": "खोजें", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "वर्कफ़्लो", "agentDetail.logs.learnMore": "और अधिक जानें", + "agentDetail.logs.loadFailed": "लॉग लोड नहीं हो सके", + "agentDetail.logs.loading": "लॉग लोड हो रहे हैं…", + "agentDetail.logs.notAvailable": "उपलब्ध नहीं", "agentDetail.logs.table.createdTime": "निर्मित समय", "agentDetail.logs.table.endUser": "अंतिम-उपयोगकर्ता", "agentDetail.logs.table.messageCount": "संदेश संख्या", "agentDetail.logs.table.operationRate": "ऑपरेशन दर", + "agentDetail.logs.table.source": "स्रोत", "agentDetail.logs.table.title": "शीर्षक", "agentDetail.logs.table.unread": "अपठित", "agentDetail.logs.table.updatedTime": "अद्यतन समय", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "एजेंट बनाने के विकल्प", "roster.createDialog.description": "इस कार्यक्षेत्र Roster में एक पुनः उपयोग योग्य एजेंट बनाएँ।", "roster.createDialog.title": "एजेंट बनाएँ", - "roster.createFailed": "एजेंट बनाने में विफल।", "roster.createForm.changeIcon": "एजेंट आइकन बदलें", "roster.createForm.descriptionLabel": "विवरण", "roster.createForm.descriptionOptional": "(वैकल्पिक)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "{{name}} हटाएँ?", "roster.deleteFailed": "एजेंट हटाने में विफल।", "roster.deleteSuccess": "एजेंट हटा दिया गया।", + "roster.duplicateFailed": "एजेंट डुप्लिकेट करने में विफल।", + "roster.duplicateSuccess": "एजेंट डुप्लिकेट किया गया।", "roster.editAgent": "{{name}} संपादित करें", "roster.editDialog.description": "इस एजेंट का Roster नाम, विवरण और भूमिका अपडेट करें।", "roster.editDialog.title": "एजेंट संपादित करें", @@ -376,7 +387,6 @@ "roster.tabs.agent": "एजेंट", "roster.tabs.human": "मानव", "roster.tabsLabel": "Roster प्रकार", - "roster.updateFailed": "एजेंट अपडेट करने में विफल। फ़ील्ड जाँचें और पुनः प्रयास करें।", "roster.updateSuccess": "एजेंट अपडेट हो गया।", "roster.usageStatus.draft": "ड्राफ्ट" } diff --git a/web/i18n/id-ID/agent-v-2.json b/web/i18n/id-ID/agent-v-2.json index 516bbe0adc5..f2827f73016 100644 --- a/web/i18n/id-ID/agent-v-2.json +++ b/web/i18n/id-ID/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "File", "agentDetail.configure.files.preview.empty": "Tidak ada konten pratinjau.", "agentDetail.configure.files.preview.failed": "Gagal memuat pratinjau.", + "agentDetail.configure.files.preview.unsupported": "File ini tidak mendukung pratinjau.", "agentDetail.configure.files.remove": "Hapus {{name}}", "agentDetail.configure.files.tip": "File yang dapat digunakan agen ini saat mengorkestrasi tugas.", "agentDetail.configure.files.toggle": "Alihkan file", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Waktu dibuat terakhir", "agentDetail.logs.filters.sort.lastUpdatedTime": "Waktu diperbarui terakhir", "agentDetail.logs.filters.source.all": "Sumber", + "agentDetail.logs.filters.source.empty": "Tidak ada sumber ditemukan", "agentDetail.logs.filters.source.label": "Sumber log", + "agentDetail.logs.filters.source.loadFailed": "Gagal memuat sumber", + "agentDetail.logs.filters.source.loading": "Memuat sumber…", + "agentDetail.logs.filters.source.searchLabel": "Cari sumber", + "agentDetail.logs.filters.source.searchPlaceholder": "Cari", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Alur kerja", "agentDetail.logs.learnMore": "Pelajari lebih lanjut", + "agentDetail.logs.loadFailed": "Gagal memuat log", + "agentDetail.logs.loading": "Memuat log…", + "agentDetail.logs.notAvailable": "Tidak tersedia", "agentDetail.logs.table.createdTime": "Waktu Dibuat", "agentDetail.logs.table.endUser": "Pengguna akhir", "agentDetail.logs.table.messageCount": "Jumlah Pesan", "agentDetail.logs.table.operationRate": "Tingkat Operasi", + "agentDetail.logs.table.source": "Sumber", "agentDetail.logs.table.title": "Judul", "agentDetail.logs.table.unread": "Belum dibaca", "agentDetail.logs.table.updatedTime": "Waktu Diperbarui", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Opsi buat agen", "roster.createDialog.description": "Buat agen yang dapat digunakan kembali di Roster ruang kerja ini.", "roster.createDialog.title": "Buat agen", - "roster.createFailed": "Gagal membuat agen.", "roster.createForm.changeIcon": "Ubah ikon agen", "roster.createForm.descriptionLabel": "Deskripsi", "roster.createForm.descriptionOptional": "(opsional)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Hapus {{name}}?", "roster.deleteFailed": "Gagal menghapus agen.", "roster.deleteSuccess": "Agen dihapus.", + "roster.duplicateFailed": "Gagal menduplikasi agen.", + "roster.duplicateSuccess": "Agen diduplikasi.", "roster.editAgent": "Edit {{name}}", "roster.editDialog.description": "Perbarui nama, deskripsi, dan peran Roster untuk agen ini.", "roster.editDialog.title": "Edit agen", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agen", "roster.tabs.human": "Manusia", "roster.tabsLabel": "Tipe Roster", - "roster.updateFailed": "Gagal memperbarui agen. Periksa field dan coba lagi.", "roster.updateSuccess": "Agen diperbarui.", "roster.usageStatus.draft": "Draf" } diff --git a/web/i18n/it-IT/agent-v-2.json b/web/i18n/it-IT/agent-v-2.json index 5c71caa9569..a38e2808ab7 100644 --- a/web/i18n/it-IT/agent-v-2.json +++ b/web/i18n/it-IT/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "File", "agentDetail.configure.files.preview.empty": "Nessun contenuto in anteprima.", "agentDetail.configure.files.preview.failed": "Impossibile caricare l’anteprima.", + "agentDetail.configure.files.preview.unsupported": "Questo file non supporta l’anteprima.", "agentDetail.configure.files.remove": "Rimuovi {{name}}", "agentDetail.configure.files.tip": "File che questo agente può utilizzare durante l’orchestrazione delle attività.", "agentDetail.configure.files.toggle": "Attiva/disattiva file", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Ultima ora di creazione", "agentDetail.logs.filters.sort.lastUpdatedTime": "Ultima ora di aggiornamento", "agentDetail.logs.filters.source.all": "Origine", + "agentDetail.logs.filters.source.empty": "Nessuna fonte trovata", "agentDetail.logs.filters.source.label": "Origine del log", + "agentDetail.logs.filters.source.loadFailed": "Impossibile caricare le fonti", + "agentDetail.logs.filters.source.loading": "Caricamento fonti…", + "agentDetail.logs.filters.source.searchLabel": "Cerca fonti", + "agentDetail.logs.filters.source.searchPlaceholder": "Cerca", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Workflow", "agentDetail.logs.learnMore": "Scopri di più", + "agentDetail.logs.loadFailed": "Impossibile caricare i log", + "agentDetail.logs.loading": "Caricamento log…", + "agentDetail.logs.notAvailable": "N/D", "agentDetail.logs.table.createdTime": "Ora di creazione", "agentDetail.logs.table.endUser": "Utente finale", "agentDetail.logs.table.messageCount": "N. msg.", "agentDetail.logs.table.operationRate": "Tasso op.", + "agentDetail.logs.table.source": "Fonte", "agentDetail.logs.table.title": "Titolo", "agentDetail.logs.table.unread": "Non letto", "agentDetail.logs.table.updatedTime": "Ora di aggiornamento", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Opzioni di creazione agente", "roster.createDialog.description": "Crea un agente riutilizzabile nel roster di questo workspace.", "roster.createDialog.title": "Crea agente", - "roster.createFailed": "Impossibile creare l’agente.", "roster.createForm.changeIcon": "Cambia icona dell’agente", "roster.createForm.descriptionLabel": "Descrizione", "roster.createForm.descriptionOptional": "(facoltativo)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Eliminare {{name}}?", "roster.deleteFailed": "Impossibile eliminare l’agente.", "roster.deleteSuccess": "Agente eliminato.", + "roster.duplicateFailed": "Impossibile duplicare l’agente.", + "roster.duplicateSuccess": "Agente duplicato.", "roster.editAgent": "Modifica {{name}}", "roster.editDialog.description": "Aggiorna il nome, la descrizione e il ruolo del roster per questo agente.", "roster.editDialog.title": "Modifica agente", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agente", "roster.tabs.human": "Umano", "roster.tabsLabel": "Tipo di roster", - "roster.updateFailed": "Impossibile aggiornare l’agente. Controlla i campi e riprova.", "roster.updateSuccess": "Agente aggiornato.", "roster.usageStatus.draft": "Bozza" } diff --git a/web/i18n/ja-JP/agent-v-2.json b/web/i18n/ja-JP/agent-v-2.json index 925dd2c0a6a..2fd910cdb64 100644 --- a/web/i18n/ja-JP/agent-v-2.json +++ b/web/i18n/ja-JP/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "ファイル", "agentDetail.configure.files.preview.empty": "プレビュー内容はありません。", "agentDetail.configure.files.preview.failed": "プレビューの読み込みに失敗しました。", + "agentDetail.configure.files.preview.unsupported": "ファイルはプレビューに対応していません。", "agentDetail.configure.files.remove": "{{name}} を削除", "agentDetail.configure.files.tip": "このエージェントがタスクのオーケストレーション中に利用できるファイル。", "agentDetail.configure.files.toggle": "ファイルの表示を切り替え", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "最終作成日時", "agentDetail.logs.filters.sort.lastUpdatedTime": "最終更新日時", "agentDetail.logs.filters.source.all": "ソース", + "agentDetail.logs.filters.source.empty": "ソースが見つかりません", "agentDetail.logs.filters.source.label": "ログソース", + "agentDetail.logs.filters.source.loadFailed": "ソースの読み込みに失敗しました", + "agentDetail.logs.filters.source.loading": "ソースを読み込み中…", + "agentDetail.logs.filters.source.searchLabel": "ソースを検索", + "agentDetail.logs.filters.source.searchPlaceholder": "検索", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "ワークフロー", "agentDetail.logs.learnMore": "詳細を見る", + "agentDetail.logs.loadFailed": "ログの読み込みに失敗しました", + "agentDetail.logs.loading": "ログを読み込み中…", + "agentDetail.logs.notAvailable": "該当なし", "agentDetail.logs.table.createdTime": "作成日時", "agentDetail.logs.table.endUser": "エンドユーザー", "agentDetail.logs.table.messageCount": "メッセージ数", "agentDetail.logs.table.operationRate": "操作率", + "agentDetail.logs.table.source": "ソース", "agentDetail.logs.table.title": "タイトル", "agentDetail.logs.table.unread": "未読", "agentDetail.logs.table.updatedTime": "更新日時", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "エージェント作成オプション", "roster.createDialog.description": "このワークスペース Roster に再利用可能なエージェントを作成します。", "roster.createDialog.title": "エージェントを作成", - "roster.createFailed": "エージェントの作成に失敗しました。", "roster.createForm.changeIcon": "エージェントアイコンを変更", "roster.createForm.descriptionLabel": "説明", "roster.createForm.descriptionOptional": "(任意)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "{{name}} を削除しますか?", "roster.deleteFailed": "エージェントの削除に失敗しました。", "roster.deleteSuccess": "エージェントを削除しました。", + "roster.duplicateFailed": "エージェントの複製に失敗しました。", + "roster.duplicateSuccess": "エージェントを複製しました。", "roster.editAgent": "{{name}} を編集", "roster.editDialog.description": "このエージェントの Roster 名、説明、ロールを更新します。", "roster.editDialog.title": "エージェントを編集", @@ -376,7 +387,6 @@ "roster.tabs.agent": "エージェント", "roster.tabs.human": "人間", "roster.tabsLabel": "Roster タイプ", - "roster.updateFailed": "エージェントの更新に失敗しました。フィールドを確認してもう一度お試しください。", "roster.updateSuccess": "エージェントを更新しました。", "roster.usageStatus.draft": "ドラフト" } diff --git a/web/i18n/ko-KR/agent-v-2.json b/web/i18n/ko-KR/agent-v-2.json index 634ece71ae8..2356cdcef67 100644 --- a/web/i18n/ko-KR/agent-v-2.json +++ b/web/i18n/ko-KR/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "파일", "agentDetail.configure.files.preview.empty": "미리보기 내용이 없습니다.", "agentDetail.configure.files.preview.failed": "미리보기를 불러오지 못했습니다.", + "agentDetail.configure.files.preview.unsupported": "이 파일은 미리보기를 지원하지 않습니다.", "agentDetail.configure.files.remove": "{{name}} 제거", "agentDetail.configure.files.tip": "이 에이전트가 작업을 오케스트레이션할 때 사용할 수 있는 파일입니다.", "agentDetail.configure.files.toggle": "파일 전환", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "마지막 생성 시간", "agentDetail.logs.filters.sort.lastUpdatedTime": "마지막 업데이트 시간", "agentDetail.logs.filters.source.all": "소스", + "agentDetail.logs.filters.source.empty": "소스를 찾을 수 없습니다", "agentDetail.logs.filters.source.label": "로그 소스", + "agentDetail.logs.filters.source.loadFailed": "소스를 불러오지 못했습니다", + "agentDetail.logs.filters.source.loading": "소스를 불러오는 중…", + "agentDetail.logs.filters.source.searchLabel": "소스 검색", + "agentDetail.logs.filters.source.searchPlaceholder": "검색", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "워크플로", "agentDetail.logs.learnMore": "자세히 알아보기", + "agentDetail.logs.loadFailed": "로그를 불러오지 못했습니다", + "agentDetail.logs.loading": "로그를 불러오는 중…", + "agentDetail.logs.notAvailable": "없음", "agentDetail.logs.table.createdTime": "생성 시간", "agentDetail.logs.table.endUser": "최종 사용자", "agentDetail.logs.table.messageCount": "메시지 수", "agentDetail.logs.table.operationRate": "작업률", + "agentDetail.logs.table.source": "소스", "agentDetail.logs.table.title": "제목", "agentDetail.logs.table.unread": "읽지 않음", "agentDetail.logs.table.updatedTime": "업데이트 시간", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "에이전트 만들기 옵션", "roster.createDialog.description": "이 워크스페이스 Roster 에 재사용 가능한 에이전트를 만듭니다.", "roster.createDialog.title": "에이전트 만들기", - "roster.createFailed": "에이전트 생성에 실패했습니다.", "roster.createForm.changeIcon": "에이전트 아이콘 변경", "roster.createForm.descriptionLabel": "설명", "roster.createForm.descriptionOptional": "(선택 사항)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "{{name}}을(를) 삭제하시겠습니까?", "roster.deleteFailed": "에이전트 삭제에 실패했습니다.", "roster.deleteSuccess": "에이전트가 삭제되었습니다.", + "roster.duplicateFailed": "에이전트 복제에 실패했습니다.", + "roster.duplicateSuccess": "에이전트가 복제되었습니다.", "roster.editAgent": "{{name}} 편집", "roster.editDialog.description": "이 에이전트의 Roster 이름, 설명 및 역할을 업데이트합니다.", "roster.editDialog.title": "에이전트 편집", @@ -376,7 +387,6 @@ "roster.tabs.agent": "에이전트", "roster.tabs.human": "사람", "roster.tabsLabel": "Roster 유형", - "roster.updateFailed": "에이전트 업데이트에 실패했습니다. 필드를 확인 후 다시 시도하세요.", "roster.updateSuccess": "에이전트가 업데이트되었습니다.", "roster.usageStatus.draft": "초안" } diff --git a/web/i18n/nl-NL/agent-v-2.json b/web/i18n/nl-NL/agent-v-2.json index 97e4087f994..9209a7ed773 100644 --- a/web/i18n/nl-NL/agent-v-2.json +++ b/web/i18n/nl-NL/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Bestanden", "agentDetail.configure.files.preview.empty": "Geen voorbeeldinhoud.", "agentDetail.configure.files.preview.failed": "Laden van voorbeeld mislukt.", + "agentDetail.configure.files.preview.unsupported": "Dit bestand ondersteunt geen voorbeeldweergave.", "agentDetail.configure.files.remove": "{{name}} verwijderen", "agentDetail.configure.files.tip": "Bestanden die deze agent kan gebruiken tijdens het orkestreren van taken.", "agentDetail.configure.files.toggle": "Bestanden in/uit", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Laatst aangemaakt", "agentDetail.logs.filters.sort.lastUpdatedTime": "Laatst bijgewerkt", "agentDetail.logs.filters.source.all": "Bron", + "agentDetail.logs.filters.source.empty": "Geen bronnen gevonden", "agentDetail.logs.filters.source.label": "Logbron", + "agentDetail.logs.filters.source.loadFailed": "Bronnen laden mislukt", + "agentDetail.logs.filters.source.loading": "Bronnen laden…", + "agentDetail.logs.filters.source.searchLabel": "Bronnen zoeken", + "agentDetail.logs.filters.source.searchPlaceholder": "Zoeken", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Workflow", "agentDetail.logs.learnMore": "Meer informatie", + "agentDetail.logs.loadFailed": "Logboeken laden mislukt", + "agentDetail.logs.loading": "Logboeken laden…", + "agentDetail.logs.notAvailable": "Niet beschikbaar", "agentDetail.logs.table.createdTime": "Aanmaaktijd", "agentDetail.logs.table.endUser": "Eindgebruiker", "agentDetail.logs.table.messageCount": "Aantal ber.", "agentDetail.logs.table.operationRate": "Actiefrequentie", + "agentDetail.logs.table.source": "Bron", "agentDetail.logs.table.title": "Titel", "agentDetail.logs.table.unread": "Ongelezen", "agentDetail.logs.table.updatedTime": "Bijgewerkte tijd", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Opties voor agent aanmaken", "roster.createDialog.description": "Maak een herbruikbare agent in het roster van deze werkruimte.", "roster.createDialog.title": "Agent aanmaken", - "roster.createFailed": "Aanmaken van agent mislukt.", "roster.createForm.changeIcon": "Agentpictogram wijzigen", "roster.createForm.descriptionLabel": "Beschrijving", "roster.createForm.descriptionOptional": "(optioneel)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "{{name}} verwijderen?", "roster.deleteFailed": "Verwijderen van agent mislukt.", "roster.deleteSuccess": "Agent verwijderd.", + "roster.duplicateFailed": "Dupliceren van agent mislukt.", + "roster.duplicateSuccess": "Agent gedupliceerd.", "roster.editAgent": "{{name}} bewerken", "roster.editDialog.description": "Werk de rosternaam, beschrijving en rol van deze agent bij.", "roster.editDialog.title": "Agent bewerken", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agent", "roster.tabs.human": "Mens", "roster.tabsLabel": "Rostertype", - "roster.updateFailed": "Bijwerken van agent mislukt. Controleer de velden en probeer het opnieuw.", "roster.updateSuccess": "Agent bijgewerkt.", "roster.usageStatus.draft": "Concept" } diff --git a/web/i18n/pl-PL/agent-v-2.json b/web/i18n/pl-PL/agent-v-2.json index deaf3bc41b2..077cb79e70c 100644 --- a/web/i18n/pl-PL/agent-v-2.json +++ b/web/i18n/pl-PL/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Pliki", "agentDetail.configure.files.preview.empty": "Brak treści podglądu.", "agentDetail.configure.files.preview.failed": "Nie udało się załadować podglądu.", + "agentDetail.configure.files.preview.unsupported": "Ten plik nie obsługuje podglądu.", "agentDetail.configure.files.remove": "Usuń {{name}}", "agentDetail.configure.files.tip": "Pliki, których ten agent może używać podczas wykonywania zadań.", "agentDetail.configure.files.toggle": "Przełącz pliki", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Ostatnio utworzone", "agentDetail.logs.filters.sort.lastUpdatedTime": "Ostatnio zaktualizowane", "agentDetail.logs.filters.source.all": "Źródło", + "agentDetail.logs.filters.source.empty": "Nie znaleziono źródeł", "agentDetail.logs.filters.source.label": "Źródło logu", + "agentDetail.logs.filters.source.loadFailed": "Nie udało się wczytać źródeł", + "agentDetail.logs.filters.source.loading": "Wczytywanie źródeł…", + "agentDetail.logs.filters.source.searchLabel": "Szukaj źródeł", + "agentDetail.logs.filters.source.searchPlaceholder": "Szukaj", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Workflow", "agentDetail.logs.learnMore": "Dowiedz się więcej", + "agentDetail.logs.loadFailed": "Nie udało się wczytać logów", + "agentDetail.logs.loading": "Wczytywanie logów…", + "agentDetail.logs.notAvailable": "Brak danych", "agentDetail.logs.table.createdTime": "Czas utworzenia", "agentDetail.logs.table.endUser": "Użytkownik końcowy", "agentDetail.logs.table.messageCount": "Liczba wiad.", "agentDetail.logs.table.operationRate": "Współczynnik operacji", + "agentDetail.logs.table.source": "Źródło", "agentDetail.logs.table.title": "Tytuł", "agentDetail.logs.table.unread": "Nieprzeczytane", "agentDetail.logs.table.updatedTime": "Czas aktualizacji", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Opcje tworzenia agenta", "roster.createDialog.description": "Utwórz reużywalnego agenta w Roster tego workspace.", "roster.createDialog.title": "Utwórz agenta", - "roster.createFailed": "Nie udało się utworzyć agenta.", "roster.createForm.changeIcon": "Zmień ikonę agenta", "roster.createForm.descriptionLabel": "Opis", "roster.createForm.descriptionOptional": "(opcjonalnie)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Usunąć {{name}}?", "roster.deleteFailed": "Nie udało się usunąć agenta.", "roster.deleteSuccess": "Agent usunięty.", + "roster.duplicateFailed": "Nie udało się zduplikować agenta.", + "roster.duplicateSuccess": "Agent zduplikowany.", "roster.editAgent": "Edytuj {{name}}", "roster.editDialog.description": "Zaktualizuj nazwę w Roster, opis i rolę tego agenta.", "roster.editDialog.title": "Edytuj agenta", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agent", "roster.tabs.human": "Człowiek", "roster.tabsLabel": "Typ Roster", - "roster.updateFailed": "Nie udało się zaktualizować agenta. Sprawdź pola i spróbuj ponownie.", "roster.updateSuccess": "Agent zaktualizowany.", "roster.usageStatus.draft": "Wersja robocza" } diff --git a/web/i18n/pt-BR/agent-v-2.json b/web/i18n/pt-BR/agent-v-2.json index 1201a64f3a9..4e65b52b3c6 100644 --- a/web/i18n/pt-BR/agent-v-2.json +++ b/web/i18n/pt-BR/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Arquivos", "agentDetail.configure.files.preview.empty": "Sem conteúdo de pré-visualização.", "agentDetail.configure.files.preview.failed": "Falha ao carregar a pré-visualização.", + "agentDetail.configure.files.preview.unsupported": "Este arquivo não oferece suporte à pré-visualização.", "agentDetail.configure.files.remove": "Remover {{name}}", "agentDetail.configure.files.tip": "Arquivos que este agente pode usar ao orquestrar tarefas.", "agentDetail.configure.files.toggle": "Alternar arquivos", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Última hora de criação", "agentDetail.logs.filters.sort.lastUpdatedTime": "Última hora de atualização", "agentDetail.logs.filters.source.all": "Origem", + "agentDetail.logs.filters.source.empty": "Nenhuma fonte encontrada", "agentDetail.logs.filters.source.label": "Origem do log", + "agentDetail.logs.filters.source.loadFailed": "Falha ao carregar fontes", + "agentDetail.logs.filters.source.loading": "Carregando fontes…", + "agentDetail.logs.filters.source.searchLabel": "Pesquisar fontes", + "agentDetail.logs.filters.source.searchPlaceholder": "Pesquisar", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Workflow", "agentDetail.logs.learnMore": "Saiba mais", + "agentDetail.logs.loadFailed": "Falha ao carregar logs", + "agentDetail.logs.loading": "Carregando logs…", + "agentDetail.logs.notAvailable": "N/D", "agentDetail.logs.table.createdTime": "Hora de criação", "agentDetail.logs.table.endUser": "Usuário final", "agentDetail.logs.table.messageCount": "Qtd. msg.", "agentDetail.logs.table.operationRate": "Taxa de op.", + "agentDetail.logs.table.source": "Fonte", "agentDetail.logs.table.title": "Título", "agentDetail.logs.table.unread": "Não lido", "agentDetail.logs.table.updatedTime": "Hora de atualização", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Opções de criação de agente", "roster.createDialog.description": "Crie um agente reutilizável no roster deste workspace.", "roster.createDialog.title": "Criar agente", - "roster.createFailed": "Falha ao criar o agente.", "roster.createForm.changeIcon": "Alterar ícone do agente", "roster.createForm.descriptionLabel": "Descrição", "roster.createForm.descriptionOptional": "(opcional)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Excluir {{name}}?", "roster.deleteFailed": "Falha ao excluir o agente.", "roster.deleteSuccess": "Agente excluído.", + "roster.duplicateFailed": "Falha ao duplicar o agente.", + "roster.duplicateSuccess": "Agente duplicado.", "roster.editAgent": "Editar {{name}}", "roster.editDialog.description": "Atualize o nome, a descrição e a função deste agente no roster.", "roster.editDialog.title": "Editar agente", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agente", "roster.tabs.human": "Humano", "roster.tabsLabel": "Tipo de roster", - "roster.updateFailed": "Falha ao atualizar o agente. Verifique os campos e tente novamente.", "roster.updateSuccess": "Agente atualizado.", "roster.usageStatus.draft": "Rascunho" } diff --git a/web/i18n/ro-RO/agent-v-2.json b/web/i18n/ro-RO/agent-v-2.json index 5ca79bdb08f..f53d4f4c201 100644 --- a/web/i18n/ro-RO/agent-v-2.json +++ b/web/i18n/ro-RO/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Fișiere", "agentDetail.configure.files.preview.empty": "Niciun conținut de previzualizat.", "agentDetail.configure.files.preview.failed": "Încărcarea previzualizării a eșuat.", + "agentDetail.configure.files.preview.unsupported": "Acest fișier nu acceptă previzualizarea.", "agentDetail.configure.files.remove": "Elimină {{name}}", "agentDetail.configure.files.tip": "Fișiere pe care acest agent le poate folosi în timpul orchestrării sarcinilor.", "agentDetail.configure.files.toggle": "Comută fișierele", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Ultima dată de creare", "agentDetail.logs.filters.sort.lastUpdatedTime": "Ultima dată de actualizare", "agentDetail.logs.filters.source.all": "Sursă", + "agentDetail.logs.filters.source.empty": "Nu s-au găsit surse", "agentDetail.logs.filters.source.label": "Sursa jurnalului", + "agentDetail.logs.filters.source.loadFailed": "Nu s-au putut încărca sursele", + "agentDetail.logs.filters.source.loading": "Se încarcă sursele…", + "agentDetail.logs.filters.source.searchLabel": "Caută surse", + "agentDetail.logs.filters.source.searchPlaceholder": "Caută", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Workflow", "agentDetail.logs.learnMore": "Aflați mai multe", + "agentDetail.logs.loadFailed": "Nu s-au putut încărca jurnalele", + "agentDetail.logs.loading": "Se încarcă jurnalele…", + "agentDetail.logs.notAvailable": "Indisponibil", "agentDetail.logs.table.createdTime": "Data creării", "agentDetail.logs.table.endUser": "Utilizator final", "agentDetail.logs.table.messageCount": "Nr. mesaje", "agentDetail.logs.table.operationRate": "Rată op.", + "agentDetail.logs.table.source": "Sursă", "agentDetail.logs.table.title": "Titlu", "agentDetail.logs.table.unread": "Necitit", "agentDetail.logs.table.updatedTime": "Data actualizării", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Opțiuni de creare agent", "roster.createDialog.description": "Creați un agent reutilizabil în roster-ul acestui workspace.", "roster.createDialog.title": "Creează agent", - "roster.createFailed": "Crearea agentului a eșuat.", "roster.createForm.changeIcon": "Schimbă pictograma agentului", "roster.createForm.descriptionLabel": "Descriere", "roster.createForm.descriptionOptional": "(opțional)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Ștergeți {{name}}?", "roster.deleteFailed": "Ștergerea agentului a eșuat.", "roster.deleteSuccess": "Agent șters.", + "roster.duplicateFailed": "Duplicarea agentului a eșuat.", + "roster.duplicateSuccess": "Agent duplicat.", "roster.editAgent": "Editați {{name}}", "roster.editDialog.description": "Actualizați numele, descrierea și rolul din roster pentru acest agent.", "roster.editDialog.title": "Editați agentul", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agent", "roster.tabs.human": "Uman", "roster.tabsLabel": "Tip de roster", - "roster.updateFailed": "Actualizarea agentului a eșuat. Verificați câmpurile și încercați din nou.", "roster.updateSuccess": "Agent actualizat.", "roster.usageStatus.draft": "Ciornă" } diff --git a/web/i18n/ru-RU/agent-v-2.json b/web/i18n/ru-RU/agent-v-2.json index 93ac4b42b60..aec1fc7d82a 100644 --- a/web/i18n/ru-RU/agent-v-2.json +++ b/web/i18n/ru-RU/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Файлы", "agentDetail.configure.files.preview.empty": "Нет содержимого для предпросмотра.", "agentDetail.configure.files.preview.failed": "Не удалось загрузить предпросмотр.", + "agentDetail.configure.files.preview.unsupported": "Этот файл не поддерживает предварительный просмотр.", "agentDetail.configure.files.remove": "Удалить {{name}}", "agentDetail.configure.files.tip": "Файлы, которые этот агент может использовать при выполнении задач.", "agentDetail.configure.files.toggle": "Переключить файлы", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Время последнего создания", "agentDetail.logs.filters.sort.lastUpdatedTime": "Время последнего обновления", "agentDetail.logs.filters.source.all": "Источник", + "agentDetail.logs.filters.source.empty": "Источники не найдены", "agentDetail.logs.filters.source.label": "Источник журнала", + "agentDetail.logs.filters.source.loadFailed": "Не удалось загрузить источники", + "agentDetail.logs.filters.source.loading": "Загрузка источников…", + "agentDetail.logs.filters.source.searchLabel": "Поиск источников", + "agentDetail.logs.filters.source.searchPlaceholder": "Поиск", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Рабочий процесс", "agentDetail.logs.learnMore": "Подробнее", + "agentDetail.logs.loadFailed": "Не удалось загрузить журналы", + "agentDetail.logs.loading": "Загрузка журналов…", + "agentDetail.logs.notAvailable": "Н/Д", "agentDetail.logs.table.createdTime": "Время создания", "agentDetail.logs.table.endUser": "Конечный пользователь", "agentDetail.logs.table.messageCount": "Кол-во сообщений", "agentDetail.logs.table.operationRate": "Уровень операций", + "agentDetail.logs.table.source": "Источник", "agentDetail.logs.table.title": "Заголовок", "agentDetail.logs.table.unread": "Непрочитанные", "agentDetail.logs.table.updatedTime": "Время обновления", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Варианты создания агента", "roster.createDialog.description": "Создайте переиспользуемого агента в Roster этого рабочего пространства.", "roster.createDialog.title": "Создать агента", - "roster.createFailed": "Не удалось создать агента.", "roster.createForm.changeIcon": "Сменить иконку агента", "roster.createForm.descriptionLabel": "Описание", "roster.createForm.descriptionOptional": "(необязательно)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Удалить {{name}}?", "roster.deleteFailed": "Не удалось удалить агента.", "roster.deleteSuccess": "Агент удалён.", + "roster.duplicateFailed": "Не удалось продублировать агента.", + "roster.duplicateSuccess": "Агент продублирован.", "roster.editAgent": "Редактировать {{name}}", "roster.editDialog.description": "Обновите имя в Roster, описание и роль этого агента.", "roster.editDialog.title": "Редактировать агента", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Агент", "roster.tabs.human": "Человек", "roster.tabsLabel": "Тип Roster", - "roster.updateFailed": "Не удалось обновить агента. Проверьте поля и попробуйте снова.", "roster.updateSuccess": "Агент обновлён.", "roster.usageStatus.draft": "Черновик" } diff --git a/web/i18n/sl-SI/agent-v-2.json b/web/i18n/sl-SI/agent-v-2.json index 81fcd90da75..676c86c29df 100644 --- a/web/i18n/sl-SI/agent-v-2.json +++ b/web/i18n/sl-SI/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Datoteke", "agentDetail.configure.files.preview.empty": "Ni vsebine za predogled.", "agentDetail.configure.files.preview.failed": "Predogleda ni bilo mogoče naložiti.", + "agentDetail.configure.files.preview.unsupported": "Ta datoteka ne podpira predogleda.", "agentDetail.configure.files.remove": "Odstrani {{name}}", "agentDetail.configure.files.tip": "Datoteke, ki jih ta agent lahko uporablja pri izvajanju opravil.", "agentDetail.configure.files.toggle": "Preklopi datoteke", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Čas zadnje izdelave", "agentDetail.logs.filters.sort.lastUpdatedTime": "Čas zadnje posodobitve", "agentDetail.logs.filters.source.all": "Vir", + "agentDetail.logs.filters.source.empty": "Ni najdenih virov", "agentDetail.logs.filters.source.label": "Vir dnevnika", + "agentDetail.logs.filters.source.loadFailed": "Virov ni bilo mogoče naložiti", + "agentDetail.logs.filters.source.loading": "Nalaganje virov…", + "agentDetail.logs.filters.source.searchLabel": "Iskanje virov", + "agentDetail.logs.filters.source.searchPlaceholder": "Iskanje", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Potek dela", "agentDetail.logs.learnMore": "Več informacij", + "agentDetail.logs.loadFailed": "Dnevnikov ni bilo mogoče naložiti", + "agentDetail.logs.loading": "Nalaganje dnevnikov…", + "agentDetail.logs.notAvailable": "Ni na voljo", "agentDetail.logs.table.createdTime": "Čas izdelave", "agentDetail.logs.table.endUser": "Končni uporabnik", "agentDetail.logs.table.messageCount": "Št. sporočil", "agentDetail.logs.table.operationRate": "Stopnja operacij", + "agentDetail.logs.table.source": "Vir", "agentDetail.logs.table.title": "Naslov", "agentDetail.logs.table.unread": "Neprebrano", "agentDetail.logs.table.updatedTime": "Čas posodobitve", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Možnosti ustvarjanja agenta", "roster.createDialog.description": "Ustvarite ponovno uporabnega agenta v Roster tega delovnega prostora.", "roster.createDialog.title": "Ustvari agenta", - "roster.createFailed": "Agenta ni bilo mogoče ustvariti.", "roster.createForm.changeIcon": "Spremeni ikono agenta", "roster.createForm.descriptionLabel": "Opis", "roster.createForm.descriptionOptional": "(neobvezno)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Izbrišem {{name}}?", "roster.deleteFailed": "Agenta ni bilo mogoče izbrisati.", "roster.deleteSuccess": "Agent izbrisan.", + "roster.duplicateFailed": "Agenta ni bilo mogoče podvojiti.", + "roster.duplicateSuccess": "Agent podvojen.", "roster.editAgent": "Uredi {{name}}", "roster.editDialog.description": "Posodobite ime v Roster, opis in vlogo za tega agenta.", "roster.editDialog.title": "Uredi agenta", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Agent", "roster.tabs.human": "Človek", "roster.tabsLabel": "Tip Roster", - "roster.updateFailed": "Agenta ni bilo mogoče posodobiti. Preverite polja in poskusite znova.", "roster.updateSuccess": "Agent posodobljen.", "roster.usageStatus.draft": "Osnutek" } diff --git a/web/i18n/th-TH/agent-v-2.json b/web/i18n/th-TH/agent-v-2.json index 5e086c531a5..9f29760392a 100644 --- a/web/i18n/th-TH/agent-v-2.json +++ b/web/i18n/th-TH/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "ไฟล์", "agentDetail.configure.files.preview.empty": "ไม่มีเนื้อหาสำหรับแสดงตัวอย่าง", "agentDetail.configure.files.preview.failed": "โหลดตัวอย่างไม่สำเร็จ", + "agentDetail.configure.files.preview.unsupported": "ไฟล์นี้ไม่รองรับการแสดงตัวอย่าง", "agentDetail.configure.files.remove": "ลบ {{name}}", "agentDetail.configure.files.tip": "ไฟล์ที่ตัวแทนนี้สามารถใช้ได้ขณะจัดการงาน", "agentDetail.configure.files.toggle": "สลับไฟล์", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "เวลาสร้างล่าสุด", "agentDetail.logs.filters.sort.lastUpdatedTime": "เวลาอัปเดตล่าสุด", "agentDetail.logs.filters.source.all": "แหล่งที่มา", + "agentDetail.logs.filters.source.empty": "ไม่พบแหล่งที่มา", "agentDetail.logs.filters.source.label": "แหล่งที่มาของบันทึก", + "agentDetail.logs.filters.source.loadFailed": "โหลดแหล่งที่มาไม่สำเร็จ", + "agentDetail.logs.filters.source.loading": "กำลังโหลดแหล่งที่มา…", + "agentDetail.logs.filters.source.searchLabel": "ค้นหาแหล่งที่มา", + "agentDetail.logs.filters.source.searchPlaceholder": "ค้นหา", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "เวิร์กโฟลว์", "agentDetail.logs.learnMore": "เรียนรู้เพิ่มเติม", + "agentDetail.logs.loadFailed": "โหลดบันทึกไม่สำเร็จ", + "agentDetail.logs.loading": "กำลังโหลดบันทึก…", + "agentDetail.logs.notAvailable": "ไม่มีข้อมูล", "agentDetail.logs.table.createdTime": "เวลาที่สร้าง", "agentDetail.logs.table.endUser": "ผู้ใช้ปลายทาง", "agentDetail.logs.table.messageCount": "จำนวนข้อความ", "agentDetail.logs.table.operationRate": "อัตราการดำเนินการ", + "agentDetail.logs.table.source": "แหล่งที่มา", "agentDetail.logs.table.title": "ชื่อเรื่อง", "agentDetail.logs.table.unread": "ยังไม่ได้อ่าน", "agentDetail.logs.table.updatedTime": "เวลาที่อัปเดต", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "ตัวเลือกการสร้างตัวแทน", "roster.createDialog.description": "สร้างตัวแทนที่นำกลับมาใช้ใหม่ได้ใน Roster ของเวิร์กสเปซนี้", "roster.createDialog.title": "สร้างตัวแทน", - "roster.createFailed": "สร้างตัวแทนไม่สำเร็จ", "roster.createForm.changeIcon": "เปลี่ยนไอคอนตัวแทน", "roster.createForm.descriptionLabel": "คำอธิบาย", "roster.createForm.descriptionOptional": "(ตัวเลือก)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "ลบ {{name}} หรือไม่", "roster.deleteFailed": "ลบตัวแทนไม่สำเร็จ", "roster.deleteSuccess": "ลบตัวแทนแล้ว", + "roster.duplicateFailed": "ทำสำเนาตัวแทนไม่สำเร็จ", + "roster.duplicateSuccess": "ทำสำเนาตัวแทนแล้ว", "roster.editAgent": "แก้ไข {{name}}", "roster.editDialog.description": "อัปเดตชื่อ คำอธิบาย และบทบาทใน Roster สำหรับตัวแทนนี้", "roster.editDialog.title": "แก้ไขตัวแทน", @@ -376,7 +387,6 @@ "roster.tabs.agent": "ตัวแทน", "roster.tabs.human": "บุคคล", "roster.tabsLabel": "ประเภท Roster", - "roster.updateFailed": "อัปเดตตัวแทนไม่สำเร็จ ตรวจสอบฟิลด์และลองอีกครั้ง", "roster.updateSuccess": "อัปเดตตัวแทนแล้ว", "roster.usageStatus.draft": "ฉบับร่าง" } diff --git a/web/i18n/tr-TR/agent-v-2.json b/web/i18n/tr-TR/agent-v-2.json index 78a4620de08..6c307a360ef 100644 --- a/web/i18n/tr-TR/agent-v-2.json +++ b/web/i18n/tr-TR/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Dosyalar", "agentDetail.configure.files.preview.empty": "Önizleme içeriği yok.", "agentDetail.configure.files.preview.failed": "Önizleme yüklenemedi.", + "agentDetail.configure.files.preview.unsupported": "Bu dosya önizlemeyi desteklemiyor.", "agentDetail.configure.files.remove": "{{name}} kaldır", "agentDetail.configure.files.tip": "Bu ajanın görevleri koordine ederken kullanabileceği dosyalar.", "agentDetail.configure.files.toggle": "Dosyaları değiştir", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Son Oluşturma zamanı", "agentDetail.logs.filters.sort.lastUpdatedTime": "Son Güncelleme zamanı", "agentDetail.logs.filters.source.all": "Kaynak", + "agentDetail.logs.filters.source.empty": "Kaynak bulunamadı", "agentDetail.logs.filters.source.label": "Günlük kaynağı", + "agentDetail.logs.filters.source.loadFailed": "Kaynaklar yüklenemedi", + "agentDetail.logs.filters.source.loading": "Kaynaklar yükleniyor…", + "agentDetail.logs.filters.source.searchLabel": "Kaynaklarda ara", + "agentDetail.logs.filters.source.searchPlaceholder": "Ara", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "İş akışı", "agentDetail.logs.learnMore": "Daha fazla bilgi", + "agentDetail.logs.loadFailed": "Günlükler yüklenemedi", + "agentDetail.logs.loading": "Günlükler yükleniyor…", + "agentDetail.logs.notAvailable": "Yok", "agentDetail.logs.table.createdTime": "Oluşturulma Zamanı", "agentDetail.logs.table.endUser": "Son kullanıcı", "agentDetail.logs.table.messageCount": "Mesaj Sayısı", "agentDetail.logs.table.operationRate": "İşlem Oranı", + "agentDetail.logs.table.source": "Kaynak", "agentDetail.logs.table.title": "Başlık", "agentDetail.logs.table.unread": "Okunmamış", "agentDetail.logs.table.updatedTime": "Güncellenme Zamanı", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Ajan oluşturma seçenekleri", "roster.createDialog.description": "Bu çalışma alanı Roster'ında yeniden kullanılabilir bir ajan oluşturun.", "roster.createDialog.title": "Ajan oluştur", - "roster.createFailed": "Ajan oluşturulamadı.", "roster.createForm.changeIcon": "Ajan simgesini değiştir", "roster.createForm.descriptionLabel": "Açıklama", "roster.createForm.descriptionOptional": "(isteğe bağlı)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "{{name}} silinsin mi?", "roster.deleteFailed": "Ajan silinemedi.", "roster.deleteSuccess": "Ajan silindi.", + "roster.duplicateFailed": "Ajan çoğaltılamadı.", + "roster.duplicateSuccess": "Ajan çoğaltıldı.", "roster.editAgent": "{{name}} düzenle", "roster.editDialog.description": "Bu ajan için Roster adı, açıklaması ve rolünü güncelleyin.", "roster.editDialog.title": "Ajanı düzenle", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Ajan", "roster.tabs.human": "İnsan", "roster.tabsLabel": "Roster türü", - "roster.updateFailed": "Ajan güncellenemedi. Alanları kontrol edip tekrar deneyin.", "roster.updateSuccess": "Ajan güncellendi.", "roster.usageStatus.draft": "Taslak" } diff --git a/web/i18n/uk-UA/agent-v-2.json b/web/i18n/uk-UA/agent-v-2.json index d1aa41c1853..09c78652bd0 100644 --- a/web/i18n/uk-UA/agent-v-2.json +++ b/web/i18n/uk-UA/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Файли", "agentDetail.configure.files.preview.empty": "Немає вмісту для перегляду.", "agentDetail.configure.files.preview.failed": "Не вдалося завантажити перегляд.", + "agentDetail.configure.files.preview.unsupported": "Попередній перегляд цього файлу не підтримується.", "agentDetail.configure.files.remove": "Видалити {{name}}", "agentDetail.configure.files.tip": "Файли, які цей агент може використовувати під час оркестрації задач.", "agentDetail.configure.files.toggle": "Перемкнути файли", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Час останнього створення", "agentDetail.logs.filters.sort.lastUpdatedTime": "Час останнього оновлення", "agentDetail.logs.filters.source.all": "Джерело", + "agentDetail.logs.filters.source.empty": "Джерела не знайдено", "agentDetail.logs.filters.source.label": "Джерело журналу", + "agentDetail.logs.filters.source.loadFailed": "Не вдалося завантажити джерела", + "agentDetail.logs.filters.source.loading": "Завантаження джерел…", + "agentDetail.logs.filters.source.searchLabel": "Пошук джерел", + "agentDetail.logs.filters.source.searchPlaceholder": "Пошук", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Робочий процес", "agentDetail.logs.learnMore": "Дізнатися більше", + "agentDetail.logs.loadFailed": "Не вдалося завантажити журнали", + "agentDetail.logs.loading": "Завантаження журналів…", + "agentDetail.logs.notAvailable": "Н/Д", "agentDetail.logs.table.createdTime": "Час створення", "agentDetail.logs.table.endUser": "Кінцевий користувач", "agentDetail.logs.table.messageCount": "Кільк. повідомл.", "agentDetail.logs.table.operationRate": "Рівень операцій", + "agentDetail.logs.table.source": "Джерело", "agentDetail.logs.table.title": "Заголовок", "agentDetail.logs.table.unread": "Непрочитані", "agentDetail.logs.table.updatedTime": "Час оновлення", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Варіанти створення агента", "roster.createDialog.description": "Створіть перевикористовного агента в Roster цього робочого простору.", "roster.createDialog.title": "Створити агента", - "roster.createFailed": "Не вдалося створити агента.", "roster.createForm.changeIcon": "Змінити іконку агента", "roster.createForm.descriptionLabel": "Опис", "roster.createForm.descriptionOptional": "(необов'язково)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Видалити {{name}}?", "roster.deleteFailed": "Не вдалося видалити агента.", "roster.deleteSuccess": "Агента видалено.", + "roster.duplicateFailed": "Не вдалося продублювати агента.", + "roster.duplicateSuccess": "Агента продубльовано.", "roster.editAgent": "Редагувати {{name}}", "roster.editDialog.description": "Оновіть ім'я в Roster, опис і роль для цього агента.", "roster.editDialog.title": "Редагувати агента", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Агент", "roster.tabs.human": "Людина", "roster.tabsLabel": "Тип Roster", - "roster.updateFailed": "Не вдалося оновити агента. Перевірте поля і спробуйте ще раз.", "roster.updateSuccess": "Агента оновлено.", "roster.usageStatus.draft": "Чернетка" } diff --git a/web/i18n/vi-VN/agent-v-2.json b/web/i18n/vi-VN/agent-v-2.json index d521db7ab0d..e0cfb6a2112 100644 --- a/web/i18n/vi-VN/agent-v-2.json +++ b/web/i18n/vi-VN/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "Tệp", "agentDetail.configure.files.preview.empty": "Không có nội dung xem trước.", "agentDetail.configure.files.preview.failed": "Tải xem trước thất bại.", + "agentDetail.configure.files.preview.unsupported": "Tệp này không hỗ trợ xem trước.", "agentDetail.configure.files.remove": "Xóa {{name}}", "agentDetail.configure.files.tip": "Các tệp mà tác nhân này có thể sử dụng khi điều phối tác vụ.", "agentDetail.configure.files.toggle": "Bật/tắt tệp", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "Thời gian tạo gần nhất", "agentDetail.logs.filters.sort.lastUpdatedTime": "Thời gian cập nhật gần nhất", "agentDetail.logs.filters.source.all": "Nguồn", + "agentDetail.logs.filters.source.empty": "Không tìm thấy nguồn", "agentDetail.logs.filters.source.label": "Nguồn nhật ký", + "agentDetail.logs.filters.source.loadFailed": "Không tải được nguồn", + "agentDetail.logs.filters.source.loading": "Đang tải nguồn…", + "agentDetail.logs.filters.source.searchLabel": "Tìm kiếm nguồn", + "agentDetail.logs.filters.source.searchPlaceholder": "Tìm kiếm", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "Quy trình làm việc", "agentDetail.logs.learnMore": "Tìm hiểu thêm", + "agentDetail.logs.loadFailed": "Không tải được nhật ký", + "agentDetail.logs.loading": "Đang tải nhật ký…", + "agentDetail.logs.notAvailable": "Không có", "agentDetail.logs.table.createdTime": "Thời gian tạo", "agentDetail.logs.table.endUser": "Người dùng cuối", "agentDetail.logs.table.messageCount": "Số tin nhắn", "agentDetail.logs.table.operationRate": "Tỷ lệ thao tác", + "agentDetail.logs.table.source": "Nguồn", "agentDetail.logs.table.title": "Tiêu đề", "agentDetail.logs.table.unread": "Chưa đọc", "agentDetail.logs.table.updatedTime": "Thời gian cập nhật", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "Tùy chọn tạo tác nhân", "roster.createDialog.description": "Tạo một tác nhân có thể tái sử dụng trong Roster của không gian làm việc này.", "roster.createDialog.title": "Tạo tác nhân", - "roster.createFailed": "Tạo tác nhân thất bại.", "roster.createForm.changeIcon": "Đổi biểu tượng tác nhân", "roster.createForm.descriptionLabel": "Mô tả", "roster.createForm.descriptionOptional": "(tùy chọn)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "Xóa {{name}}?", "roster.deleteFailed": "Xóa tác nhân thất bại.", "roster.deleteSuccess": "Đã xóa tác nhân.", + "roster.duplicateFailed": "Nhân bản tác nhân thất bại.", + "roster.duplicateSuccess": "Đã nhân bản tác nhân.", "roster.editAgent": "Chỉnh sửa {{name}}", "roster.editDialog.description": "Cập nhật tên, mô tả và vai trò Roster cho tác nhân này.", "roster.editDialog.title": "Chỉnh sửa tác nhân", @@ -376,7 +387,6 @@ "roster.tabs.agent": "Tác nhân", "roster.tabs.human": "Con người", "roster.tabsLabel": "Loại Roster", - "roster.updateFailed": "Cập nhật tác nhân thất bại. Kiểm tra các trường và thử lại.", "roster.updateSuccess": "Đã cập nhật tác nhân.", "roster.usageStatus.draft": "Bản nháp" } diff --git a/web/i18n/zh-Hans/agent-v-2.json b/web/i18n/zh-Hans/agent-v-2.json index 27f30d27199..32a51de221d 100644 --- a/web/i18n/zh-Hans/agent-v-2.json +++ b/web/i18n/zh-Hans/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "文件", "agentDetail.configure.files.preview.empty": "暂无预览内容。", "agentDetail.configure.files.preview.failed": "预览加载失败。", + "agentDetail.configure.files.preview.unsupported": "该文件不支持预览。", "agentDetail.configure.files.remove": "移除 {{name}}", "agentDetail.configure.files.tip": "此智能体在编排任务时可使用的文件。", "agentDetail.configure.files.toggle": "展开或收起文件", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "最近创建时间", "agentDetail.logs.filters.sort.lastUpdatedTime": "最近更新时间", "agentDetail.logs.filters.source.all": "来源", + "agentDetail.logs.filters.source.empty": "暂无来源", "agentDetail.logs.filters.source.label": "日志来源", + "agentDetail.logs.filters.source.loadFailed": "来源加载失败", + "agentDetail.logs.filters.source.loading": "来源加载中…", + "agentDetail.logs.filters.source.searchLabel": "搜索来源", + "agentDetail.logs.filters.source.searchPlaceholder": "搜索", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "工作流", "agentDetail.logs.learnMore": "了解更多", + "agentDetail.logs.loadFailed": "日志加载失败", + "agentDetail.logs.loading": "日志加载中…", + "agentDetail.logs.notAvailable": "暂无", "agentDetail.logs.table.createdTime": "创建时间", "agentDetail.logs.table.endUser": "终端用户", "agentDetail.logs.table.messageCount": "消息数", "agentDetail.logs.table.operationRate": "操作率", + "agentDetail.logs.table.source": "来源", "agentDetail.logs.table.title": "标题", "agentDetail.logs.table.unread": "未读", "agentDetail.logs.table.updatedTime": "更新时间", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "创建智能体选项", "roster.createDialog.description": "在当前工作区 Roster 中创建一个可复用智能体。", "roster.createDialog.title": "创建智能体", - "roster.createFailed": "智能体创建失败。", "roster.createForm.changeIcon": "更换智能体图标", "roster.createForm.descriptionLabel": "描述", "roster.createForm.descriptionOptional": "(可选)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "删除 {{name}}?", "roster.deleteFailed": "智能体删除失败。", "roster.deleteSuccess": "智能体已删除。", + "roster.duplicateFailed": "智能体复制失败。", + "roster.duplicateSuccess": "智能体已复制。", "roster.editAgent": "编辑 {{name}}", "roster.editDialog.description": "更新此智能体在 Roster 中的名称、描述和角色。", "roster.editDialog.title": "编辑智能体", @@ -376,7 +387,6 @@ "roster.tabs.agent": "智能体", "roster.tabs.human": "人工成员", "roster.tabsLabel": "Roster 类型", - "roster.updateFailed": "智能体更新失败。请检查字段后重试。", "roster.updateSuccess": "智能体已更新。", "roster.usageStatus.draft": "草稿" } diff --git a/web/i18n/zh-Hant/agent-v-2.json b/web/i18n/zh-Hant/agent-v-2.json index efb8491540f..4144843418c 100644 --- a/web/i18n/zh-Hant/agent-v-2.json +++ b/web/i18n/zh-Hant/agent-v-2.json @@ -82,6 +82,7 @@ "agentDetail.configure.files.label": "檔案", "agentDetail.configure.files.preview.empty": "暫無預覽內容。", "agentDetail.configure.files.preview.failed": "預覽載入失敗。", + "agentDetail.configure.files.preview.unsupported": "此檔案不支援預覽。", "agentDetail.configure.files.remove": "移除 {{name}}", "agentDetail.configure.files.tip": "此智能體在編排任務時可使用的檔案。", "agentDetail.configure.files.toggle": "展開或收合檔案", @@ -241,14 +242,23 @@ "agentDetail.logs.filters.sort.lastCreatedTime": "最近建立時間", "agentDetail.logs.filters.sort.lastUpdatedTime": "最近更新時間", "agentDetail.logs.filters.source.all": "來源", + "agentDetail.logs.filters.source.empty": "沒有找到來源", "agentDetail.logs.filters.source.label": "日誌來源", + "agentDetail.logs.filters.source.loadFailed": "來源載入失敗", + "agentDetail.logs.filters.source.loading": "來源載入中…", + "agentDetail.logs.filters.source.searchLabel": "搜尋來源", + "agentDetail.logs.filters.source.searchPlaceholder": "搜尋", "agentDetail.logs.filters.source.webapp": "Webapp", "agentDetail.logs.filters.source.workflow": "工作流程", "agentDetail.logs.learnMore": "了解更多", + "agentDetail.logs.loadFailed": "日誌載入失敗", + "agentDetail.logs.loading": "日誌載入中…", + "agentDetail.logs.notAvailable": "無", "agentDetail.logs.table.createdTime": "建立時間", "agentDetail.logs.table.endUser": "終端使用者", "agentDetail.logs.table.messageCount": "訊息數", "agentDetail.logs.table.operationRate": "操作率", + "agentDetail.logs.table.source": "來源", "agentDetail.logs.table.title": "標題", "agentDetail.logs.table.unread": "未讀", "agentDetail.logs.table.updatedTime": "更新時間", @@ -324,7 +334,6 @@ "roster.createAgentOptions": "建立智能體選項", "roster.createDialog.description": "在目前工作區 Roster 中建立一個可複用智能體。", "roster.createDialog.title": "建立智能體", - "roster.createFailed": "智能體建立失敗。", "roster.createForm.changeIcon": "更換智能體圖示", "roster.createForm.descriptionLabel": "描述", "roster.createForm.descriptionOptional": "(選用)", @@ -341,6 +350,8 @@ "roster.deleteDialog.title": "刪除 {{name}}?", "roster.deleteFailed": "智能體刪除失敗。", "roster.deleteSuccess": "智能體已刪除。", + "roster.duplicateFailed": "智能體複製失敗。", + "roster.duplicateSuccess": "智能體已複製。", "roster.editAgent": "編輯 {{name}}", "roster.editDialog.description": "更新此智能體在 Roster 中的名稱、描述和角色。", "roster.editDialog.title": "編輯智能體", @@ -376,7 +387,6 @@ "roster.tabs.agent": "智能體", "roster.tabs.human": "人工成員", "roster.tabsLabel": "Roster 類型", - "roster.updateFailed": "智能體更新失敗。請檢查欄位後再試。", "roster.updateSuccess": "智能體已更新。", "roster.usageStatus.draft": "草稿" } diff --git a/web/service/client.spec.ts b/web/service/client.spec.ts index f38769992e1..414cf37d325 100644 --- a/web/service/client.spec.ts +++ b/web/service/client.spec.ts @@ -188,6 +188,40 @@ describe('consoleQuery agent mutation defaults', () => { }) }) + it('should cache copied agent detail and invalidate roster lists after copying an agent', async () => { + const consoleQuery = await loadConsoleQuery() + const queryClient = new QueryClient() + const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries') + const copiedAgent = createAgent({ id: 'copied-agent', name: 'Agent copy' }) + + const mutationOptions = consoleQuery.agent.byAgentId.copy.post.mutationOptions() + await mutationOptions.onSuccess?.( + copiedAgent, + { + params: { + agent_id: 'source-agent', + }, + body: {}, + }, + undefined, + createMutationContext(queryClient), + ) + + expect(queryClient.getQueryData(consoleQuery.agent.byAgentId.get.queryKey({ + input: { + params: { + agent_id: copiedAgent.id, + }, + }, + }))).toEqual(copiedAgent) + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.agent.get.key(), + }) + expect(invalidateQueries).toHaveBeenCalledWith({ + queryKey: consoleQuery.agent.inviteOptions.get.key(), + }) + }) + it('should invalidate invite option lists after updating an agent', async () => { const consoleQuery = await loadConsoleQuery() const queryClient = new QueryClient() diff --git a/web/service/client.ts b/web/service/client.ts index 0e0ecaf6ed3..73c23f60da8 100644 --- a/web/service/client.ts +++ b/web/service/client.ts @@ -361,6 +361,30 @@ export const consoleQuery: RouterUtils = createTanstackQue }, }, byAgentId: { + copy: { + post: { + mutationOptions: { + onSuccess: (copiedAgent, _variables, _onMutateResult, context) => { + context.client.setQueryData( + consoleQuery.agent.byAgentId.get.queryKey({ + input: { + params: { + agent_id: copiedAgent.id, + }, + }, + }), + copiedAgent, + ) + context.client.invalidateQueries({ + queryKey: consoleQuery.agent.get.key(), + }) + context.client.invalidateQueries({ + queryKey: consoleQuery.agent.inviteOptions.get.key(), + }) + }, + }, + }, + }, put: { mutationOptions: { onSuccess: (updatedAgent, variables, _onMutateResult, context) => {