mirror of
https://github.com/langgenius/dify.git
synced 2026-06-22 19:21:13 +08:00
chore(agent-v2): sync nightly updates to main (2026-06-18) (#37610)
Co-authored-by: Joel <iamjoel007@gmail.com> Co-authored-by: yyh <yuanyouhuilyz@gmail.com>
This commit is contained in:
parent
0df30dd269
commit
8732d1463a
@ -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', () => {
|
||||
|
||||
@ -35,7 +35,6 @@ const MenuDialog = ({
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="absolute top-0 right-0 h-full w-1/2 bg-components-panel-bg" />
|
||||
{children}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@ -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}`)
|
||||
|
||||
@ -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])
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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<OnSelectBlock>((type, pluginDefaultValue) => {
|
||||
handleNodeAdd(
|
||||
|
||||
@ -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<OnSelectBlock>((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)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<BasePanelProps> = ({
|
||||
|
||||
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<BasePanelProps> = ({
|
||||
}, [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)
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 (
|
||||
<AgentOrchestratePanel
|
||||
agentId={agentId}
|
||||
appId={isInline ? appId : undefined}
|
||||
activeConfigSnapshot={activeConfigSnapshot}
|
||||
agentSoulConfig={agentSoulConfig as AgentSoulConfig}
|
||||
agentName={composerState?.agent?.name}
|
||||
|
||||
@ -87,6 +87,7 @@ function AgentRosterDrawer({
|
||||
mode = 'detail',
|
||||
open,
|
||||
portalContainerRef,
|
||||
showAccessIcon = true,
|
||||
showConsoleLink = true,
|
||||
showDetailActions = true,
|
||||
onClose,
|
||||
@ -96,6 +97,7 @@ function AgentRosterDrawer({
|
||||
mode?: AgentRosterDrawerMode
|
||||
open: boolean
|
||||
portalContainerRef: RefObject<HTMLDivElement | null>
|
||||
showAccessIcon?: boolean
|
||||
showConsoleLink?: boolean
|
||||
showDetailActions?: boolean
|
||||
onClose: () => void
|
||||
@ -144,7 +146,7 @@ function AgentRosterDrawer({
|
||||
<DrawerTitle className={cn('truncate', isSetup ? 'system-xl-semibold text-text-primary' : 'system-sm-medium text-text-secondary')}>
|
||||
{title}
|
||||
</DrawerTitle>
|
||||
{!isSetup && <span aria-hidden className="i-ri-lock-line size-3 shrink-0 text-text-tertiary" />}
|
||||
{!isSetup && showAccessIcon && <span aria-hidden className="i-ri-lock-line size-3 shrink-0 text-text-tertiary" />}
|
||||
</div>
|
||||
{description && (
|
||||
<p className={cn(isSetup ? 'min-w-full' : 'truncate', 'system-xs-regular text-text-tertiary')}>
|
||||
@ -341,6 +343,7 @@ export function AgentRosterField({
|
||||
mode={panelMode}
|
||||
open={panelOpen}
|
||||
portalContainerRef={portalContainerRef}
|
||||
showAccessIcon={!isInlineSetup}
|
||||
showDetailActions={showPanelDetailActions}
|
||||
onClose={() => setPanelOpen(false)}
|
||||
>
|
||||
|
||||
@ -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<HTMLDivElement>(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({
|
||||
? (
|
||||
<AgentOrchestrateDrawerPanel
|
||||
agentId={displayedAgent.id}
|
||||
appId={appId}
|
||||
inlineComposerState={inlineAgentQuery.data}
|
||||
isInline={isInlineAgentReady}
|
||||
nodeId={id}
|
||||
|
||||
@ -12,11 +12,38 @@ import {
|
||||
getIterationStartNode,
|
||||
getLoopStartNode,
|
||||
getNestedNodePosition,
|
||||
getNodeCatalogType,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
getTopLeftNodePosition,
|
||||
hasRetryNode,
|
||||
} from '../node'
|
||||
|
||||
describe('getNodeCatalogType', () => {
|
||||
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({
|
||||
|
||||
@ -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<Node, 'id'> & { id?: string }): {
|
||||
newNode: Node
|
||||
newIterationStartNode?: Node
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
@ -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(),
|
||||
})),
|
||||
})
|
||||
|
||||
@ -39,6 +39,7 @@ export type AgentFileNode = {
|
||||
id: string
|
||||
name: string
|
||||
icon: FileTreeIconType
|
||||
driveKey?: string
|
||||
children?: AgentFileNode[]
|
||||
}
|
||||
|
||||
|
||||
@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -23,7 +23,7 @@ function renderEmptySections() {
|
||||
}}
|
||||
>
|
||||
<AgentSkills agentId="agent-1" />
|
||||
<AgentFiles />
|
||||
<AgentFiles agentId="agent-1" />
|
||||
<AgentTools />
|
||||
<AgentKnowledgeRetrieval />
|
||||
</AgentComposerProvider>
|
||||
|
||||
@ -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<string, {
|
||||
|
||||
const mockFormatForDisplay = vi.hoisted(() => 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<typeof import('@tanstack/react-hotkeys')>()
|
||||
@ -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 <div data-testid="publish-impact-popover">{children}</div>
|
||||
return ReactDOM.createPortal(<div data-testid="publish-impact-popover">{children}</div>, 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<PublishHandler>(),
|
||||
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<typeof createStore>) => 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(
|
||||
<JotaiProvider store={store}>
|
||||
<AgentConfigurePublishBar
|
||||
agentId="agent-1"
|
||||
activeConfigSnapshot={activeConfigSnapshot}
|
||||
draftSavedAt={draftSavedAt}
|
||||
agentName="Iris"
|
||||
isPublishing={isPublishing}
|
||||
publishedReferenceCount={publishedReferenceCount}
|
||||
publishedReferences={publishedReferences}
|
||||
onPublish={onPublish}
|
||||
onOpenVersions={vi.fn()}
|
||||
/>
|
||||
</JotaiProvider>,
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<JotaiProvider store={store}>
|
||||
<AgentConfigurePublishBar
|
||||
agentId="agent-1"
|
||||
activeConfigIsPublished={activeConfigIsPublished}
|
||||
activeConfigSnapshot={activeConfigSnapshot}
|
||||
draftSavedAt={draftSavedAt}
|
||||
agentName="Iris"
|
||||
isPublishing={isPublishing}
|
||||
onPublish={onPublish}
|
||||
onOpenVersions={vi.fn()}
|
||||
/>
|
||||
</JotaiProvider>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
|
||||
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',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
@ -32,12 +32,12 @@ export function ConfigureSectionConfigurableItem({
|
||||
</span>
|
||||
</div>
|
||||
{!readOnly && (
|
||||
<div className="hidden shrink-0 items-center gap-1 group-focus-within:flex group-hover:flex">
|
||||
<div className="pointer-events-none flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={editAriaLabel}
|
||||
onClick={onEdit}
|
||||
className="flex size-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary 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-base-hover hover:text-text-secondary focus-visible:bg-state-base-hover focus-visible:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
>
|
||||
<span aria-hidden className="i-ri-equalizer-2-line size-4" />
|
||||
</button>
|
||||
@ -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"
|
||||
>
|
||||
<span aria-hidden className="i-ri-delete-bin-line size-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{hasBadge && (
|
||||
<span className="shrink-0 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary group-focus-within:hidden group-hover:hidden">
|
||||
<span className="shrink-0 rounded-[5px] border border-divider-deep bg-components-badge-bg-dimm px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary transition-opacity group-focus-within:opacity-0 group-hover:opacity-0">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AgentComposerProvider initialDraft={agentFilesDraft}>
|
||||
<AgentFiles />
|
||||
<AgentComposerProvider initialDraft={initialDraft}>
|
||||
<AgentFiles agentId="agent-1" />
|
||||
</AgentComposerProvider>
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
@ -77,7 +165,7 @@ function renderReadonlyAgentFiles() {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AgentComposerProvider initialDraft={agentFilesDraft}>
|
||||
<AgentOrchestrateReadOnlyContext value>
|
||||
<AgentFiles />
|
||||
<AgentFiles agentId="agent-1" />
|
||||
</AgentOrchestrateReadOnlyContext>
|
||||
</AgentComposerProvider>
|
||||
</QueryClientProvider>,
|
||||
@ -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.
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
export type AgentFileApiContext = {
|
||||
agentId: string
|
||||
workflow?: {
|
||||
appId: string
|
||||
nodeId: string
|
||||
}
|
||||
}
|
||||
@ -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<FileTreeIconType>([
|
||||
'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)
|
||||
}
|
||||
@ -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<string>()
|
||||
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 (
|
||||
<li className="group/file-row relative min-w-0">
|
||||
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
|
||||
<button
|
||||
type="button"
|
||||
data-selected={selected || undefined}
|
||||
aria-current={selected ? 'true' : undefined}
|
||||
className="group/file-tree-row relative flex h-6 w-full min-w-0 cursor-pointer items-center rounded-md pr-7 pl-2 text-left outline-hidden select-none hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid data-[selected]:bg-state-base-active"
|
||||
onClick={() => setIsPreviewOpen(true)}
|
||||
<Dialog open={isPreviewOpen} onOpenChange={handlePreviewOpenChange}>
|
||||
<DialogTrigger
|
||||
render={(
|
||||
<button
|
||||
type="button"
|
||||
data-selected={selected || undefined}
|
||||
aria-current={selected ? 'true' : undefined}
|
||||
className="group/file-tree-row relative flex h-6 w-full min-w-0 cursor-pointer items-center rounded-md pr-7 pl-2 text-left outline-hidden select-none hover:bg-state-base-hover focus-visible:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:ring-inset data-[selected]:bg-state-base-active"
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{Array.from({ length: Math.max(depth - 1, 0) }, (_, index) => (
|
||||
<FileTreeGuide key={index} />
|
||||
@ -79,23 +179,28 @@ function AgentFileItem({
|
||||
<div className="flex min-w-0 flex-[1_0_0] items-center py-0.5">
|
||||
{children}
|
||||
</div>
|
||||
</button>
|
||||
{isPreviewOpen && (
|
||||
<AgentSkillDetailDialog
|
||||
skillName={file.name}
|
||||
detail={{
|
||||
description: t('agentDetail.configure.files.tip'),
|
||||
files,
|
||||
filePreview: {
|
||||
content: previewQuery.data?.content,
|
||||
isError: previewQuery.isError,
|
||||
isLoading: previewQuery.isPending,
|
||||
},
|
||||
selectedFileId: file.id,
|
||||
sections: [],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<AgentSkillDetailDialog
|
||||
skillName={file.name}
|
||||
detail={{
|
||||
description: t('agentDetail.configure.files.tip'),
|
||||
files,
|
||||
filePreview: {
|
||||
binary: previewQuery.data?.binary,
|
||||
content: previewQuery.data?.text ?? undefined,
|
||||
downloadUrl: downloadQuery.data?.url,
|
||||
fileName: selectedPreviewFile.name,
|
||||
isDownloadError: downloadQuery.isError,
|
||||
isDownloadLoading: shouldDownloadPreviewFile && downloadQuery.isPending,
|
||||
isError: previewQuery.isError,
|
||||
isImage: isImagePreviewFile,
|
||||
isLoading: previewQuery.isPending,
|
||||
},
|
||||
onSelectFile: selectedFile => setSelectedFileId(selectedFile.id),
|
||||
selectedFileId: selectedFileId ?? file.id,
|
||||
sections: [],
|
||||
}}
|
||||
/>
|
||||
</Dialog>
|
||||
{!readOnly && (
|
||||
<button
|
||||
@ -112,26 +217,107 @@ function AgentFileItem({
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentFiles() {
|
||||
export function AgentFiles({
|
||||
agentId,
|
||||
appId,
|
||||
nodeId,
|
||||
}: {
|
||||
agentId: string
|
||||
appId?: string
|
||||
nodeId?: string
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const [files, setFiles] = useAtom(agentComposerFilesAtom)
|
||||
const [draftFiles, setDraftFiles] = useAtom(agentComposerFilesAtom)
|
||||
const filesTip = t('agentDetail.configure.files.tip')
|
||||
const filesTreeId = 'agent-configure-files-tree'
|
||||
const [isUploadOpen, setIsUploadOpen] = useState(false)
|
||||
const promptAddCallbackRef = useRef<AgentOrchestrateAddActionOptions['onAdded']>(undefined)
|
||||
const apiContext: AgentFileApiContext = useMemo(() => appId && nodeId
|
||||
? {
|
||||
agentId,
|
||||
workflow: {
|
||||
appId,
|
||||
nodeId,
|
||||
},
|
||||
}
|
||||
: { agentId }, [agentId, appId, nodeId])
|
||||
const agentDriveFilesQuery = useQuery({
|
||||
...consoleQuery.agent.byAgentId.drive.files.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
query: {
|
||||
prefix: FILES_DRIVE_PREFIX,
|
||||
},
|
||||
},
|
||||
}),
|
||||
enabled: !apiContext.workflow,
|
||||
})
|
||||
const workflowDriveFilesQuery = useQuery({
|
||||
...consoleQuery.apps.byAppId.agent.drive.files.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
app_id: appId ?? '',
|
||||
},
|
||||
query: {
|
||||
node_id: nodeId,
|
||||
prefix: FILES_DRIVE_PREFIX,
|
||||
},
|
||||
},
|
||||
}),
|
||||
enabled: !!apiContext.workflow,
|
||||
})
|
||||
const driveFilesQuery = apiContext.workflow ? workflowDriveFilesQuery : agentDriveFilesQuery
|
||||
const { mutate: deleteAgentFile } = useMutation(consoleQuery.agent.byAgentId.files.delete.mutationOptions())
|
||||
const { mutate: deleteWorkflowAgentFile } = useMutation(consoleQuery.apps.byAppId.agent.files.delete.mutationOptions())
|
||||
const files = driveFilesQuery.isSuccess
|
||||
? (driveFilesQuery.data.items ?? []).map(toAgentFileNodeFromDriveItem)
|
||||
: draftFiles
|
||||
const removeFile = useCallback((fileId: string) => {
|
||||
setFiles(removeFileNode(files, fileId))
|
||||
}, [files, setFiles])
|
||||
const file = findAgentFileNode(files, fileId)
|
||||
const driveKey = file?.driveKey
|
||||
|
||||
setDraftFiles(removeFileNode(draftFiles, fileId))
|
||||
if (!driveKey)
|
||||
return
|
||||
|
||||
const onSuccess = () => {
|
||||
void driveFilesQuery.refetch()
|
||||
}
|
||||
if (apiContext.workflow) {
|
||||
deleteWorkflowAgentFile({
|
||||
params: {
|
||||
app_id: apiContext.workflow.appId,
|
||||
},
|
||||
query: {
|
||||
key: driveKey,
|
||||
node_id: apiContext.workflow.nodeId,
|
||||
},
|
||||
}, { onSuccess })
|
||||
return
|
||||
}
|
||||
|
||||
deleteAgentFile({
|
||||
params: {
|
||||
agent_id: apiContext.agentId,
|
||||
},
|
||||
query: {
|
||||
key: driveKey,
|
||||
},
|
||||
}, { onSuccess })
|
||||
}, [apiContext, deleteAgentFile, deleteWorkflowAgentFile, draftFiles, driveFilesQuery, files, setDraftFiles])
|
||||
const handleOpenUpload = useCallback((options?: AgentOrchestrateAddActionOptions) => {
|
||||
promptAddCallbackRef.current = options?.onAdded
|
||||
setIsUploadOpen(true)
|
||||
}, [])
|
||||
useRegisterAgentOrchestrateAddAction('files', handleOpenUpload)
|
||||
const handleUploaded = useCallback((file: AgentFileNode) => {
|
||||
setFiles([...files, file])
|
||||
setDraftFiles([...draftFiles, file])
|
||||
void driveFilesQuery.refetch()
|
||||
promptAddCallbackRef.current?.(file)
|
||||
promptAddCallbackRef.current = undefined
|
||||
}, [files, setFiles])
|
||||
}, [draftFiles, driveFilesQuery, setDraftFiles])
|
||||
const handleUploadOpenChange = useCallback((open: boolean) => {
|
||||
if (!open)
|
||||
promptAddCallbackRef.current = undefined
|
||||
@ -173,6 +359,7 @@ export function AgentFiles() {
|
||||
depth={depth}
|
||||
file={file}
|
||||
files={files}
|
||||
apiContext={apiContext}
|
||||
selected={selected}
|
||||
onRemove={removeFile}
|
||||
>
|
||||
@ -183,6 +370,7 @@ export function AgentFiles() {
|
||||
)}
|
||||
</ConfigureSection>
|
||||
<AgentFileUploadDialog
|
||||
apiContext={apiContext}
|
||||
open={isUploadOpen}
|
||||
onOpenChange={handleUploadOpenChange}
|
||||
onUploaded={handleUploaded}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import type { FileResponse } from '@dify/contracts/api/console/files/types.gen'
|
||||
import type { FileTreeIconType } from '@langgenius/dify-ui/file-tree'
|
||||
import type { AgentFileApiContext } from './api-context'
|
||||
import type { AgentFileNode } from '@/features/agent-v2/agent-composer/form-state'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
@ -14,59 +14,23 @@ import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import { getFileIconType } from './file-icon'
|
||||
|
||||
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'])
|
||||
|
||||
function getFileExtension(fileName: string) {
|
||||
return fileName.split('.').pop()?.toLowerCase() ?? ''
|
||||
type AgentDriveFileCommit = {
|
||||
file: {
|
||||
drive_key: string
|
||||
file_id: string
|
||||
mime_type?: string | null
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
function toAgentFileNode(uploadedFile: FileResponse): AgentFileNode {
|
||||
function toAgentFileNode(committedFile: AgentDriveFileCommit['file']): AgentFileNode {
|
||||
return {
|
||||
id: uploadedFile.id,
|
||||
name: uploadedFile.name,
|
||||
icon: getFileIconType(uploadedFile.name, uploadedFile.mime_type),
|
||||
id: committedFile.file_id,
|
||||
name: committedFile.name,
|
||||
icon: getFileIconType(committedFile.name, committedFile.mime_type),
|
||||
driveKey: committedFile.drive_key,
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,10 +132,12 @@ function AgentFileUploader({
|
||||
}
|
||||
|
||||
export function AgentFileUploadDialog({
|
||||
apiContext,
|
||||
open,
|
||||
onOpenChange,
|
||||
onUploaded,
|
||||
}: {
|
||||
apiContext: AgentFileApiContext
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onUploaded: (file: AgentFileNode) => void
|
||||
@ -180,9 +146,43 @@ export function AgentFileUploadDialog({
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const [file, setFile] = useState<File>()
|
||||
const uploadFileMutation = useMutation(consoleQuery.files.upload.post.mutationOptions())
|
||||
const commitAgentFileMutation = useMutation(consoleQuery.agent.byAgentId.files.post.mutationOptions())
|
||||
const commitWorkflowAgentFileMutation = useMutation(consoleQuery.apps.byAppId.agent.files.post.mutationOptions())
|
||||
const isUploading = uploadFileMutation.isPending
|
||||
|| commitAgentFileMutation.isPending
|
||||
|| commitWorkflowAgentFileMutation.isPending
|
||||
|
||||
const commitUploadedFile = (uploadedFile: FileResponse, options: {
|
||||
onSuccess: (committedFile: AgentDriveFileCommit) => void
|
||||
onError: () => void
|
||||
}) => {
|
||||
const body = {
|
||||
upload_file_id: uploadedFile.id,
|
||||
}
|
||||
|
||||
if (apiContext.workflow) {
|
||||
commitWorkflowAgentFileMutation.mutate({
|
||||
params: {
|
||||
app_id: apiContext.workflow.appId,
|
||||
},
|
||||
query: {
|
||||
node_id: apiContext.workflow.nodeId,
|
||||
},
|
||||
body,
|
||||
}, options)
|
||||
return
|
||||
}
|
||||
|
||||
commitAgentFileMutation.mutate({
|
||||
params: {
|
||||
agent_id: apiContext.agentId,
|
||||
},
|
||||
body,
|
||||
}, options)
|
||||
}
|
||||
|
||||
const handleUpload = () => {
|
||||
if (!file || uploadFileMutation.isPending)
|
||||
if (!file || isUploading)
|
||||
return
|
||||
|
||||
uploadFileMutation.mutate({
|
||||
@ -191,10 +191,17 @@ export function AgentFileUploadDialog({
|
||||
},
|
||||
}, {
|
||||
onSuccess: (uploadedFile) => {
|
||||
toast.success(t('agentDetail.configure.files.upload.success'))
|
||||
onUploaded(toAgentFileNode(uploadedFile))
|
||||
setFile(undefined)
|
||||
onOpenChange(false)
|
||||
commitUploadedFile(uploadedFile, {
|
||||
onSuccess: (committedFile) => {
|
||||
toast.success(t('agentDetail.configure.files.upload.success'))
|
||||
onUploaded(toAgentFileNode(committedFile.file))
|
||||
setFile(undefined)
|
||||
onOpenChange(false)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('agentDetail.configure.files.upload.failed'))
|
||||
},
|
||||
})
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('agentDetail.configure.files.upload.failed'))
|
||||
@ -205,6 +212,8 @@ export function AgentFileUploadDialog({
|
||||
const handleOpenChange = (nextOpen: boolean) => {
|
||||
if (!nextOpen) {
|
||||
uploadFileMutation.reset()
|
||||
commitAgentFileMutation.reset()
|
||||
commitWorkflowAgentFileMutation.reset()
|
||||
setFile(undefined)
|
||||
}
|
||||
onOpenChange(nextOpen)
|
||||
@ -225,14 +234,14 @@ export function AgentFileUploadDialog({
|
||||
onChange={setFile}
|
||||
/>
|
||||
<div className="flex justify-end gap-2 pt-6">
|
||||
<Button type="button" onClick={() => handleOpenChange(false)} disabled={uploadFileMutation.isPending}>
|
||||
<Button type="button" onClick={() => handleOpenChange(false)} disabled={isUploading}>
|
||||
{tCommon('operation.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
disabled={!file}
|
||||
loading={uploadFileMutation.isPending}
|
||||
loading={isUploading}
|
||||
onClick={handleUpload}
|
||||
>
|
||||
{t('agentDetail.configure.files.upload.action')}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentConfigSnapshotDetailResponse, AgentConfigSnapshotSummaryResponse, AgentPublishedReferenceResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { AgentConfigSnapshotDetailResponse, AgentConfigSnapshotSummaryResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { AgentConfigurePublishPayload } from './publish-bar'
|
||||
import type { DefaultModel, Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
@ -20,6 +20,8 @@ import { AgentTools } from './tools'
|
||||
|
||||
type AgentOrchestratePanelProps = {
|
||||
agentId: string
|
||||
appId?: string
|
||||
activeConfigIsPublished?: boolean
|
||||
activeConfigSnapshot?: AgentConfigSnapshotSummaryResponse | null
|
||||
agentSoulConfig?: AgentConfigSnapshotDetailResponse['config_snapshot']
|
||||
agentName?: string | null
|
||||
@ -27,8 +29,6 @@ type AgentOrchestratePanelProps = {
|
||||
textGenerationModelList: Model[]
|
||||
draftSavedAt?: number
|
||||
isPublishing?: boolean
|
||||
publishedReferenceCount?: number
|
||||
publishedReferences?: AgentPublishedReferenceResponse[]
|
||||
className?: string
|
||||
readOnly?: boolean
|
||||
showHeader?: boolean
|
||||
@ -40,6 +40,8 @@ type AgentOrchestratePanelProps = {
|
||||
|
||||
export function AgentOrchestratePanel({
|
||||
agentId,
|
||||
appId,
|
||||
activeConfigIsPublished,
|
||||
activeConfigSnapshot,
|
||||
agentSoulConfig,
|
||||
agentName,
|
||||
@ -47,8 +49,6 @@ export function AgentOrchestratePanel({
|
||||
textGenerationModelList,
|
||||
draftSavedAt,
|
||||
isPublishing,
|
||||
publishedReferenceCount,
|
||||
publishedReferences,
|
||||
className,
|
||||
readOnly = false,
|
||||
showHeader = true,
|
||||
@ -87,7 +87,10 @@ export function AgentOrchestratePanel({
|
||||
/>
|
||||
<AgentPromptEditor />
|
||||
<AgentSkills agentId={agentId} />
|
||||
<AgentFiles />
|
||||
<AgentFiles
|
||||
agentId={agentId}
|
||||
appId={appId}
|
||||
/>
|
||||
<AgentTools />
|
||||
<AgentKnowledgeRetrieval />
|
||||
<AgentAdvancedSettings />
|
||||
@ -99,14 +102,13 @@ export function AgentOrchestratePanel({
|
||||
{showPublishBar && (
|
||||
<AgentConfigurePublishBar
|
||||
agentId={agentId}
|
||||
activeConfigIsPublished={activeConfigIsPublished}
|
||||
activeConfigSnapshot={activeConfigSnapshot}
|
||||
agentSoulConfig={agentSoulConfig}
|
||||
agentName={agentName}
|
||||
currentModel={currentModel}
|
||||
draftSavedAt={draftSavedAt}
|
||||
isPublishing={isPublishing}
|
||||
publishedReferenceCount={publishedReferenceCount}
|
||||
publishedReferences={publishedReferences}
|
||||
onPublish={onPublish}
|
||||
onOpenVersions={onOpenVersions}
|
||||
/>
|
||||
|
||||
@ -77,6 +77,32 @@ describe('AgentKnowledgeRetrieval', () => {
|
||||
name: 'agentV2.agentDetail.configure.knowledgeRetrieval.remove:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}',
|
||||
})).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep row actions focusable while visually hidden until hover or focus', () => {
|
||||
renderKnowledgeRetrieval()
|
||||
|
||||
const editButton = screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.knowledgeRetrieval.edit:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}',
|
||||
})
|
||||
const removeButton = screen.getByRole('button', {
|
||||
name: 'agentV2.agentDetail.configure.knowledgeRetrieval.remove:{"name":"agentV2.agentDetail.configure.knowledgeRetrieval.retrievalOne"}',
|
||||
})
|
||||
const actionGroup = editButton.parentElement
|
||||
|
||||
expect(actionGroup).toHaveClass('flex')
|
||||
expect(actionGroup).not.toHaveClass('hidden')
|
||||
expect(actionGroup).toHaveClass(
|
||||
'opacity-0',
|
||||
'group-focus-within:opacity-100',
|
||||
'group-hover:opacity-100',
|
||||
)
|
||||
expect(removeButton).toHaveClass(
|
||||
'hover:bg-state-destructive-hover',
|
||||
'hover:text-text-destructive',
|
||||
'focus-visible:bg-state-destructive-hover',
|
||||
'focus-visible:text-text-destructive',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('User Interactions', () => {
|
||||
|
||||
@ -43,7 +43,7 @@ const queryModeOptions: KnowledgeRetrievalQueryMode[] = ['agent', 'custom']
|
||||
const optionCardClassName = cn(
|
||||
'flex h-8 flex-1 items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg px-3 py-2 text-center system-sm-regular text-text-secondary transition-colors',
|
||||
'hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover',
|
||||
'focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
'focus-visible:ring-1 focus-visible:ring-state-accent-solid focus-visible:outline-hidden',
|
||||
'data-checked:border-[1.5px] data-checked:border-components-option-card-option-selected-border data-checked:bg-components-option-card-option-selected-bg data-checked:font-medium data-checked:text-text-primary data-checked:shadow-xs data-checked:shadow-shadow-shadow-3',
|
||||
)
|
||||
|
||||
|
||||
@ -355,12 +355,12 @@ export function AgentPromptEditor() {
|
||||
onKeyUpCapture={handleEditorKeyUp}
|
||||
onPointerUpCapture={handleEditorPointerUp}
|
||||
>
|
||||
<div ref={editorRef} className="min-h-[72px] overflow-y-auto px-3 pt-0.5">
|
||||
<div ref={editorRef} className="min-h-[104px] overflow-y-auto px-3 pt-0.5">
|
||||
<PromptEditor
|
||||
instanceId="agent-configure-prompt-editor"
|
||||
compact
|
||||
wrapperClassName="min-h-[72px]"
|
||||
className="min-h-[72px] text-text-primary"
|
||||
wrapperClassName="min-h-[104px]"
|
||||
className="min-h-[104px] text-text-primary"
|
||||
placeholder={promptPlaceholder}
|
||||
placeholderClassName="top-0!"
|
||||
editable={!readOnly}
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentConfigSnapshotDetailResponse, AgentConfigSnapshotSummaryResponse, AgentPublishedReferenceResponse, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { AgentConfigSnapshotDetailResponse, AgentConfigSnapshotSummaryResponse, AgentSoulConfig } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { RegisterableHotkey } from '@tanstack/react-hotkeys'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { formatForDisplay, useHotkey } from '@tanstack/react-hotkeys'
|
||||
import { formatForDisplay } from '@tanstack/react-hotkeys'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useConfigPublishPayload, useHasAgentComposerUnpublishedChanges } from '@/features/agent-v2/agent-composer/store'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { AgentPublishImpactPopover } from './publish-impact-popover'
|
||||
|
||||
const PUBLISH_AGENT_HOTKEY = 'Mod+Shift+P'
|
||||
const PUBLISH_AGENT_HOTKEY = 'Mod+Shift+P' satisfies RegisterableHotkey
|
||||
const PUBLISH_IMPACT_BAR_HIDE_DELAY = 160
|
||||
|
||||
export type AgentConfigurePublishPayload = {
|
||||
agent_id: string
|
||||
@ -22,6 +25,7 @@ type AgentConfigurePublishState = 'draft' | 'publishing' | 'published' | 'unpubl
|
||||
|
||||
type AgentConfigurePublishBarProps = {
|
||||
agentId: string
|
||||
activeConfigIsPublished?: boolean
|
||||
activeConfigSnapshot?: AgentConfigSnapshotSummaryResponse | null
|
||||
agentSoulConfig?: AgentConfigSnapshotDetailResponse['config_snapshot']
|
||||
agentName?: string | null
|
||||
@ -31,17 +35,17 @@ type AgentConfigurePublishBarProps = {
|
||||
}
|
||||
draftSavedAt?: number
|
||||
isPublishing?: boolean
|
||||
publishedReferenceCount?: number
|
||||
publishedReferences?: AgentPublishedReferenceResponse[]
|
||||
onPublish?: (payload: AgentConfigurePublishPayload) => void | Promise<void>
|
||||
onOpenVersions: () => void
|
||||
}
|
||||
|
||||
function getPublishState({
|
||||
activeConfigIsPublished,
|
||||
activeConfigSnapshot,
|
||||
isDirty,
|
||||
isPublishing,
|
||||
}: {
|
||||
activeConfigIsPublished?: boolean
|
||||
activeConfigSnapshot?: AgentConfigSnapshotSummaryResponse | null
|
||||
isDirty: boolean
|
||||
isPublishing: boolean
|
||||
@ -52,7 +56,7 @@ function getPublishState({
|
||||
if (!activeConfigSnapshot)
|
||||
return 'draft'
|
||||
|
||||
if (isDirty)
|
||||
if (!activeConfigIsPublished || isDirty)
|
||||
return 'unpublished'
|
||||
|
||||
return 'published'
|
||||
@ -70,19 +74,20 @@ function PublishShortcut() {
|
||||
|
||||
export function AgentConfigurePublishBar({
|
||||
agentId,
|
||||
activeConfigIsPublished,
|
||||
activeConfigSnapshot,
|
||||
agentSoulConfig,
|
||||
agentName,
|
||||
currentModel,
|
||||
draftSavedAt,
|
||||
isPublishing = false,
|
||||
publishedReferenceCount = 0,
|
||||
publishedReferences = [],
|
||||
onPublish,
|
||||
onOpenVersions,
|
||||
}: AgentConfigurePublishBarProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||
const [shouldHidePublishBar, setShouldHidePublishBar] = useState(false)
|
||||
const hidePublishBarTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
|
||||
const hasUnpublishedChanges = useHasAgentComposerUnpublishedChanges()
|
||||
const publishPayload = useConfigPublishPayload({
|
||||
agentId,
|
||||
@ -90,12 +95,27 @@ export function AgentConfigurePublishBar({
|
||||
currentModel,
|
||||
})
|
||||
const publishState = getPublishState({
|
||||
activeConfigIsPublished,
|
||||
activeConfigSnapshot,
|
||||
isDirty: hasUnpublishedChanges,
|
||||
isPublishing,
|
||||
})
|
||||
const canPublish = publishState === 'draft' || publishState === 'unpublished'
|
||||
|
||||
const handleImpactPopoverOpenChange = (open: boolean) => {
|
||||
if (hidePublishBarTimerRef.current)
|
||||
clearTimeout(hidePublishBarTimerRef.current)
|
||||
|
||||
if (!open) {
|
||||
setShouldHidePublishBar(false)
|
||||
return
|
||||
}
|
||||
|
||||
hidePublishBarTimerRef.current = setTimeout(() => {
|
||||
setShouldHidePublishBar(true)
|
||||
}, PUBLISH_IMPACT_BAR_HIDE_DELAY)
|
||||
}
|
||||
|
||||
const handlePublish = () => {
|
||||
if (!canPublish)
|
||||
return
|
||||
@ -103,13 +123,12 @@ export function AgentConfigurePublishBar({
|
||||
void onPublish?.(publishPayload)
|
||||
}
|
||||
|
||||
useHotkey(PUBLISH_AGENT_HOTKEY, (event) => {
|
||||
event.preventDefault()
|
||||
handlePublish()
|
||||
}, {
|
||||
enabled: canPublish,
|
||||
ignoreInputs: false,
|
||||
})
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (hidePublishBarTimerRef.current)
|
||||
clearTimeout(hidePublishBarTimerRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const publishedMeta = activeConfigSnapshot?.created_at
|
||||
? t('agentDetail.configure.publishBar.publishedAt', {
|
||||
@ -166,7 +185,13 @@ export function AgentConfigurePublishBar({
|
||||
|
||||
return (
|
||||
<div className="flex h-16 shrink-0 items-center justify-center px-4 pt-2 pb-3">
|
||||
<div className="flex max-w-full min-w-0 items-center gap-2 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]">
|
||||
<div
|
||||
className={cn(
|
||||
'flex max-w-full min-w-0 items-center gap-2 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]',
|
||||
shouldHidePublishBar && 'pointer-events-none opacity-0',
|
||||
)}
|
||||
aria-hidden={shouldHidePublishBar}
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-1 px-2 system-xs-regular text-text-tertiary">
|
||||
<span className="flex size-4 shrink-0 items-center justify-center">
|
||||
<StatusDot size="small" status={currentStateMeta.dotStatus} />
|
||||
@ -187,10 +212,12 @@ export function AgentConfigurePublishBar({
|
||||
</button>
|
||||
<AgentPublishImpactPopover
|
||||
actionLabel={currentStateMeta.actionLabel}
|
||||
actionShortcut={currentStateMeta.showShortcut ? <PublishShortcut /> : null}
|
||||
hotkey={PUBLISH_AGENT_HOTKEY}
|
||||
agentId={agentId}
|
||||
agentName={agentName}
|
||||
disabled={!canPublish}
|
||||
publishedReferenceCount={publishedReferenceCount}
|
||||
publishedReferences={publishedReferences}
|
||||
onOpenChange={handleImpactPopoverOpenChange}
|
||||
onPublish={handlePublish}
|
||||
trigger={(
|
||||
<Button
|
||||
|
||||
@ -1,76 +1,122 @@
|
||||
'use client'
|
||||
|
||||
import type { AgentPublishedReferenceResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { MouseEvent, ReactElement } from 'react'
|
||||
import type { AgentIconType, AgentReferencingWorkflowResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { RegisterableHotkey } from '@tanstack/react-hotkeys'
|
||||
import type { MouseEvent, ReactElement, ReactNode } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useHotkey } from '@tanstack/react-hotkeys'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { cloneElement, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import Link from '@/next/link'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
|
||||
type AgentPublishImpactPopoverProps = {
|
||||
actionLabel: string
|
||||
actionShortcut?: ReactNode
|
||||
hotkey: RegisterableHotkey
|
||||
agentId: string
|
||||
agentName?: string | null
|
||||
disabled?: boolean
|
||||
publishedReferenceCount?: number
|
||||
publishedReferences?: AgentPublishedReferenceResponse[]
|
||||
trigger: ReactElement<{
|
||||
onClick?: (event: MouseEvent<HTMLElement>) => void
|
||||
}>
|
||||
onOpenChange?: (open: boolean) => void
|
||||
onPublish: () => void
|
||||
}
|
||||
|
||||
const workflowReferenceAvatarClassNames = [
|
||||
'bg-components-icon-bg-green-soft text-components-icon-bg-green-solid',
|
||||
'bg-components-icon-bg-orange-dark-soft text-components-icon-bg-orange-dark-solid',
|
||||
'bg-components-icon-bg-pink-soft text-components-icon-bg-pink-solid',
|
||||
'bg-components-icon-bg-blue-soft text-components-icon-bg-blue-solid',
|
||||
] as const
|
||||
|
||||
const getWorkflowReferenceHref = (reference: AgentPublishedReferenceResponse) => `/app/${reference.app_id}/workflow`
|
||||
|
||||
const getWorkflowReferenceInitial = (name: string) => {
|
||||
return name.trim().charAt(0).toUpperCase() || '?'
|
||||
}
|
||||
const getWorkflowReferenceHref = (reference: AgentReferencingWorkflowResponse) => `/app/${reference.app_id}/workflow`
|
||||
|
||||
export function AgentPublishImpactPopover({
|
||||
actionLabel,
|
||||
actionShortcut,
|
||||
hotkey,
|
||||
agentId,
|
||||
agentName,
|
||||
disabled = false,
|
||||
publishedReferenceCount = 0,
|
||||
publishedReferences = [],
|
||||
trigger,
|
||||
onOpenChange,
|
||||
onPublish,
|
||||
}: AgentPublishImpactPopoverProps) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const [open, setOpen] = useState(false)
|
||||
const hasPublishedReferences = publishedReferenceCount > 0 && publishedReferences.length > 0
|
||||
|
||||
if (!hasPublishedReferences || disabled)
|
||||
return trigger
|
||||
|
||||
const triggerWithImpact = cloneElement(trigger, {
|
||||
onClick: (event: MouseEvent<HTMLElement>) => {
|
||||
event.preventDefault()
|
||||
setOpen(true)
|
||||
},
|
||||
const [isCheckingReferences, setIsCheckingReferences] = useState(false)
|
||||
const workflowReferencesQuery = useQuery({
|
||||
...consoleQuery.agent.byAgentId.referencingWorkflows.get.queryOptions({
|
||||
input: {
|
||||
params: {
|
||||
agent_id: agentId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
enabled: false,
|
||||
})
|
||||
const publishedReferences = workflowReferencesQuery.data?.data ?? []
|
||||
|
||||
const updateOpen = (nextOpen: boolean) => {
|
||||
setOpen(nextOpen)
|
||||
onOpenChange?.(nextOpen)
|
||||
}
|
||||
|
||||
const handlePublish = () => {
|
||||
setOpen(false)
|
||||
updateOpen(false)
|
||||
onPublish()
|
||||
}
|
||||
|
||||
const handlePublishRequest = async () => {
|
||||
if (disabled || isCheckingReferences)
|
||||
return
|
||||
|
||||
if (open) {
|
||||
handlePublish()
|
||||
return
|
||||
}
|
||||
|
||||
setIsCheckingReferences(true)
|
||||
try {
|
||||
const result = await workflowReferencesQuery.refetch()
|
||||
const references = result.data?.data ?? []
|
||||
if (references.length > 0) {
|
||||
updateOpen(true)
|
||||
return
|
||||
}
|
||||
|
||||
onPublish()
|
||||
}
|
||||
finally {
|
||||
setIsCheckingReferences(false)
|
||||
}
|
||||
}
|
||||
|
||||
useHotkey(hotkey, (event) => {
|
||||
event.preventDefault()
|
||||
void handlePublishRequest()
|
||||
}, {
|
||||
enabled: !disabled,
|
||||
ignoreInputs: false,
|
||||
})
|
||||
|
||||
if (disabled)
|
||||
return trigger
|
||||
|
||||
const triggerWithImpact = cloneElement(trigger, {
|
||||
onClick: async (event: MouseEvent<HTMLElement>) => {
|
||||
event.preventDefault()
|
||||
await handlePublishRequest()
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover open={open} onOpenChange={updateOpen}>
|
||||
<PopoverTrigger
|
||||
render={triggerWithImpact}
|
||||
/>
|
||||
<PopoverContent
|
||||
placement="top-end"
|
||||
sideOffset={-40}
|
||||
popupClassName="w-96 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-0 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px]"
|
||||
sideOffset={-32}
|
||||
popupClassName="w-96 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-0 shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px] transition-[transform,scale,opacity] duration-150 ease-out data-starting-style:translate-y-5 data-ending-style:translate-y-5 motion-reduce:transition-none motion-reduce:data-starting-style:translate-y-0 motion-reduce:data-ending-style:translate-y-0"
|
||||
>
|
||||
<div className="flex flex-col gap-0">
|
||||
<div className="flex flex-col gap-0.5 px-3 pt-3.5 pb-1">
|
||||
@ -84,7 +130,7 @@ export function AgentPublishImpactPopover({
|
||||
{t('agentDetail.configure.publishImpact.descriptionPrefix')}
|
||||
{' '}
|
||||
<span className="system-xs-medium">
|
||||
{t('agentDetail.configure.publishImpact.workflowCount', { count: publishedReferenceCount })}
|
||||
{t('agentDetail.configure.publishImpact.workflowCount', { count: publishedReferences.length })}
|
||||
</span>
|
||||
{t('agentDetail.configure.publishImpact.descriptionSuffix')}
|
||||
</p>
|
||||
@ -95,8 +141,8 @@ export function AgentPublishImpactPopover({
|
||||
{t('agentDetail.configure.publishImpact.affectedWorkflows')}
|
||||
</div>
|
||||
<div className="flex max-h-44 flex-col gap-px overflow-y-auto rounded-xl border border-components-panel-border p-1">
|
||||
{publishedReferences.map((reference, index) => (
|
||||
<ReferenceLink key={`${reference.app_id}-${reference.workflow_id}`} reference={reference} index={index} />
|
||||
{publishedReferences.map(reference => (
|
||||
<ReferenceLink key={`${reference.app_id}-${reference.workflow_id}`} reference={reference} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
@ -106,17 +152,18 @@ export function AgentPublishImpactPopover({
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="h-8 min-w-18 rounded-lg px-3"
|
||||
onClick={() => setOpen(false)}
|
||||
onClick={() => updateOpen(false)}
|
||||
>
|
||||
{t('agentDetail.configure.publishImpact.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="h-8 min-w-18 rounded-lg px-3"
|
||||
className="h-8 min-w-18 gap-1 rounded-lg px-3"
|
||||
onClick={handlePublish}
|
||||
>
|
||||
{actionLabel}
|
||||
<span className="shrink-0">{actionLabel}</span>
|
||||
{actionShortcut}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -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 (
|
||||
<Link
|
||||
href={getWorkflowReferenceHref(reference)}
|
||||
@ -139,14 +187,14 @@ function ReferenceLink({
|
||||
rel="noopener noreferrer"
|
||||
className="flex min-w-0 items-center gap-2 rounded-lg py-1 pr-2.5 pl-2 system-sm-regular text-text-secondary hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid focus-visible:outline-hidden"
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'flex size-5 shrink-0 items-center justify-center rounded-md border-[0.5px] border-divider-regular system-xs-medium',
|
||||
workflowReferenceAvatarClassNames[index % workflowReferenceAvatarClassNames.length],
|
||||
)}
|
||||
>
|
||||
{getWorkflowReferenceInitial(reference.app_name)}
|
||||
<span aria-hidden className="shrink-0">
|
||||
<AppIcon
|
||||
size="tiny"
|
||||
iconType={iconType}
|
||||
icon={reference.app_icon ?? undefined}
|
||||
background={reference.app_icon_background}
|
||||
imageUrl={imageUrl}
|
||||
/>
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{reference.app_name}</span>
|
||||
<span aria-hidden className="i-ri-external-link-line size-3 shrink-0 text-text-tertiary" />
|
||||
|
||||
@ -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/',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@ -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 }) => (
|
||||
<FileTreeFile selected={selected} onClick={() => onSelectFile(file)}>
|
||||
{children}
|
||||
</FileTreeFile>
|
||||
)
|
||||
: undefined}
|
||||
header={(
|
||||
<>
|
||||
<h3 id="agent-skill-detail-files-heading" className="sr-only">
|
||||
@ -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 (
|
||||
<div className="flex min-h-40 items-center justify-center">
|
||||
<Loading type="area" />
|
||||
@ -114,7 +144,7 @@ function AgentFilePreviewContent({
|
||||
)
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
if (isError || isDownloadError) {
|
||||
return (
|
||||
<p className="system-sm-regular text-text-tertiary">
|
||||
{t('agentDetail.configure.files.preview.failed')}
|
||||
@ -122,6 +152,45 @@ function AgentFilePreviewContent({
|
||||
)
|
||||
}
|
||||
|
||||
if (isImage && downloadUrl) {
|
||||
return (
|
||||
<div className="flex min-h-40 items-start justify-center">
|
||||
<img
|
||||
src={downloadUrl}
|
||||
alt={fileName ?? ''}
|
||||
className="max-h-[560px] max-w-full rounded-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (binary) {
|
||||
if (downloadUrl) {
|
||||
return (
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
||||
<span className="system-sm-regular text-text-tertiary">
|
||||
{t('agentDetail.configure.files.preview.unsupported')}
|
||||
</span>
|
||||
<a
|
||||
href={downloadUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="inline-flex min-w-0 items-center gap-1 rounded-md px-2 py-1 system-sm-medium text-text-accent outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
>
|
||||
<span aria-hidden className="i-ri-download-2-line size-4 shrink-0" />
|
||||
<span className="shrink-0">{tCommon('operation.download')}</span>
|
||||
</a>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="system-sm-regular text-text-tertiary">
|
||||
{t('agentDetail.configure.files.preview.empty')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
return (
|
||||
<p className="system-sm-regular text-text-tertiary">
|
||||
@ -171,8 +240,14 @@ export function AgentSkillDetailDialog({
|
||||
>
|
||||
{detail.filePreview && (
|
||||
<AgentFilePreviewContent
|
||||
binary={detail.filePreview.binary}
|
||||
content={detail.filePreview.content}
|
||||
downloadUrl={detail.filePreview.downloadUrl}
|
||||
fileName={detail.filePreview.fileName}
|
||||
isDownloadError={detail.filePreview.isDownloadError}
|
||||
isDownloadLoading={detail.filePreview.isDownloadLoading}
|
||||
isError={detail.filePreview.isError}
|
||||
isImage={detail.filePreview.isImage}
|
||||
isLoading={detail.filePreview.isLoading}
|
||||
/>
|
||||
)}
|
||||
@ -181,7 +256,12 @@ export function AgentSkillDetailDialog({
|
||||
))}
|
||||
</ScrollArea>
|
||||
<div className="flex w-56 max-w-56 min-w-0 shrink-0 items-start justify-center p-4 pl-2">
|
||||
<AgentSkillFileList files={detail.files} fileCount={fileCount} selectedFileId={detail.selectedFileId} />
|
||||
<AgentSkillFileList
|
||||
files={detail.files}
|
||||
fileCount={fileCount}
|
||||
selectedFileId={detail.selectedFileId}
|
||||
onSelectFile={detail.onSelectFile}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@ -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<string>()
|
||||
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 (
|
||||
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
|
||||
@ -71,7 +129,7 @@ export function AgentSkillItem({
|
||||
<button
|
||||
type="button"
|
||||
className="flex h-full min-w-0 flex-1 cursor-pointer items-center gap-1 rounded-md text-left outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
onClick={() => setIsPreviewOpen(true)}
|
||||
onClick={handleOpenPreview}
|
||||
>
|
||||
<span aria-hidden className="i-custom-public-agent-building-blocks size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate system-sm-medium text-text-secondary">
|
||||
@ -103,6 +161,19 @@ export function AgentSkillItem({
|
||||
detail={{
|
||||
description: skill.description ?? t('agentDetail.configure.skills.tip'),
|
||||
files: detailFiles,
|
||||
filePreview: {
|
||||
binary: previewQuery.data?.binary,
|
||||
content: previewQuery.data?.text ?? undefined,
|
||||
downloadUrl: downloadQuery.data?.url,
|
||||
fileName: selectedFile?.name,
|
||||
isDownloadError: downloadQuery.isError,
|
||||
isDownloadLoading: (isImagePreviewFile || !!previewQuery.data?.binary) && downloadQuery.isPending,
|
||||
isError: previewQuery.isError,
|
||||
isImage: isImagePreviewFile,
|
||||
isLoading: previewQuery.isPending,
|
||||
},
|
||||
onSelectFile: file => setSelectedFileId(file.id),
|
||||
selectedFileId: previewFileId,
|
||||
sections: [],
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -113,6 +113,7 @@ function AgentConfigurePageLoadedContent({
|
||||
>
|
||||
<AgentOrchestratePanel
|
||||
agentId={agentId}
|
||||
activeConfigIsPublished={agentQuery.data?.active_config_is_published}
|
||||
activeConfigSnapshot={activeConfigSnapshot}
|
||||
agentSoulConfig={agentSoulConfig}
|
||||
agentName={agentQuery.data?.name}
|
||||
|
||||
@ -88,6 +88,18 @@ export function useAgentConfigureSync({
|
||||
consoleQuery.agent.byAgentId.composer.get.queryKey({ input: { params: { agent_id: agentId } } }),
|
||||
composerState,
|
||||
)
|
||||
queryClient.setQueryData(
|
||||
consoleQuery.agent.byAgentId.get.queryKey({ input: { params: { agent_id: agentId } } }),
|
||||
(agentDetail) => {
|
||||
if (!agentDetail)
|
||||
return agentDetail
|
||||
|
||||
return {
|
||||
...agentDetail,
|
||||
active_config_is_published: true,
|
||||
}
|
||||
},
|
||||
)
|
||||
void queryClient.invalidateQueries({
|
||||
queryKey: consoleQuery.agent.byAgentId.versions.get.key(),
|
||||
})
|
||||
|
||||
@ -0,0 +1,303 @@
|
||||
import type { AgentLogConversationItemResponse } from '@dify/contracts/api/console/agent/types.gen'
|
||||
import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from 'react'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import {
|
||||
ScrollAreaContent,
|
||||
ScrollAreaRoot,
|
||||
ScrollAreaScrollbar,
|
||||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '@langgenius/dify-ui/scroll-area'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { LogSourceCell } from './source-cell'
|
||||
|
||||
export function AgentLogsTable({
|
||||
logs,
|
||||
isPending,
|
||||
isError,
|
||||
isSuccess,
|
||||
onRetry,
|
||||
}: {
|
||||
logs: AgentLogConversationItemResponse[]
|
||||
isPending: boolean
|
||||
isError: boolean
|
||||
isSuccess: boolean
|
||||
onRetry: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const tableHeaderLabels = {
|
||||
unread: t('agentDetail.logs.table.unread'),
|
||||
title: t('agentDetail.logs.table.title'),
|
||||
source: t('agentDetail.logs.table.source'),
|
||||
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'),
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-w-0 flex-col overflow-hidden">
|
||||
<div className="shrink-0">
|
||||
<table aria-hidden="true" className="w-full table-fixed border-collapse">
|
||||
<LogsTableColGroup />
|
||||
<LogsTableHeader labels={tableHeaderLabels} />
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-label={t('agentDetail.logs.title')}
|
||||
role="region"
|
||||
tabIndex={-1}
|
||||
className="overscroll-contain"
|
||||
>
|
||||
<ScrollAreaContent>
|
||||
<table className="w-full table-fixed border-collapse">
|
||||
<LogsTableColGroup />
|
||||
<LogsTableHeader labels={tableHeaderLabels} rowClassName="sr-only" />
|
||||
<AgentLogsTableBody
|
||||
logs={logs}
|
||||
isPending={isPending}
|
||||
isError={isError}
|
||||
isSuccess={isSuccess}
|
||||
onRetry={onRetry}
|
||||
/>
|
||||
</table>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className="data-[orientation=vertical]:translate-x-1">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AgentLogsTableBody({
|
||||
logs,
|
||||
isPending,
|
||||
isError,
|
||||
isSuccess,
|
||||
onRetry,
|
||||
}: {
|
||||
logs: AgentLogConversationItemResponse[]
|
||||
isPending: boolean
|
||||
isError: boolean
|
||||
isSuccess: boolean
|
||||
onRetry: () => void
|
||||
}) {
|
||||
const { t } = useTranslation('agentV2')
|
||||
const { t: tCommon } = useTranslation('common')
|
||||
const { formatTime } = useTimestamp()
|
||||
const notAvailable = t('agentDetail.logs.notAvailable')
|
||||
const formatLogTime = (value?: number | null) =>
|
||||
value == null ? notAvailable : formatTime(value, t('roster.dateTimeFormat'))
|
||||
|
||||
return (
|
||||
<tbody className="system-sm-regular text-text-secondary">
|
||||
{isPending && (
|
||||
<LogsSkeletonRows />
|
||||
)}
|
||||
{isError && (
|
||||
<LogsStateRow>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span>{t('agentDetail.logs.loadFailed')}</span>
|
||||
<Button variant="secondary" size="small" onClick={onRetry}>
|
||||
{tCommon('operation.retry')}
|
||||
</Button>
|
||||
</div>
|
||||
</LogsStateRow>
|
||||
)}
|
||||
{isSuccess && logs.length === 0 && (
|
||||
<LogsStateRow>
|
||||
{t('agentDetail.logs.empty')}
|
||||
</LogsStateRow>
|
||||
)}
|
||||
{isSuccess && logs.map(log => (
|
||||
<tr
|
||||
key={log.id}
|
||||
className="h-10 border-b border-divider-subtle hover:bg-background-default-hover"
|
||||
>
|
||||
<td className="px-0">
|
||||
<span className={cn(
|
||||
'mx-auto block size-1.5 rounded-full',
|
||||
log.unread ? 'bg-util-colors-blue-blue-500' : 'bg-transparent',
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<TableCell className="system-sm-medium text-text-secondary">
|
||||
{log.title || notAvailable}
|
||||
</TableCell>
|
||||
<td className="px-3">
|
||||
<LogSourceCell source={log.source} />
|
||||
</td>
|
||||
<TableCell translate="no">
|
||||
{log.end_user_id || notAvailable}
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums">
|
||||
{log.message_count}
|
||||
</TableCell>
|
||||
<TableCell className="text-text-quaternary">
|
||||
{formatRate(log.user_rate, notAvailable)}
|
||||
</TableCell>
|
||||
<TableCell className="text-text-quaternary">
|
||||
{formatRate(log.operation_rate, notAvailable)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatLogTime(log.updated_at)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatLogTime(log.created_at)}
|
||||
</TableCell>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<thead>
|
||||
<tr className={cn('h-7 bg-background-section-burn text-left system-xs-medium-uppercase text-text-tertiary', rowClassName)}>
|
||||
<th scope="col" className="rounded-l-lg px-0">
|
||||
<span className="sr-only">{labels.unread}</span>
|
||||
</th>
|
||||
<TableHead>{labels.title}</TableHead>
|
||||
<TableHead>{labels.source}</TableHead>
|
||||
<TableHead>{labels.endUser}</TableHead>
|
||||
<TableHead>{labels.messageCount}</TableHead>
|
||||
<TableHead>{labels.userRate}</TableHead>
|
||||
<TableHead>{labels.operationRate}</TableHead>
|
||||
<TableHead>{labels.updatedTime}</TableHead>
|
||||
<TableHead className="rounded-r-lg">{labels.createdTime}</TableHead>
|
||||
</tr>
|
||||
</thead>
|
||||
)
|
||||
}
|
||||
|
||||
function LogsTableColGroup() {
|
||||
return (
|
||||
<colgroup>
|
||||
<col className="w-5" />
|
||||
<col className="w-[28%]" />
|
||||
<col className="w-[16%]" />
|
||||
<col className="w-[15%]" />
|
||||
<col className="w-[8%]" />
|
||||
<col className="w-[7%]" />
|
||||
<col className="w-[7%]" />
|
||||
<col className="w-[10%]" />
|
||||
<col className="w-[9%]" />
|
||||
</colgroup>
|
||||
)
|
||||
}
|
||||
|
||||
function LogsStateRow({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
return (
|
||||
<tr className="h-20 border-b border-divider-subtle">
|
||||
<td colSpan={9} className="px-3 text-center text-text-tertiary">
|
||||
{children}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}
|
||||
|
||||
function LogsSkeletonRows() {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: 10 }, (_, index) => (
|
||||
<tr key={index} className="h-10 border-b border-divider-subtle">
|
||||
<td className="px-0">
|
||||
<span className="mx-auto block size-1.5 rounded-full bg-text-quaternary opacity-20" />
|
||||
</td>
|
||||
<td className="px-3">
|
||||
<div className="h-3 w-3/4 rounded-sm bg-text-quaternary opacity-20" />
|
||||
</td>
|
||||
<td className="px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="size-5 rounded-full bg-text-quaternary opacity-20" />
|
||||
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3">
|
||||
<div className="h-3 w-28 rounded-sm bg-text-quaternary opacity-20" />
|
||||
</td>
|
||||
<td className="px-3">
|
||||
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
|
||||
</td>
|
||||
<td className="px-3">
|
||||
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
|
||||
</td>
|
||||
<td className="px-3">
|
||||
<div className="h-3 w-8 rounded-sm bg-text-quaternary opacity-20" />
|
||||
</td>
|
||||
<td className="px-3">
|
||||
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
|
||||
</td>
|
||||
<td className="px-3">
|
||||
<div className="h-3 w-24 rounded-sm bg-text-quaternary opacity-20" />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({
|
||||
className,
|
||||
...props
|
||||
}: ThHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<th
|
||||
scope="col"
|
||||
className={cn('px-3 text-left whitespace-nowrap', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: TdHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<td
|
||||
className={cn('min-w-0 px-3 whitespace-nowrap', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="truncate">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
@ -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 (
|
||||
<div className="truncate text-text-quaternary">
|
||||
{t('agentDetail.logs.notAvailable')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<LogSourceIcon source={source} />
|
||||
<div className="min-w-0 flex-1 truncate">
|
||||
{source.app_name}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -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 <span aria-hidden className="i-ri-apps-2-line size-5 shrink-0 text-text-quaternary" />
|
||||
|
||||
return (
|
||||
<AppIcon
|
||||
size="xs"
|
||||
rounded
|
||||
iconType={getLogSourceIconType(source)}
|
||||
icon={source.app_icon ?? undefined}
|
||||
background={source.app_icon_background}
|
||||
imageUrl={getLogSourceImageUrl(source)}
|
||||
className="size-5 text-sm"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -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 (
|
||||
<Combobox<AgentLogSourceResponse, true>
|
||||
multiple
|
||||
items={groups}
|
||||
value={selectedSources}
|
||||
itemToStringLabel={getSourceLabel}
|
||||
onValueChange={(nextSources) => {
|
||||
setInputValue('')
|
||||
onChange(nextSources.map(source => source.id))
|
||||
}}
|
||||
inputValue={inputValue}
|
||||
onInputValueChange={setInputValue}
|
||||
>
|
||||
<ComboboxTrigger
|
||||
aria-label={t('agentDetail.logs.filters.source.label')}
|
||||
className="mt-0 w-fit max-w-full min-w-22"
|
||||
>
|
||||
<ComboboxValue placeholder={t('agentDetail.logs.filters.source.all')}>
|
||||
{(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 })
|
||||
}}
|
||||
</ComboboxValue>
|
||||
</ComboboxTrigger>
|
||||
<ComboboxContent popupClassName="w-80 p-0">
|
||||
<div className="p-2 pb-1">
|
||||
<ComboboxInputGroup className="h-8 min-h-8 px-2">
|
||||
<span aria-hidden className="mr-0.5 i-ri-search-line size-4 shrink-0 text-components-input-text-placeholder" />
|
||||
<ComboboxInput
|
||||
aria-label={t('agentDetail.logs.filters.source.searchLabel')}
|
||||
placeholder={t('agentDetail.logs.filters.source.searchPlaceholder')}
|
||||
className="block h-4.5 grow px-1 py-0 system-sm-regular text-components-input-text-filled"
|
||||
/>
|
||||
<ComboboxClear className="mr-0" />
|
||||
</ComboboxInputGroup>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<SourcePickerStatus>
|
||||
{t('agentDetail.logs.filters.source.loading')}
|
||||
</SourcePickerStatus>
|
||||
)}
|
||||
{isError && (
|
||||
<SourcePickerStatus className="flex items-center justify-center gap-2">
|
||||
<span>{t('agentDetail.logs.filters.source.loadFailed')}</span>
|
||||
<Button variant="secondary" size="small" onClick={onRetry}>
|
||||
{t('operation.retry', { ns: 'common' })}
|
||||
</Button>
|
||||
</SourcePickerStatus>
|
||||
)}
|
||||
{!isLoading && !isError && (
|
||||
<>
|
||||
<ComboboxList className="max-h-69 p-2 pt-1">
|
||||
{groups.map(group => (
|
||||
<ComboboxGroup key={group.type} items={group.sources ?? []}>
|
||||
<ComboboxGroupLabel className="px-1 pt-2 pb-1">
|
||||
{getSourceGroupLabel(group, t)}
|
||||
</ComboboxGroupLabel>
|
||||
<ComboboxCollection>
|
||||
{(source: AgentLogSourceResponse) => (
|
||||
<ComboboxItem
|
||||
key={source.id}
|
||||
value={source}
|
||||
className="min-h-7 grid-cols-[1fr] gap-0 px-1 py-1"
|
||||
>
|
||||
<ComboboxItemText className="flex min-w-0 items-center gap-2 px-0 system-sm-regular">
|
||||
<SourceCheckbox checked={value.includes(source.id)} />
|
||||
<LogSourceIcon source={source} />
|
||||
<span className="min-w-0 flex-1 truncate">
|
||||
{source.app_name}
|
||||
</span>
|
||||
</ComboboxItemText>
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxCollection>
|
||||
</ComboboxGroup>
|
||||
))}
|
||||
</ComboboxList>
|
||||
<ComboboxEmpty className="px-3 py-3 text-center system-xs-regular">
|
||||
{t('agentDetail.logs.filters.source.empty')}
|
||||
</ComboboxEmpty>
|
||||
</>
|
||||
)}
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
||||
function SourcePickerStatus({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('px-3 py-3 text-center system-xs-regular text-text-tertiary', className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SourceCheckbox({
|
||||
checked,
|
||||
}: {
|
||||
checked: boolean
|
||||
}) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn(
|
||||
'flex size-4 shrink-0 items-center justify-center rounded-sm border shadow-xs shadow-shadow-shadow-3',
|
||||
checked
|
||||
? 'border-transparent bg-components-checkbox-bg text-components-checkbox-icon'
|
||||
: 'border-components-checkbox-border bg-components-checkbox-bg-unchecked',
|
||||
)}
|
||||
>
|
||||
{checked && <span className="i-ri-check-line size-3" />}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@ -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<T extends string> = {
|
||||
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<SourceKey, 'all'>
|
||||
unread?: boolean
|
||||
}
|
||||
|
||||
export const periodOptions: Array<FilterOption<PeriodKey>> = [
|
||||
{ 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<FilterOption<SourceKey>> = [
|
||||
{ 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<Record<PeriodKey, number>> = {
|
||||
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<T extends string>(options: Array<FilterOption<T>>, 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),
|
||||
}
|
||||
}
|
||||
@ -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<PeriodKey>('last7days')
|
||||
const [source, setSource] = useState<SourceKey>('all')
|
||||
const [source, setSource] = useState<SourceFilterValue>([])
|
||||
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 (
|
||||
<section
|
||||
@ -110,30 +124,19 @@ export function AgentLogsPage() {
|
||||
}}
|
||||
/>
|
||||
|
||||
<Select
|
||||
<AgentLogSourcePicker
|
||||
value={source}
|
||||
onValueChange={(nextValue) => {
|
||||
if (nextValue) {
|
||||
setPage(1)
|
||||
setSource(nextValue as SourceKey)
|
||||
}
|
||||
groups={logSourcesQuery.data?.groups ?? []}
|
||||
isLoading={logSourcesQuery.isPending}
|
||||
isError={logSourcesQuery.isError}
|
||||
onRetry={() => {
|
||||
void logSourcesQuery.refetch()
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
aria-label={t('agentDetail.logs.filters.source.label')}
|
||||
className="mt-0 w-fit max-w-full min-w-22"
|
||||
>
|
||||
{t(selectedSource.labelKey)}
|
||||
</SelectTrigger>
|
||||
<SelectContent popupClassName="w-80">
|
||||
{sourceOptions.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{t(option.labelKey)}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
onChange={(nextSource) => {
|
||||
setPage(1)
|
||||
setSource(nextSource)
|
||||
}}
|
||||
/>
|
||||
|
||||
<SearchInput
|
||||
aria-label={t('agentDetail.logs.filters.search.label')}
|
||||
@ -148,93 +151,27 @@ export function AgentLogsPage() {
|
||||
</div>
|
||||
|
||||
<Sort
|
||||
order={sortOrder}
|
||||
value={sortValue}
|
||||
order="desc"
|
||||
value="updated_at"
|
||||
items={[
|
||||
{ value: 'created_at', name: t('agentDetail.logs.filters.sort.lastCreatedTime') },
|
||||
{ value: 'updated_at', name: t('agentDetail.logs.filters.sort.lastUpdatedTime') },
|
||||
]}
|
||||
onSelect={(value) => {
|
||||
setPage(1)
|
||||
setSortBy(value)
|
||||
}}
|
||||
onSelect={() => {}}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="min-h-0 flex-1 px-6 pt-2 pb-3">
|
||||
<div className="flex h-full min-w-0 flex-col overflow-x-auto">
|
||||
<div className="min-w-[1212px] shrink-0">
|
||||
<table aria-hidden="true" className="w-full table-fixed border-collapse">
|
||||
<LogsTableColGroup />
|
||||
<LogsTableHeader labels={tableHeaderLabels} />
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<ScrollAreaRoot className="relative min-h-0 min-w-[1212px] flex-1 overflow-hidden">
|
||||
<ScrollAreaViewport
|
||||
aria-label={t('agentDetail.logs.title')}
|
||||
role="region"
|
||||
tabIndex={-1}
|
||||
className="overscroll-contain"
|
||||
>
|
||||
<ScrollAreaContent>
|
||||
<table className="w-full table-fixed border-collapse">
|
||||
<LogsTableColGroup />
|
||||
<LogsTableHeader labels={tableHeaderLabels} rowClassName="sr-only" />
|
||||
<tbody className="system-sm-regular text-text-secondary">
|
||||
{rows.length > 0
|
||||
? rows.map(log => (
|
||||
<tr
|
||||
key={log.id}
|
||||
className="h-10 border-b border-divider-subtle hover:bg-background-default-hover"
|
||||
>
|
||||
<td className="px-0">
|
||||
<span className={cn(
|
||||
'mx-auto block size-1.5 rounded-full',
|
||||
log.unread ? 'bg-util-colors-blue-blue-500' : 'bg-transparent',
|
||||
)}
|
||||
/>
|
||||
</td>
|
||||
<TableCell className="system-sm-medium text-text-secondary">
|
||||
{log.title}
|
||||
</TableCell>
|
||||
<TableCell translate="no">
|
||||
{log.endUser}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.messageCount}
|
||||
</TableCell>
|
||||
<TableCell className="text-text-quaternary">
|
||||
{log.userRate}
|
||||
</TableCell>
|
||||
<TableCell className="text-text-quaternary">
|
||||
{log.operationRate}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.updatedTime}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{log.createdTime}
|
||||
</TableCell>
|
||||
</tr>
|
||||
))
|
||||
: (
|
||||
<tr className="h-20 border-b border-divider-subtle">
|
||||
<td colSpan={8} className="px-3 text-center text-text-tertiary">
|
||||
{t('agentDetail.logs.empty')}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className="data-[orientation=vertical]:translate-x-1">
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollAreaRoot>
|
||||
</div>
|
||||
<AgentLogsTable
|
||||
logs={logs}
|
||||
isPending={logsQuery.isPending}
|
||||
isError={logsQuery.isError}
|
||||
isSuccess={logsQuery.isSuccess}
|
||||
onRetry={() => {
|
||||
void logsQuery.refetch()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Pagination
|
||||
@ -262,84 +199,3 @@ export function AgentLogsPage() {
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
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 (
|
||||
<thead>
|
||||
<tr className={cn('h-7 bg-background-section-burn text-left system-xs-medium-uppercase text-text-tertiary', rowClassName)}>
|
||||
<th scope="col" className="rounded-l-lg px-0">
|
||||
<span className="sr-only">{labels.unread}</span>
|
||||
</th>
|
||||
<TableHead>{labels.title}</TableHead>
|
||||
<TableHead>{labels.endUser}</TableHead>
|
||||
<TableHead>{labels.messageCount}</TableHead>
|
||||
<TableHead>{labels.userRate}</TableHead>
|
||||
<TableHead>{labels.operationRate}</TableHead>
|
||||
<TableHead>{labels.updatedTime}</TableHead>
|
||||
<TableHead className="rounded-r-lg">{labels.createdTime}</TableHead>
|
||||
</tr>
|
||||
</thead>
|
||||
)
|
||||
}
|
||||
|
||||
function LogsTableColGroup() {
|
||||
return (
|
||||
<colgroup>
|
||||
<col className="w-5" />
|
||||
<col className="w-[42%]" />
|
||||
<col className="w-[14%]" />
|
||||
<col className="w-24" />
|
||||
<col className="w-20" />
|
||||
<col className="w-18" />
|
||||
<col className="w-34" />
|
||||
<col className="w-34" />
|
||||
</colgroup>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({
|
||||
className,
|
||||
...props
|
||||
}: ThHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<th
|
||||
scope="col"
|
||||
className={cn('px-3 text-left whitespace-nowrap', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: TdHTMLAttributes<HTMLTableCellElement>) {
|
||||
return (
|
||||
<td
|
||||
className={cn('min-w-0 px-3 whitespace-nowrap', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="truncate">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ export function AgentDetailPage({
|
||||
return <AgentMonitoringPage agentId={agentId} />
|
||||
|
||||
if (section === 'logs')
|
||||
return <AgentLogsPage />
|
||||
return <AgentLogsPage agentId={agentId} />
|
||||
|
||||
if (section === 'access')
|
||||
return <AgentAccessPage agentId={agentId} />
|
||||
|
||||
@ -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> = {}): 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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 (
|
||||
<article className="group relative col-span-1 h-36.5 min-w-0 overflow-hidden rounded-xl border-[0.5px] border-solid border-components-card-border bg-components-card-bg shadow-xs shadow-shadow-shadow-3 transition-shadow duration-200 ease-in-out hover:shadow-lg">
|
||||
@ -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"
|
||||
>
|
||||
<div className="flex items-center gap-3 pt-3.5 pr-4 pb-2 pl-3.5">
|
||||
<span aria-hidden className="shrink-0">
|
||||
@ -146,6 +168,14 @@ function AgentRosterItem({
|
||||
{agent.description}
|
||||
</div>
|
||||
</div>
|
||||
{isDraft && (
|
||||
<div className="absolute top-[-0.5px] right-0 flex h-5 items-start overflow-hidden">
|
||||
<div className="h-5 w-3 bg-background-section-burn [clip-path:polygon(0_0,100%_0,100%_100%)]" />
|
||||
<div className="flex h-5 items-center bg-background-section-burn pr-2 pl-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t('roster.usageStatus.draft')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
<div className="flex min-w-0 shrink-0 items-center pt-2 pr-3 pb-3 pl-4 system-xs-regular text-text-tertiary">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-1.5">
|
||||
@ -172,14 +202,6 @@ function AgentRosterItem({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isDraft && (
|
||||
<div className="absolute top-0 right-0 flex h-5 items-start overflow-hidden">
|
||||
<div className="h-5 w-3 bg-background-section-burn [clip-path:polygon(0_0,100%_0,100%_100%)]" />
|
||||
<div className="flex h-5 items-center bg-background-section-burn pr-2 pl-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
||||
{t('roster.usageStatus.draft')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="pointer-events-none absolute top-2 right-2 z-20 flex items-center overflow-hidden rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 opacity-0 shadow-lg backdrop-blur-xs transition-opacity group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100 has-data-popup-open:pointer-events-auto has-data-popup-open:opacity-100"
|
||||
>
|
||||
@ -196,7 +218,11 @@ function AgentRosterItem({
|
||||
<span aria-hidden className="i-ri-edit-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span>{t('roster.editInfo')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled className="gap-2">
|
||||
<DropdownMenuItem
|
||||
disabled={duplicateAgentMutation.isPending}
|
||||
className="gap-2"
|
||||
onClick={handleDuplicate}
|
||||
>
|
||||
<span aria-hidden className="i-ri-file-copy-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span>{tCommon('operation.duplicate')}</span>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@ -70,9 +70,6 @@ export function CreateAgentDialog() {
|
||||
toast.success(t('roster.createSuccess'))
|
||||
handleOpenChange(false)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('roster.createFailed'))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -136,9 +136,6 @@ export function EditAgentDialog({
|
||||
toast.success(t('roster.updateSuccess'))
|
||||
onOpenChange(false)
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t('roster.updateFailed'))
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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": "مسودة"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": "پیشنویس"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": "ड्राफ्ट"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": "ドラフト"
|
||||
}
|
||||
|
||||
@ -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": "초안"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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ă"
|
||||
}
|
||||
|
||||
@ -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": "Черновик"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": "ฉบับร่าง"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": "Чернетка"
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
@ -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": "草稿"
|
||||
}
|
||||
|
||||
@ -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": "草稿"
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -361,6 +361,30 @@ export const consoleQuery: RouterUtils<typeof consoleClient> = 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) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user