diff --git a/eslint-suppressions.json b/eslint-suppressions.json index 502ce2933f3..e10b1811574 100644 --- a/eslint-suppressions.json +++ b/eslint-suppressions.json @@ -5206,12 +5206,6 @@ "web/app/components/workflow/block-selector/market-place-plugin/item.tsx": { "erasable-syntax-only/enums": { "count": 1 - }, - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 } }, "web/app/components/workflow/block-selector/market-place-plugin/list.tsx": { @@ -5243,14 +5237,6 @@ "count": 1 } }, - "web/app/components/workflow/block-selector/start-blocks.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/tabs.tsx": { "jsx-a11y/click-events-have-key-events": { "count": 2 @@ -5280,22 +5266,6 @@ "count": 2 } }, - "web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, - "web/app/components/workflow/block-selector/trigger-plugin/item.tsx": { - "jsx-a11y/click-events-have-key-events": { - "count": 1 - }, - "jsx-a11y/no-static-element-interactions": { - "count": 1 - } - }, "web/app/components/workflow/block-selector/types.ts": { "erasable-syntax-only/enums": { "count": 4 diff --git a/packages/iconify-collections/assets/vender/workflow/marketplace.svg b/packages/iconify-collections/assets/vender/workflow/marketplace.svg new file mode 100644 index 00000000000..f04fe04e3f4 --- /dev/null +++ b/packages/iconify-collections/assets/vender/workflow/marketplace.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/iconify-collections/assets/vender/workflow/start-placeholder.svg b/packages/iconify-collections/assets/vender/workflow/start-placeholder.svg new file mode 100644 index 00000000000..f28e12d7ec3 --- /dev/null +++ b/packages/iconify-collections/assets/vender/workflow/start-placeholder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/iconify-collections/assets/vender/workflow/user-input.svg b/packages/iconify-collections/assets/vender/workflow/user-input.svg new file mode 100644 index 00000000000..80157ca000e --- /dev/null +++ b/packages/iconify-collections/assets/vender/workflow/user-input.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/iconify-collections/custom-vender/icons.json b/packages/iconify-collections/custom-vender/icons.json index c8427ff479d..f85c44d912d 100644 --- a/packages/iconify-collections/custom-vender/icons.json +++ b/packages/iconify-collections/custom-vender/icons.json @@ -1,6 +1,6 @@ { "prefix": "custom-vender", - "lastModified": 1776670621, + "lastModified": 1781036531, "icons": { "features-citations": { "body": "" @@ -1084,6 +1084,11 @@ "width": 16, "height": 16 }, + "workflow-marketplace": { + "body": "", + "width": 12, + "height": 12 + }, "workflow-parameter-extractor": { "body": "" }, @@ -1095,12 +1100,22 @@ "width": 16, "height": 16 }, + "workflow-start-placeholder": { + "body": "", + "width": 13.3333, + "height": 13.3333 + }, "workflow-templating-transform": { "body": "" }, "workflow-trigger-all": { "body": "" }, + "workflow-user-input": { + "body": "", + "width": 16, + "height": 16 + }, "workflow-variable-x": { "body": "" }, diff --git a/packages/iconify-collections/custom-vender/info.json b/packages/iconify-collections/custom-vender/info.json index 52df22b171f..f08b18fcad6 100644 --- a/packages/iconify-collections/custom-vender/info.json +++ b/packages/iconify-collections/custom-vender/info.json @@ -1,7 +1,7 @@ { "prefix": "custom-vender", "name": "Dify Custom Vender", - "total": 281, + "total": 284, "version": "0.0.0-private", "author": { "name": "LangGenius, Inc.", diff --git a/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx b/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx index d16da63b93d..956c95fa754 100644 --- a/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx +++ b/web/app/components/workflow-app/components/__tests__/workflow-main.spec.tsx @@ -2,6 +2,7 @@ import type { ReactNode } from 'react' import type { WorkflowProps } from '@/app/components/workflow' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { ChatVarType } from '@/app/components/workflow/panel/chat-variable-panel/type' +import { BlockEnum } from '@/app/components/workflow/types' import WorkflowMain from '../workflow-main' const mockSetFeatures = vi.fn() @@ -91,6 +92,12 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: { appDetail: { mode: string } }) => T) => selector({ + appDetail: { mode: 'workflow' }, + }), +})) + vi.mock('reactflow', () => ({ useReactFlow: () => ({ getNodes: () => [], @@ -260,6 +267,18 @@ vi.mock('@/app/components/workflow-app/hooks', () => ({ }), })) +vi.mock('@/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas', () => ({ + useWorkflowDraftGraphForCanvas: () => ({ + getWorkflowDraftGraphForCanvas: (graph?: { nodes?: unknown[], edges?: unknown[], viewport?: unknown }) => ({ + nodes: graph?.nodes?.length + ? graph.nodes + : [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }], + edges: graph?.edges || [], + viewport: graph?.viewport || { x: 0, y: 0, zoom: 1 }, + }), + }), +})) + vi.mock('../workflow-children', () => ({ default: () =>
workflow-children
, })) @@ -460,4 +479,36 @@ describe('WorkflowMain', () => { }) }) }) + + it('restores a local start placeholder for empty collaboration workflow updates', async () => { + collaborationRuntime.isEnabled = true + mockFetchWorkflowDraft.mockResolvedValue({ + features: {}, + conversation_variables: [], + environment_variables: [], + graph: { + nodes: [], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }, + }) + + render( + , + ) + + await collaborationListeners.workflowUpdate?.() + + await waitFor(() => { + expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({ + nodes: [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }) + }) + }) }) diff --git a/web/app/components/workflow-app/components/workflow-main.tsx b/web/app/components/workflow-app/components/workflow-main.tsx index 1d28d78d22e..08fc2e8daf6 100644 --- a/web/app/components/workflow-app/components/workflow-main.tsx +++ b/web/app/components/workflow-app/components/workflow-main.tsx @@ -11,9 +11,11 @@ import { useRef, } from 'react' import { useReactFlow } from 'reactflow' +import { useStore as useAppStore } from '@/app/components/app/store' import { useFeaturesStore } from '@/app/components/base/features/hooks' import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' import { WorkflowWithInnerContext } from '@/app/components/workflow' +import { useWorkflowDraftGraphForCanvas } from '@/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { useCollaboration } from '@/app/components/workflow/collaboration/hooks/use-collaboration' import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions' @@ -44,8 +46,10 @@ const WorkflowMain = ({ const featuresStore = useFeaturesStore() const workflowStore = useWorkflowStore() const appId = useStore(s => s.appId) + const appDetail = useAppStore(s => s.appDetail) const containerRef = useRef(null) const reactFlow = useReactFlow() + const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail?.mode) const reactFlowStore = useMemo(() => ({ getState: () => ({ @@ -175,13 +179,8 @@ const WorkflowMain = ({ handleWorkflowDataUpdate(response) // Update workflow canvas (nodes, edges, viewport) - if (response.graph) { - handleUpdateWorkflowCanvas({ - nodes: response.graph.nodes || [], - edges: response.graph.edges || [], - viewport: response.graph.viewport || { x: 0, y: 0, zoom: 1 }, - }) - } + if (response.graph) + handleUpdateWorkflowCanvas(getWorkflowDraftGraphForCanvas(response.graph)) } catch (error) { console.error('Failed to fetch updated workflow:', error) @@ -189,7 +188,7 @@ const WorkflowMain = ({ }) return unsubscribe - }, [appId, handleWorkflowDataUpdate, handleUpdateWorkflowCanvas, isCollaborationEnabled]) + }, [appId, getWorkflowDraftGraphForCanvas, handleWorkflowDataUpdate, handleUpdateWorkflowCanvas, isCollaborationEnabled]) // Listen for sync requests from other users (only processed by leader) useEffect(() => { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-auto-onboarding.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-auto-onboarding.spec.ts index f8119026186..0d7d2b14994 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-auto-onboarding.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-auto-onboarding.spec.ts @@ -32,6 +32,7 @@ describe('useAutoOnboarding', () => { showOnboarding: false, hasShownOnboarding: false, notInitialWorkflow: false, + isWorkflowDataLoaded: true, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, @@ -56,11 +57,36 @@ describe('useAutoOnboarding', () => { expect(mockSetShouldAutoOpenStartNodeSelector).toHaveBeenCalledWith(true) }) + it('should skip auto onboarding before workflow data is loaded', () => { + mockWorkflowStore.getState.mockReturnValue({ + showOnboarding: false, + hasShownOnboarding: false, + notInitialWorkflow: false, + isWorkflowDataLoaded: false, + setShowOnboarding: mockSetShowOnboarding, + setHasShownOnboarding: mockSetHasShownOnboarding, + setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, + hasSelectedStartNode: false, + setHasSelectedStartNode: mockSetHasSelectedStartNode, + }) + + renderHook(() => useAutoOnboarding()) + + act(() => { + vi.advanceTimersByTime(500) + }) + + expect(mockSetShowOnboarding).not.toHaveBeenCalled() + expect(mockSetHasShownOnboarding).not.toHaveBeenCalled() + expect(mockSetShouldAutoOpenStartNodeSelector).not.toHaveBeenCalled() + }) + it('should skip auto onboarding when it is already visible or the workflow is not initial', () => { mockWorkflowStore.getState.mockReturnValue({ showOnboarding: true, hasShownOnboarding: false, notInitialWorkflow: true, + isWorkflowDataLoaded: true, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, @@ -84,6 +110,7 @@ describe('useAutoOnboarding', () => { showOnboarding: false, hasShownOnboarding: true, notInitialWorkflow: false, + isWorkflowDataLoaded: true, setShowOnboarding: mockSetShowOnboarding, setHasShownOnboarding: mockSetHasShownOnboarding, setShouldAutoOpenStartNodeSelector: mockSetShouldAutoOpenStartNodeSelector, diff --git a/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts index c92e438cb3c..a53f0cac380 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-available-nodes-meta-data.spec.ts @@ -37,6 +37,7 @@ describe('useAvailableNodesMetaData', () => { expect(result.current.nodesMap?.[BlockEnum.Start]?.metaData.isUndeletable).toBe(false) expect(result.current.nodesMap?.[BlockEnum.End]).toBeDefined() + expect(result.current.nodesMap?.[BlockEnum.StartPlaceholder]).toBeDefined() expect(result.current.nodesMap?.[BlockEnum.TriggerWebhook]).toBeDefined() expect(result.current.nodesMap?.[BlockEnum.TriggerSchedule]).toBeDefined() expect(result.current.nodesMap?.[BlockEnum.TriggerPlugin]).toBeDefined() diff --git a/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts index d9e9cbdb8db..4b54a4e0da7 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-nodes-sync-draft.spec.ts @@ -2,6 +2,7 @@ import { act } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' import { renderHookWithSystemFeatures } from '@/__tests__/utils/mock-system-features' +import { BlockEnum } from '@/app/components/workflow/types' import { useNodesSyncDraft } from '../use-nodes-sync-draft' const mockGetNodes = vi.fn() @@ -134,7 +135,7 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => }, } mockGetNodesReadOnly.mockReturnValue(false) - mockGetNodes.mockReturnValue([{ id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start' } }]) + mockGetNodes.mockReturnValue([{ id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start } }]) mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new', updated_at: 1 }) mockCollaborationIsConnected.mockReturnValue(false) mockCollaborationGetIsLeader.mockReturnValue(true) @@ -199,13 +200,15 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => ...reactFlowState, edges: [ { id: 'edge-1', source: 'n1', target: 'n2', data: { _isTemp: false, _private: 'drop', stable: 'keep' } }, + { id: 'placeholder-edge', source: 'start-placeholder', target: 'n1', data: { stable: 'drop' } }, { id: 'temp-edge', source: 'n2', target: 'n3', data: { _isTemp: true } }, ], transform: [10, 20, 1.5], } mockGetNodes.mockReturnValue([ - { id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start', _tempField: 'drop', label: 'Start' } }, - { id: 'temp-node', position: { x: 1, y: 1 }, data: { type: 'answer', _isTempNode: true } }, + { id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start, _tempField: 'drop', label: 'Start' } }, + { id: 'start-placeholder', position: { x: 1, y: 1 }, data: { type: BlockEnum.StartPlaceholder } }, + { id: 'temp-node', position: { x: 2, y: 2 }, data: { type: BlockEnum.Answer, _isTempNode: true } }, ]) workflowStoreState = { ...workflowStoreState, @@ -241,7 +244,7 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => url: '/apps/app-1/workflows/draft', params: { graph: { - nodes: [{ id: 'n1', position: { x: 0, y: 0 }, data: { type: 'start', label: 'Start' } }], + nodes: [{ id: 'n1', position: { x: 0, y: 0 }, data: { type: BlockEnum.Start, label: 'Start' } }], edges: [{ id: 'edge-1', source: 'n1', target: 'n2', data: { stable: 'keep' } }], viewport: { x: 10, y: 20, zoom: 1.5 }, }, @@ -292,6 +295,32 @@ describe('useNodesSyncDraft — handleRefreshWorkflowDraft(true) on 409', () => })) }) + it('should not post the local start placeholder when the page closes', () => { + reactFlowState = { + ...reactFlowState, + edges: [ + { id: 'placeholder-edge', source: 'start-placeholder', target: 'n1', data: {} }, + ], + } + mockGetNodes.mockReturnValue([ + { id: 'start-placeholder', position: { x: 0, y: 0 }, data: { type: BlockEnum.StartPlaceholder } }, + { id: 'n1', position: { x: 1, y: 1 }, data: { type: BlockEnum.Start } }, + ]) + + const { result } = renderUseNodesSyncDraft() + + act(() => { + result.current.syncWorkflowDraftWhenPageClose() + }) + + expect(mockPostWithKeepalive).toHaveBeenCalledWith('/api/apps/app-1/workflows/draft', expect.objectContaining({ + graph: expect.objectContaining({ + nodes: [{ id: 'n1', position: { x: 1, y: 1 }, data: { type: BlockEnum.Start } }], + edges: [], + }), + })) + }) + it('should emit sync request instead of syncing when current user is collaboration follower', async () => { isCollaborationEnabled = true mockCollaborationIsConnected.mockReturnValue(true) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts new file mode 100644 index 00000000000..71ac93add53 --- /dev/null +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-draft-graph-for-canvas.spec.ts @@ -0,0 +1,103 @@ +import { renderHook } from '@testing-library/react' +import { BlockEnum } from '@/app/components/workflow/types' +import { AppModeEnum } from '@/types/app' +import { useWorkflowDraftGraphForCanvas } from '../use-workflow-draft-graph-for-canvas' + +let generateNewNodeCalls: Array> = [] + +vi.mock('@/app/components/workflow/utils', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + generateNewNode: (args: { data: Record, position: Record }) => { + generateNewNodeCalls.push(args) + return { + newNode: { + id: `generated-${generateNewNodeCalls.length}`, + type: 'custom', + data: args.data, + position: args.position, + }, + } + }, + } +}) + +describe('useWorkflowDraftGraphForCanvas', () => { + beforeEach(() => { + vi.clearAllMocks() + generateNewNodeCalls = [] + }) + + it('should restore a local start placeholder for workflow graphs without an entry node', () => { + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + + const graph = result.current.getWorkflowDraftGraphForCanvas({ + nodes: [], + edges: [], + }) + + expect(graph).toMatchObject({ + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }) + expect(graph.nodes).toHaveLength(1) + expect(graph.nodes[0]).toMatchObject({ + data: { + type: BlockEnum.StartPlaceholder, + title: 'workflow.blocks.start-placeholder', + desc: '', + selected: true, + }, + }) + }) + + it.each([ + BlockEnum.Start, + BlockEnum.TriggerSchedule, + BlockEnum.TriggerWebhook, + BlockEnum.TriggerPlugin, + BlockEnum.StartPlaceholder, + ])('should preserve existing %s entry nodes', (type) => { + const node = { id: 'entry', data: { type } } + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + + const graph = result.current.getWorkflowDraftGraphForCanvas({ + nodes: [node] as never, + edges: [], + }) + + expect(graph.nodes).toEqual([node]) + expect(generateNewNodeCalls).toHaveLength(0) + }) + + it('should not restore a start placeholder for non-workflow app modes', () => { + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.ADVANCED_CHAT)) + + const graph = result.current.getWorkflowDraftGraphForCanvas({ + nodes: [], + edges: [], + }) + + expect(graph.nodes).toEqual([]) + expect(generateNewNodeCalls).toHaveLength(0) + }) + + it('should reuse the provided local start placeholder template when available', () => { + const localStartPlaceholder = { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } } + const draftNode = { id: 'llm', data: { type: BlockEnum.LLM } } + const { result } = renderHook(() => useWorkflowDraftGraphForCanvas(AppModeEnum.WORKFLOW)) + + const graph = result.current.getWorkflowDraftGraphForCanvas({ + nodes: [draftNode] as never, + edges: [], + viewport: { x: 1, y: 2, zoom: 0.5 }, + }, { + localStartPlaceholderNodes: [localStartPlaceholder] as never, + }) + + expect(graph.nodes).toEqual([localStartPlaceholder, draftNode]) + expect(graph.viewport).toEqual({ x: 1, y: 2, zoom: 0.5 }) + expect(generateNewNodeCalls).toHaveLength(0) + }) +}) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts index 4827c5508c7..47ac39c60e2 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-init.spec.ts @@ -14,6 +14,7 @@ const mockWorkflowStoreSetState = vi.fn() const mockWorkflowStoreGetState = vi.fn() const mockFetchNodesDefaultConfigs = vi.fn() const mockFetchPublishedWorkflow = vi.fn() +const mockSyncWorkflowDraft = vi.fn() let appStoreState: { appDetail: { @@ -43,7 +44,12 @@ vi.mock('@/app/components/app/store', () => ({ })) vi.mock('../use-workflow-template', () => ({ - useWorkflowTemplate: () => ({ nodes: [], edges: [] }), + useWorkflowTemplate: () => ({ + nodes: appStoreState.appDetail.mode === 'workflow' + ? [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }] + : [{ id: 'start', data: { type: BlockEnum.Start } }], + edges: [], + }), })) vi.mock('@/service/use-workflow', () => ({ @@ -55,7 +61,6 @@ vi.mock('@/service/use-workflow', () => ({ })) const mockFetchWorkflowDraft = vi.fn() -const mockSyncWorkflowDraft = vi.fn() vi.mock('@/service/workflow', () => ({ fetchWorkflowDraft: (...args: unknown[]) => mockFetchWorkflowDraft(...args), @@ -71,13 +76,17 @@ const notExistError = () => ({ const draftResponse = { id: 'draft-id', - graph: { nodes: [], edges: [] }, + graph: { + nodes: [{ id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }], + edges: [], + }, hash: 'server-hash', created_at: 0, created_by: { id: '', name: '', email: '' }, updated_at: 1, updated_by: { id: '', name: '', email: '' }, tool_published: false, + features: { retriever_resource: { enabled: true } }, environment_variables: [], conversation_variables: [], version: '1', @@ -85,7 +94,7 @@ const draftResponse = { marked_comment: '', } -describe('useWorkflowInit — hash fix (draft_workflow_not_exist)', () => { +describe('useWorkflowInit', () => { beforeEach(() => { vi.clearAllMocks() appStoreState = { @@ -103,32 +112,115 @@ describe('useWorkflowInit — hash fix (draft_workflow_not_exist)', () => { mockFetchPublishedWorkflow.mockResolvedValue({ created_at: 0, graph: { nodes: [], edges: [] } }) mockFetchWorkflowDraft .mockRejectedValueOnce(notExistError()) - .mockResolvedValueOnce(draftResponse) - mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 }) + mockSyncWorkflowDraft.mockReset() }) - it('should call setSyncWorkflowDraftHash with hash returned by syncWorkflowDraft', async () => { - renderHook(() => useWorkflowInit()) - await waitFor(() => expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash')) - }) - - it('should store hash BEFORE making the recursive fetchWorkflowDraft call', async () => { - const order: string[] = [] - mockSetSyncWorkflowDraftHash.mockImplementation((h: string) => order.push(`hash:${h}`)) + it('should create an empty backend draft and restore a local start placeholder when the workflow draft does not exist', async () => { mockFetchWorkflowDraft .mockReset() .mockRejectedValueOnce(notExistError()) - .mockImplementationOnce(async () => { - order.push('fetch:2') - return draftResponse + .mockResolvedValueOnce({ + ...draftResponse, + graph: { nodes: [], edges: [] }, + hash: 'new-workflow-hash', }) mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 }) + const { result } = renderHook(() => useWorkflowInit()) + + await waitFor(() => { + expect(result.current.data?.graph.nodes).toEqual([ + { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, + ]) + }) + + expect(mockWorkflowStoreSetState).toHaveBeenCalledWith(expect.objectContaining({ + showOnboarding: false, + shouldAutoOpenStartNodeSelector: false, + hasSelectedStartNode: false, + hasShownOnboarding: true, + })) + expect(mockSyncWorkflowDraft).toHaveBeenCalledWith(expect.objectContaining({ + params: expect.objectContaining({ + graph: { + nodes: [], + edges: [], + }, + }), + })) + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash') + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-workflow-hash') + }) + + it('should keep creating the first backend draft for advanced chat apps', async () => { + appStoreState = { + appDetail: { id: 'app-1', name: 'Test', mode: 'advanced-chat' }, + } + mockFetchWorkflowDraft + .mockReset() + .mockRejectedValueOnce(notExistError()) + .mockResolvedValueOnce(draftResponse) + mockSyncWorkflowDraft.mockResolvedValue({ hash: 'new-hash', updated_at: 1 }) + renderHook(() => useWorkflowInit()) - await waitFor(() => expect(order).toContain('fetch:2')) - expect(order).toContain('hash:new-hash') - expect(order.indexOf('hash:new-hash')).toBeLessThan(order.indexOf('fetch:2')) + await waitFor(() => expect(mockSyncWorkflowDraft).toHaveBeenCalledWith(expect.objectContaining({ + params: expect.objectContaining({ + graph: { + nodes: [{ id: 'start', data: { type: BlockEnum.Start } }], + edges: [], + }, + }), + }))) + expect(mockWorkflowStoreSetState).toHaveBeenCalledWith(expect.objectContaining({ + showOnboarding: false, + shouldAutoOpenStartNodeSelector: false, + hasShownOnboarding: false, + })) + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('new-hash') + }) + + it('should restore a local start placeholder when an existing workflow draft has an empty graph', async () => { + mockFetchWorkflowDraft.mockReset().mockResolvedValue({ + ...draftResponse, + graph: { nodes: [], edges: [] }, + hash: 'empty-draft-hash', + }) + + const { result } = renderHook(() => useWorkflowInit()) + + await waitFor(() => { + expect(result.current.data?.graph.nodes).toEqual([ + { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, + ]) + }) + + expect(mockSyncWorkflowDraft).not.toHaveBeenCalled() + expect(mockSetSyncWorkflowDraftHash).toHaveBeenCalledWith('empty-draft-hash') + }) + + it('should preserve existing draft nodes when restoring the local start placeholder', async () => { + const existingNode = { id: 'llm', data: { type: BlockEnum.LLM } } + const existingEdge = { source: 'llm', target: 'answer' } + mockFetchWorkflowDraft.mockReset().mockResolvedValue({ + ...draftResponse, + graph: { + nodes: [existingNode], + edges: [existingEdge], + }, + }) + + const { result } = renderHook(() => useWorkflowInit()) + + await waitFor(() => { + expect(result.current.data?.graph.nodes).toEqual([ + { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, + existingNode, + ]) + }) + + expect(result.current.data?.graph.edges).toEqual([existingEdge]) + expect(mockSyncWorkflowDraft).not.toHaveBeenCalled() }) it('should hydrate draft state, preload defaults, and derive published workflow metadata on success', async () => { diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts index 209f9d9c0e5..c77e52bf4d5 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-refresh-draft.spec.ts @@ -1,5 +1,7 @@ import { act, renderHook, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { BlockEnum } from '@/app/components/workflow/types' +import { AppModeEnum } from '@/types/app' import { useWorkflowRefreshDraft } from '../use-workflow-refresh-draft' @@ -11,6 +13,11 @@ const mockSetEnvSecrets = vi.fn() const mockSetConversationVariables = vi.fn() const mockSetIsWorkflowDataLoaded = vi.fn() const mockCancel = vi.fn() +let appStoreState: { + appDetail: { + mode: string + } +} let workflowStoreState: { appId: string @@ -30,6 +37,11 @@ vi.mock('@/app/components/workflow/store', () => ({ }), })) +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: typeof appStoreState) => T): T => + selector(appStoreState), +})) + vi.mock('@/app/components/workflow/hooks', () => ({ useWorkflowUpdate: () => ({ handleUpdateWorkflowCanvas: mockHandleUpdateWorkflowCanvas }), })) @@ -60,6 +72,9 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => { setConversationVariables: mockSetConversationVariables, setIsWorkflowDataLoaded: mockSetIsWorkflowDataLoaded, } + appStoreState = { + appDetail: { mode: AppModeEnum.ADVANCED_CHAT }, + } mockFetchWorkflowDraft.mockResolvedValue(draftResponse) }) @@ -141,6 +156,70 @@ describe('useWorkflowRefreshDraft — notUpdateCanvas parameter', () => { }) }) + it('should restore a local start placeholder for workflow drafts without an entry node', async () => { + appStoreState = { + appDetail: { mode: AppModeEnum.WORKFLOW }, + } + mockFetchWorkflowDraft.mockResolvedValue({ + hash: 'server-hash', + graph: { + nodes: [], + edges: [], + }, + environment_variables: [], + conversation_variables: [], + }) + + const { result } = renderHook(() => useWorkflowRefreshDraft()) + + act(() => { + result.current.handleRefreshWorkflowDraft() + }) + + await waitFor(() => { + expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({ + nodes: [ + expect.objectContaining({ + data: expect.objectContaining({ + type: BlockEnum.StartPlaceholder, + title: 'workflow.blocks.start-placeholder', + desc: '', + selected: true, + }), + }), + ], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }) + }) + }) + + it('should not restore a local start placeholder for non-workflow app modes', async () => { + mockFetchWorkflowDraft.mockResolvedValue({ + hash: 'server-hash', + graph: { + nodes: [], + edges: [], + }, + environment_variables: [], + conversation_variables: [], + }) + + const { result } = renderHook(() => useWorkflowRefreshDraft()) + + act(() => { + result.current.handleRefreshWorkflowDraft() + }) + + await waitFor(() => { + expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({ + nodes: [], + edges: [], + viewport: { x: 0, y: 0, zoom: 1 }, + }) + }) + }) + it('should restore loaded state when refresh fails after workflow data was already loaded', async () => { mockFetchWorkflowDraft.mockRejectedValue(new Error('refresh failed')) diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx b/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx index da9c207e82f..34bde7be4c9 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-start-run.spec.tsx @@ -113,6 +113,23 @@ describe('useWorkflowStartRun', () => { expect(mockSetShowInputsPanel).toHaveBeenCalledWith(false) }) + it('should not sync or run when only the start placeholder exists', async () => { + mockGetNodes.mockReturnValue([ + { id: 'start-placeholder', data: { type: BlockEnum.StartPlaceholder } }, + ]) + + const { result } = renderHook(() => useWorkflowStartRun()) + + await act(async () => { + await result.current.handleWorkflowStartRunInWorkflow() + }) + + expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() + expect(mockHandleRun).not.toHaveBeenCalled() + expect(mockSetShowDebugAndPreviewPanel).not.toHaveBeenCalled() + expect(mockSetShowInputsPanel).not.toHaveBeenCalled() + }) + it('should open the input panel instead of running immediately when start inputs are required', async () => { mockGetNodes.mockReturnValue([ { id: 'inset-s-1', data: { type: BlockEnum.Start, variables: [{ name: 'query' }] } }, diff --git a/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts b/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts index 3e9934df745..ecf285db60a 100644 --- a/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts +++ b/web/app/components/workflow-app/hooks/__tests__/use-workflow-template.spec.ts @@ -1,13 +1,24 @@ import { renderHook } from '@testing-library/react' +import { AppModeEnum } from '@/types/app' import { useWorkflowTemplate } from '../use-workflow-template' const mockUseIsChatMode = vi.fn() let generateNewNodeCalls: Array> = [] +let appStoreState: { + appDetail: { + mode: string + } +} vi.mock('@/app/components/workflow-app/hooks/use-is-chat-mode', () => ({ useIsChatMode: () => mockUseIsChatMode(), })) +vi.mock('@/app/components/app/store', () => ({ + useStore: (selector: (state: typeof appStoreState) => T): T => + selector(appStoreState), +})) + vi.mock('@/app/components/workflow/utils', async (importOriginal) => { const actual = await importOriginal() return { @@ -29,9 +40,12 @@ describe('useWorkflowTemplate', () => { beforeEach(() => { vi.clearAllMocks() generateNewNodeCalls = [] + appStoreState = { + appDetail: { mode: AppModeEnum.WORKFLOW }, + } }) - it('should return only the start node template in workflow mode', () => { + it('should return only the start placeholder template in workflow mode', () => { mockUseIsChatMode.mockReturnValue(false) const { result } = renderHook(() => useWorkflowTemplate()) @@ -39,9 +53,35 @@ describe('useWorkflowTemplate', () => { expect(result.current.nodes).toHaveLength(1) expect(result.current.edges).toEqual([]) expect(generateNewNodeCalls).toHaveLength(1) + expect(generateNewNodeCalls[0]!.data).toMatchObject({ + type: 'start-placeholder', + title: 'workflow.blocks.start-placeholder', + selected: true, + desc: '', + }) + }) + + it('should return the start node template for non-workflow app modes', () => { + appStoreState = { + appDetail: { mode: AppModeEnum.COMPLETION }, + } + mockUseIsChatMode.mockReturnValue(false) + + const { result } = renderHook(() => useWorkflowTemplate()) + + expect(result.current.nodes).toHaveLength(1) + expect(result.current.edges).toEqual([]) + expect(generateNewNodeCalls).toHaveLength(1) + expect(generateNewNodeCalls[0]!.data).toMatchObject({ + type: 'start', + title: 'workflow.blocks.start', + }) }) it('should build start, llm, and answer templates with linked edges in chat mode', () => { + appStoreState = { + appDetail: { mode: AppModeEnum.ADVANCED_CHAT }, + } mockUseIsChatMode.mockReturnValue(true) const { result } = renderHook(() => useWorkflowTemplate()) diff --git a/web/app/components/workflow-app/hooks/use-auto-onboarding.ts b/web/app/components/workflow-app/hooks/use-auto-onboarding.ts index e4f5774adf1..4b43b72894c 100644 --- a/web/app/components/workflow-app/hooks/use-auto-onboarding.ts +++ b/web/app/components/workflow-app/hooks/use-auto-onboarding.ts @@ -12,11 +12,15 @@ export const useAutoOnboarding = () => { showOnboarding, hasShownOnboarding, notInitialWorkflow, + isWorkflowDataLoaded, setShowOnboarding, setHasShownOnboarding, setShouldAutoOpenStartNodeSelector, } = workflowStore.getState() + if (!isWorkflowDataLoaded) + return + // Skip if already showing onboarding or it's the initial workflow creation if (showOnboarding || notInitialWorkflow) return diff --git a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts index 58676dd9b4f..bc468e73e35 100644 --- a/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts +++ b/web/app/components/workflow-app/hooks/use-available-nodes-meta-data.ts @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import { WORKFLOW_COMMON_NODES } from '@/app/components/workflow/constants/node' import AnswerDefault from '@/app/components/workflow/nodes/answer/default' import EndDefault from '@/app/components/workflow/nodes/end/default' +import StartPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default' import StartDefault from '@/app/components/workflow/nodes/start/default' import TriggerPluginDefault from '@/app/components/workflow/nodes/trigger-plugin/default' import TriggerScheduleDefault from '@/app/components/workflow/nodes/trigger-schedule/default' @@ -34,6 +35,7 @@ export const useAvailableNodesMetaData = () => { isChatMode ? [AnswerDefault] : [ + StartPlaceholderDefault, EndDefault, TriggerWebhookDefault, TriggerScheduleDefault, diff --git a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts index d9eafcfc7da..f7bd2f4c2fb 100644 --- a/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts +++ b/web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts @@ -9,6 +9,7 @@ import { collaborationManager } from '@/app/components/workflow/collaboration/co import { useSerialAsyncCallback } from '@/app/components/workflow/hooks/use-serial-async-callback' import { useNodesReadOnly } from '@/app/components/workflow/hooks/use-workflow' import { useWorkflowStore } from '@/app/components/workflow/store' +import { BlockEnum } from '@/app/components/workflow/types' import { API_PREFIX } from '@/config' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { postWithKeepalive } from '@/service/fetch' @@ -32,7 +33,13 @@ export const useNodesSyncDraft = () => { edges, transform, } = store.getState() - const nodes = getNodes().filter(node => !node.data?._isTempNode) + const allNodes = getNodes() + const nodes = allNodes.filter(node => !node.data?._isTempNode && node.data?.type !== BlockEnum.StartPlaceholder) + const skippedNodeIds = new Set( + allNodes + .filter(node => node.data?._isTempNode || node.data?.type === BlockEnum.StartPlaceholder) + .map(node => node.id), + ) const [x, y, zoom] = transform const { appId, @@ -54,7 +61,7 @@ export const useNodesSyncDraft = () => { }) }) }) - const producedEdges = produce(edges.filter(edge => !edge.data?._isTemp), (draft) => { + const producedEdges = produce(edges.filter(edge => !edge.data?._isTemp && !skippedNodeIds.has(edge.source) && !skippedNodeIds.has(edge.target)), (draft) => { draft.forEach((edge) => { Object.keys(edge.data).forEach((key) => { if (key.startsWith('_')) diff --git a/web/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas.ts b/web/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas.ts new file mode 100644 index 00000000000..aa805e37b01 --- /dev/null +++ b/web/app/components/workflow-app/hooks/use-workflow-draft-graph-for-canvas.ts @@ -0,0 +1,76 @@ +import type { + Node, + WorkflowDataUpdater, +} from '@/app/components/workflow/types' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { START_INITIAL_POSITION } from '@/app/components/workflow/constants' +import startPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default' +import { BlockEnum } from '@/app/components/workflow/types' +import { generateNewNode } from '@/app/components/workflow/utils' +import { AppModeEnum } from '@/types/app' + +type HydrateWorkflowDraftGraphOptions = { + localStartPlaceholderNodes?: Node[] +} + +const hasWorkflowEntryNode = (nodes: Node[] = []): boolean => { + return nodes.some(node => ( + node?.data?.type === BlockEnum.Start + || node?.data?.type === BlockEnum.TriggerSchedule + || node?.data?.type === BlockEnum.TriggerWebhook + || node?.data?.type === BlockEnum.TriggerPlugin + )) +} + +const hasStartPlaceholderNode = (nodes: Node[] = []): boolean => { + return nodes.some(node => node?.data?.type === BlockEnum.StartPlaceholder) +} + +export const useWorkflowDraftGraphForCanvas = (appMode?: AppModeEnum | string) => { + const { t } = useTranslation() + + const getNodesWithLocalStartPlaceholder = useCallback(( + nodes: Node[] = [], + localStartPlaceholderNodes?: Node[], + ) => { + if (appMode !== AppModeEnum.WORKFLOW || hasWorkflowEntryNode(nodes) || hasStartPlaceholderNode(nodes)) + return nodes + + if (localStartPlaceholderNodes?.length) + return [...localStartPlaceholderNodes, ...nodes] + + const { newNode: startPlaceholderNode } = generateNewNode({ + data: { + ...startPlaceholderDefault.defaultValue, + selected: true, + type: startPlaceholderDefault.metaData.type, + title: t(`blocks.${startPlaceholderDefault.metaData.type}`, { ns: 'workflow' }), + desc: '', + }, + position: START_INITIAL_POSITION, + }) + + return [startPlaceholderNode, ...nodes] + }, [appMode, t]) + + const getWorkflowDraftGraphForCanvas = useCallback(( + graph?: Partial, + options?: HydrateWorkflowDraftGraphOptions, + ): WorkflowDataUpdater => { + const nodes = getNodesWithLocalStartPlaceholder( + graph?.nodes || [], + options?.localStartPlaceholderNodes, + ) + + return { + nodes, + edges: graph?.edges || [], + viewport: graph?.viewport || { x: 0, y: 0, zoom: 1 }, + } + }, [getNodesWithLocalStartPlaceholder]) + + return { + getWorkflowDraftGraphForCanvas, + } +} diff --git a/web/app/components/workflow-app/hooks/use-workflow-init.ts b/web/app/components/workflow-app/hooks/use-workflow-init.ts index 548407257e2..7a4ccd63101 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-init.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-init.ts @@ -20,6 +20,7 @@ import { syncWorkflowDraft, } from '@/service/workflow' import { AppModeEnum } from '@/types/app' +import { useWorkflowDraftGraphForCanvas } from './use-workflow-draft-graph-for-canvas' import { useWorkflowTemplate } from './use-workflow-template' const hasConnectedUserInput = (nodes: Node[] = [], edges: Edge[] = []): boolean => { @@ -32,6 +33,7 @@ const hasConnectedUserInput = (nodes: Node[] = [], edges: Edge[] = []): boolean return edges.some(edge => startNodeIds.includes(edge.source)) } + export const useWorkflowInit = () => { const workflowStore = useWorkflowStore() const { @@ -39,6 +41,7 @@ export const useWorkflowInit = () => { edges: edgesTemplate, } = useWorkflowTemplate() const appDetail = useAppStore(state => state.appDetail)! + const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail.mode) const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash) const [data, setData] = useState() const [isLoading, setIsLoading] = useState(true) @@ -58,17 +61,24 @@ export const useWorkflowInit = () => { const handleGetInitialWorkflowData = useCallback(async () => { try { const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`) - setData(res) + const initialData = { + ...res, + graph: getWorkflowDraftGraphForCanvas(res.graph, { + localStartPlaceholderNodes: nodesTemplate, + }), + } + + setData(initialData) workflowStore.setState({ - envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { + envSecrets: (initialData.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { acc[env.id] = env.value return acc }, {} as Record), - environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [], - conversationVariables: res.conversation_variables || [], + environmentVariables: initialData.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [], + conversationVariables: initialData.conversation_variables || [], isWorkflowDataLoaded: true, }) - setSyncWorkflowDraftHash(res.hash) + setSyncWorkflowDraftHash(initialData.hash) setIsLoading(false) } catch (error: any) { @@ -78,19 +88,18 @@ export const useWorkflowInit = () => { const isAdvancedChat = appDetail.mode === AppModeEnum.ADVANCED_CHAT workflowStore.setState({ notInitialWorkflow: true, - showOnboarding: !isAdvancedChat, - shouldAutoOpenStartNodeSelector: !isAdvancedChat, - hasShownOnboarding: false, + showOnboarding: false, + shouldAutoOpenStartNodeSelector: false, + hasSelectedStartNode: false, + hasShownOnboarding: !isAdvancedChat, }) - const nodesData = isAdvancedChat ? nodesTemplate : [] - const edgesData = isAdvancedChat ? edgesTemplate : [] syncWorkflowDraft({ url: `/apps/${appDetail.id}/workflows/draft`, params: { graph: { - nodes: nodesData, - edges: edgesData, + nodes: isAdvancedChat ? nodesTemplate : [], + edges: isAdvancedChat ? edgesTemplate : [], }, features: { retriever_resource: { enabled: true }, @@ -107,7 +116,7 @@ export const useWorkflowInit = () => { }) } } - }, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) + }, [appDetail, getWorkflowDraftGraphForCanvas, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) useEffect(() => { handleGetInitialWorkflowData() diff --git a/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts b/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts index a7283c00781..dec94f33bb9 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-refresh-draft.ts @@ -1,12 +1,15 @@ -import type { WorkflowDataUpdater } from '@/app/components/workflow/types' import { useCallback } from 'react' +import { useStore as useAppStore } from '@/app/components/app/store' import { useWorkflowUpdate } from '@/app/components/workflow/hooks' import { useWorkflowStore } from '@/app/components/workflow/store' import { fetchWorkflowDraft } from '@/service/workflow' +import { useWorkflowDraftGraphForCanvas } from './use-workflow-draft-graph-for-canvas' export const useWorkflowRefreshDraft = () => { + const appDetail = useAppStore(s => s.appDetail) const workflowStore = useWorkflowStore() const { handleUpdateWorkflowCanvas } = useWorkflowUpdate() + const { getWorkflowDraftGraphForCanvas } = useWorkflowDraftGraphForCanvas(appDetail?.mode) const handleRefreshWorkflowDraft = useCallback((notUpdateCanvas?: boolean) => { const { @@ -31,14 +34,8 @@ export const useWorkflowRefreshDraft = () => { fetchWorkflowDraft(`/apps/${appId}/workflows/draft`) .then((response) => { // Ensure we have a valid workflow structure with viewport - if (!notUpdateCanvas) { - const workflowData: WorkflowDataUpdater = { - nodes: response.graph?.nodes || [], - edges: response.graph?.edges || [], - viewport: response.graph?.viewport || { x: 0, y: 0, zoom: 1 }, - } - handleUpdateWorkflowCanvas(workflowData) - } + if (!notUpdateCanvas) + handleUpdateWorkflowCanvas(getWorkflowDraftGraphForCanvas(response.graph)) setSyncWorkflowDraftHash(response.hash) setEnvSecrets((response.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { acc[env.id] = env.value @@ -55,7 +52,7 @@ export const useWorkflowRefreshDraft = () => { .finally(() => { setIsSyncingWorkflowDraft(false) }) - }, [handleUpdateWorkflowCanvas, workflowStore]) + }, [getWorkflowDraftGraphForCanvas, handleUpdateWorkflowCanvas, workflowStore]) return { handleRefreshWorkflowDraft, diff --git a/web/app/components/workflow-app/hooks/use-workflow-start-run.tsx b/web/app/components/workflow-app/hooks/use-workflow-start-run.tsx index 9344a8d1ab6..516a653cd18 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-start-run.tsx +++ b/web/app/components/workflow-app/hooks/use-workflow-start-run.tsx @@ -34,6 +34,9 @@ export const useWorkflowStartRun = () => { const { getNodes } = store.getState() const nodes = getNodes() const startNode = nodes.find(node => node.data.type === BlockEnum.Start) + if (!startNode) + return + const startVariables = startNode?.data.variables || [] const fileSettings = featuresStore!.getState().features.file const { diff --git a/web/app/components/workflow-app/hooks/use-workflow-template.ts b/web/app/components/workflow-app/hooks/use-workflow-template.ts index 5a89d3b3e76..d45c588889a 100644 --- a/web/app/components/workflow-app/hooks/use-workflow-template.ts +++ b/web/app/components/workflow-app/hooks/use-workflow-template.ts @@ -1,29 +1,39 @@ import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' import { useTranslation } from 'react-i18next' +import { useStore as useAppStore } from '@/app/components/app/store' import { NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION, } from '@/app/components/workflow/constants' import answerDefault from '@/app/components/workflow/nodes/answer/default' import llmDefault from '@/app/components/workflow/nodes/llm/default' +import startPlaceholderDefault from '@/app/components/workflow/nodes/start-placeholder/default' import startDefault from '@/app/components/workflow/nodes/start/default' import { generateNewNode } from '@/app/components/workflow/utils' +import { AppModeEnum } from '@/types/app' import { useIsChatMode } from './use-is-chat-mode' export const useWorkflowTemplate = () => { const isChatMode = useIsChatMode() + const appDetail = useAppStore(s => s.appDetail) const { t } = useTranslation() - const { newNode: startNode } = generateNewNode({ - data: { - ...startDefault.defaultValue as StartNodeType, - type: startDefault.metaData.type, - title: t(`blocks.${startDefault.metaData.type}`, { ns: 'workflow' }), - }, - position: START_INITIAL_POSITION, - }) + const createStartNode = () => { + const { newNode: startNode } = generateNewNode({ + data: { + ...startDefault.defaultValue as StartNodeType, + type: startDefault.metaData.type, + title: t(`blocks.${startDefault.metaData.type}`, { ns: 'workflow' }), + }, + position: START_INITIAL_POSITION, + }) + + return startNode + } if (isChatMode) { + const startNode = createStartNode() + const { newNode: llmNode } = generateNewNode({ id: 'llm', data: { @@ -77,10 +87,26 @@ export const useWorkflowTemplate = () => { edges: [startToLlmEdge, llmToAnswerEdge], } } - else { + if (appDetail?.mode === AppModeEnum.WORKFLOW) { + const { newNode: startPlaceholderNode } = generateNewNode({ + data: { + ...startPlaceholderDefault.defaultValue, + selected: true, + type: startPlaceholderDefault.metaData.type, + title: t(`blocks.${startPlaceholderDefault.metaData.type}`, { ns: 'workflow' }), + desc: '', + }, + position: START_INITIAL_POSITION, + }) + return { - nodes: [startNode], + nodes: [startPlaceholderNode], edges: [], } } + + return { + nodes: [createStartNode()], + edges: [], + } } diff --git a/web/app/components/workflow/__tests__/block-icon.spec.tsx b/web/app/components/workflow/__tests__/block-icon.spec.tsx index c3b30a67b60..7bec2b41eff 100644 --- a/web/app/components/workflow/__tests__/block-icon.spec.tsx +++ b/web/app/components/workflow/__tests__/block-icon.spec.tsx @@ -9,7 +9,7 @@ describe('BlockIcon', () => { const iconContainer = container.firstElementChild expect(iconContainer).toHaveClass('w-4', 'h-4', 'bg-util-colors-blue-brand-blue-brand-500', 'extra-class') - expect(iconContainer?.querySelector('svg')).toBeInTheDocument() + expect(iconContainer?.querySelector('.i-custom-vender-workflow-user-input')).toBeInTheDocument() }) it('normalizes protected plugin icon urls for tool-like nodes', () => { diff --git a/web/app/components/workflow/block-icon.tsx b/web/app/components/workflow/block-icon.tsx index 571ea475e25..15292a07049 100644 --- a/web/app/components/workflow/block-icon.tsx +++ b/web/app/components/workflow/block-icon.tsx @@ -45,6 +45,7 @@ const ICON_CONTAINER_CLASSNAME_SIZE_MAP: Record = { const DEFAULT_ICON_MAP: Record> = { [BlockEnum.Start]: Home, + [BlockEnum.StartPlaceholder]: Home, [BlockEnum.LLM]: Llm, [BlockEnum.Code]: Code, [BlockEnum.End]: End, @@ -96,6 +97,7 @@ const normalizeToolIconUrl = (toolIcon: string) => { const ICON_CONTAINER_BG_COLOR_MAP: Record = { [BlockEnum.Start]: 'bg-util-colors-blue-brand-blue-brand-500', + [BlockEnum.StartPlaceholder]: 'bg-util-colors-blue-brand-blue-brand-500', [BlockEnum.LLM]: 'bg-util-colors-indigo-indigo-500', [BlockEnum.Code]: 'bg-util-colors-blue-blue-500', [BlockEnum.End]: 'bg-util-colors-warning-warning-500', @@ -129,12 +131,46 @@ const BlockIcon: FC = ({ className, toolIcon, }) => { + const isStart = type === BlockEnum.Start + const isStartPlaceholder = type === BlockEnum.StartPlaceholder const isToolOrDataSourceOrTriggerPlugin = type === BlockEnum.Tool || type === BlockEnum.DataSource || type === BlockEnum.TriggerPlugin const showDefaultIcon = !isToolOrDataSourceOrTriggerPlugin || !toolIcon const resolvedToolIcon = typeof toolIcon === 'string' ? normalizeToolIconUrl(toolIcon) : toolIcon + if (isStart) { + return ( +
+ +
+ ) + } + + if (isStartPlaceholder) { + return ( +
+ +
+ ) + } + return (
({ useGetLanguage: vi.fn(), @@ -49,7 +50,7 @@ vi.mock('@/utils/var', async (importOriginal) => { const actual = await importOriginal() return { ...actual, - getMarketplaceUrl: () => 'https://marketplace.test/start', + getMarketplaceUrl: (path: string) => `https://marketplace.test${path}`, } }) @@ -186,7 +187,7 @@ describe('AllStartBlocks', () => { const user = userEvent.setup() const onSelect = vi.fn() - render( + const { container } = render( { ) await waitFor(() => { - expect(screen.getByText('workflow.tabs.allTriggers')).toBeInTheDocument() + expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument() }) expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument() + expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument() + expect(screen.getByText('workflow.blocks.mostCommon')).toBeInTheDocument() expect(screen.getByText('Provider One')).toBeInTheDocument() + expect(container.querySelectorAll('.bg-divider-subtle')).toHaveLength(0) await user.click(screen.getByText('workflow.blocks.start')) expect(onSelect).toHaveBeenCalledWith(BlockEnum.Start) @@ -225,7 +229,150 @@ describe('AllStartBlocks', () => { />, ) - expect(await screen.findByRole('link', { name: /plugin\.findMoreInMarketplace/ })).toHaveAttribute('href', 'https://marketplace.test/start') + const footer = await screen.findByRole('link', { name: /plugin\.findMoreInMarketplace/ }) + expect(footer).toHaveAttribute('href', 'https://marketplace.test/plugins/trigger') + expect(footer).toHaveClass('system-sm-medium', 'h-8', 'rounded-b-lg', 'bg-components-panel-bg-blur', 'text-text-accent-light-mode-only', 'shadow-lg') + expect(footer.querySelector('.i-custom-vender-main-nav-marketplace')).not.toBeInTheDocument() + expect(footer.querySelector('svg')).toBeInTheDocument() + }) + + it('should keep the panel marketplace footer icon style', async () => { + enableMarketplaceForRender = true + + render( + , + ) + + const footer = await screen.findByRole('link', { name: /workflow\.nodes\.startPlaceholder\.browseMoreOnMarketplace/ }) + expect(footer).toHaveAttribute('href', 'https://marketplace.test/plugins/trigger') + expect(footer).toHaveClass('flex-col') + expect(footer.querySelector('.w-8 .bg-divider-subtle')).toBeInTheDocument() + expect(footer.querySelector('.i-custom-vender-workflow-marketplace')).toBeInTheDocument() + expect(footer.querySelector('svg')).not.toBeInTheDocument() + }) + + it('should keep the panel divider between user input and installed triggers', async () => { + const { container } = render( + , + ) + + await waitFor(() => { + expect(screen.getByText('Provider One')).toBeInTheDocument() + }) + + expect(container.querySelectorAll('.px-4.py-1 .bg-divider-subtle')).toHaveLength(1) + }) + + it('should render searched marketplace results after built-in and installed trigger options', async () => { + enableMarketplaceForRender = true + mockUseAllTriggerPlugins.mockReturnValue(createTriggerPluginsQueryResult([ + createTriggerProvider({ + label: { en_US: 'Start Provider', zh_Hans: 'Start Provider' }, + }), + ])) + mockUseMarketplacePlugins.mockReturnValue(createMarketplacePluginsMock({ + plugins: [ + createPlugin({ + name: 'start-marketplace', + label: { en_US: 'Start Marketplace', zh_Hans: 'Start Marketplace' }, + }), + ], + })) + + const { container } = render( + , + ) + + await waitFor(() => { + expect(screen.getByText('Start Marketplace')).toBeInTheDocument() + }) + + const text = container.textContent || '' + expect(text.indexOf('workflow.blocks.start')).toBeLessThan(text.indexOf('Start Provider')) + expect(text.indexOf('Start Provider')).toBeLessThan(text.indexOf('Start Marketplace')) + expect(screen.getAllByRole('link', { name: /plugin\.searchInMarketplace/i })).toHaveLength(1) + expect(container.querySelectorAll('.px-4.py-1 .bg-divider-subtle')).toHaveLength(1) + }) + + it('should show the user input conflict state without allowing another start selection', () => { + const onSelect = vi.fn() + enableMarketplaceForRender = true + mockUseNodes.mockReturnValue([ + { + id: 'start', + data: { + type: BlockEnum.Start, + }, + }, + ] as never) + + render( + , + ) + + expect(screen.getByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument() + expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument() + expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument() + expect(screen.getByText('common.operation.added')).toBeInTheDocument() + const footer = screen.getByRole('link', { name: /plugin\.findMoreInMarketplace/ }) + expect(footer).toHaveClass('system-sm-medium', 'h-8', 'rounded-b-lg', 'bg-components-panel-bg-blur', 'text-text-accent-light-mode-only', 'shadow-lg') + expect(footer.querySelector('.i-custom-vender-main-nav-marketplace')).not.toBeInTheDocument() + expect(footer.querySelector('svg')).toBeInTheDocument() + + fireEvent.click(screen.getByText('workflow.blocks.start')) + fireEvent.click(screen.getByText('Provider One')) + + expect(onSelect).not.toHaveBeenCalled() + }) + + it('should keep user input visible but disabled when another trigger already exists', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + + render( + , + ) + + expect(screen.queryByText('workflow.tabs.allTriggers')).not.toBeInTheDocument() + expect(screen.getByText('workflow.blocks.start')).toBeInTheDocument() + expect(screen.getByText('workflow.blocks.mostCommon').closest('.opacity-30')).toBeInTheDocument() + expect(screen.getByText('workflow.blocks.start').closest('.cursor-not-allowed')).toBeInTheDocument() + + await user.hover(screen.getByText('workflow.blocks.start')) + + expect(await screen.findByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument() + + fireEvent.click(screen.getByText('workflow.blocks.start')) + expect(onSelect).not.toHaveBeenCalled() + + await user.click(screen.getByText('workflow.blocks.trigger-schedule')) + expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerSchedule) }) }) @@ -256,11 +403,15 @@ describe('AllStartBlocks', () => { }) }) - expect(screen.getByText('workflow.tabs.noPluginsFound')).toBeInTheDocument() + expect(screen.getByText('workflow.nodes.startPlaceholder.noTriggersFound')).toBeInTheDocument() expect(screen.getByRole('link', { name: 'workflow.tabs.requestToCommunity' })).toHaveAttribute( 'href', 'https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml', ) + expect(screen.getByRole('link', { name: /plugin\.findMoreInMarketplace/ })).toHaveAttribute( + 'href', + 'https://marketplace.test/plugins/trigger', + ) }) }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/featured-triggers.spec.tsx b/web/app/components/workflow/block-selector/__tests__/featured-triggers.spec.tsx index 5955665f5e1..45767d76078 100644 --- a/web/app/components/workflow/block-selector/__tests__/featured-triggers.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/featured-triggers.spec.tsx @@ -193,5 +193,37 @@ describe('FeaturedTriggers', () => { event_label: 'Created', })) }) + + it('should align featured item icons with the trigger list column', () => { + const provider = createTriggerProvider() + + render( + , + ) + + const installedRow = screen.getByText('Provider One').closest('.select-none') + expect(installedRow).toHaveClass('h-8', 'pr-2', 'pl-3') + expect(installedRow?.parentElement?.parentElement?.parentElement).toHaveClass('p-1') + + const uninstalledRow = screen.getByText('Plugin Two').closest('.group') + expect(uninstalledRow).toHaveClass('h-8', 'pr-2', 'pl-3') + expect(uninstalledRow?.parentElement).toHaveClass('mb-1', 'last-of-type:mb-0') + expect(uninstalledRow?.parentElement?.parentElement).toHaveClass('p-1') + }) }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx index 0ae49213098..7ad791fc499 100644 --- a/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/hooks.spec.tsx @@ -7,14 +7,27 @@ describe('block-selector hooks', () => { vi.clearAllMocks() }) - it('falls back to the first valid tab when the preferred start tab is disabled', () => { + it('keeps the start tab enabled when a configured user input start node exists', () => { const { result } = renderHook(() => useTabs({ noStart: false, - hasUserInputNode: true, defaultActiveTab: TabsEnum.Start, })) - expect(result.current.tabs.find(tab => tab.key === TabsEnum.Start)?.disabled).toBe(true) + expect(result.current.tabs.find(tab => tab.key === TabsEnum.Start)?.disabled).toBeFalsy() + expect(result.current.activeTab).toBe(TabsEnum.Start) + }) + + it('disables the start tab when an unconfigured start placeholder exists', () => { + const { result } = renderHook(() => useTabs({ + noStart: false, + hasStartPlaceholderNode: true, + defaultActiveTab: TabsEnum.Start, + })) + + const startTab = result.current.tabs.find(tab => tab.key === TabsEnum.Start) + expect(startTab?.disabled).toBe(true) + expect(startTab?.disabledTip).toBe('workflow.tabs.unconfiguredStartDisabledTip') + expect(startTab?.disabledTipLinkKey).toBe('startNodesDocs') expect(result.current.activeTab).toBe(TabsEnum.Blocks) }) @@ -22,7 +35,6 @@ describe('block-selector hooks', () => { const props: Parameters[0] = { noBlocks: false, noStart: false, - hasUserInputNode: true, forceEnableStartTab: true, } diff --git a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx index d426b43cfdc..52928540e2d 100644 --- a/web/app/components/workflow/block-selector/__tests__/index.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/index.spec.tsx @@ -65,6 +65,7 @@ describe('NodeSelectorWrapper', () => { availableNodesMetaData: { nodes: [ createBlock(BlockEnum.Start, 'Start'), + createBlock(BlockEnum.StartPlaceholder, 'Start Placeholder'), createBlock(BlockEnum.Tool, 'Tool'), createBlock(BlockEnum.Code, 'Code'), createBlock(BlockEnum.DataSource, 'Data Source'), @@ -79,6 +80,7 @@ describe('NodeSelectorWrapper', () => { expect(await screen.findByText('Code')).toBeInTheDocument() expect(screen.queryByText('Start')).not.toBeInTheDocument() + expect(screen.queryByText('Start Placeholder')).not.toBeInTheDocument() expect(screen.queryByText('Tool')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx index 1473022ef94..884e45f6259 100644 --- a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx @@ -7,7 +7,7 @@ import { FlowType } from '@/types/common' import { renderWorkflowComponent } from '../../__tests__/workflow-test-env' import { BlockEnum } from '../../types' import NodeSelector from '../main' -import { BlockClassificationEnum } from '../types' +import { BlockClassificationEnum, TabsEnum } from '../types' vi.mock('reactflow', () => ({ useStoreApi: () => ({ @@ -22,6 +22,22 @@ vi.mock('@/service/use-plugins', () => ({ plugins: [], isLoading: false, }), + useFeaturedTriggersRecommendations: () => ({ + plugins: [], + isLoading: false, + }), +})) + +vi.mock('@/app/components/plugins/marketplace/hooks', () => ({ + useMarketplacePlugins: () => ({ + plugins: [], + queryPluginsWithDebounced: vi.fn(), + }), +})) + +vi.mock('@/service/use-triggers', () => ({ + useAllTriggerPlugins: () => ({ data: [] }), + useInvalidateAllTriggerPlugins: () => vi.fn(), })) vi.mock('@/service/use-tools', () => ({ @@ -45,13 +61,18 @@ const createBlock = (type: BlockEnum, title: string): NodeDefault => ({ checkValid: () => ({ isValid: true }), }) -const renderNodeSelector = (ui: ReactElement) => { +type RenderNodeSelectorOptions = Parameters[1] + +const renderNodeSelector = (ui: ReactElement, options?: RenderNodeSelectorOptions) => { return renderWorkflowComponent(ui, { + ...options, hooksStoreProps: { + ...options?.hooksStoreProps, configsMap: { flowId: 'app-1', flowType: FlowType.appFlow, fileSettings: {} as never, + ...options?.hooksStoreProps?.configsMap, }, }, @@ -230,4 +251,68 @@ describe('NodeSelector', () => { expect(trigger.closest('[aria-haspopup="dialog"]')).toBe(trigger) expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument() }) + + it('disables the start tab with a setup tooltip when an unconfigured start node is on the canvas', async () => { + const user = userEvent.setup() + + renderNodeSelector( + , + { + initialStoreState: { + nodes: [ + { + id: 'start-placeholder', + data: { + type: BlockEnum.StartPlaceholder, + }, + }, + ] as never, + }, + }, + ) + + await user.hover(screen.getByText('workflow.tabs.start')) + + expect(await screen.findByText('workflow.tabs.unconfiguredStartDisabledTip')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'workflow.tabs.startDisabledTipLearnMore' })).toHaveAttribute( + 'href', + 'https://docs.dify.ai/en/use-dify/nodes/trigger/overview', + ) + expect(screen.getByPlaceholderText('workflow.tabs.searchBlock')).toBeInTheDocument() + }) + + it('keeps the start tab enabled when a configured user input start node is on the canvas', () => { + renderNodeSelector( + , + { + initialStoreState: { + nodes: [ + { + id: 'start', + data: { + type: BlockEnum.Start, + }, + }, + ] as never, + }, + }, + ) + + expect(screen.getByText('workflow.tabs.start')).toHaveAttribute('aria-disabled', 'false') + expect(screen.getByText('workflow.nodes.startPlaceholder.userInputConflictTip')).toBeInTheDocument() + }) }) diff --git a/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx b/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx index 6bb50aeca30..d7e513c3602 100644 --- a/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/start-blocks.spec.tsx @@ -1,5 +1,5 @@ import type { CommonNodeType } from '../../types' -import { render, screen } from '@testing-library/react' +import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import { useAvailableNodesMetaData } from '../../../workflow-app/hooks' @@ -76,5 +76,71 @@ describe('StartBlocks', () => { expect(screen.queryByText('workflow.blocks.start')).not.toBeInTheDocument() expect(onContentStateChange).toHaveBeenCalledWith(false) }) + + it('should show most common badge for user input in the start selector content', () => { + render( + , + ) + + expect(screen.getByText('workflow.blocks.mostCommon')).toBeInTheDocument() + expect(screen.queryByText('workflow.blocks.originalStartNode')).not.toBeInTheDocument() + }) + + it('should render built-in start block preview titles and Dify Team author', async () => { + const user = userEvent.setup() + + render( + , + ) + + await user.hover(screen.getByText('workflow.blocks.trigger-webhook')) + + expect(screen.queryByText('workflow.customWebhook')).not.toBeInTheDocument() + await waitFor(() => { + expect(screen.getAllByText('workflow.blocks.trigger-webhook')).toHaveLength(2) + }) + + await user.hover(screen.getByText('workflow.blocks.start')) + + await waitFor(() => { + expect(document.body).toHaveTextContent('tools.author workflow.difyTeam') + }) + }) + + it('should keep disabled user input reachable from the keyboard with the conflict reason', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + + render( + , + ) + + const userInputButton = screen.getByRole('button', { + name: /workflow\.blocks\.start.*workflow\.nodes\.startPlaceholder\.userInputConflictTip/, + }) + expect(userInputButton).toHaveAttribute('aria-disabled', 'true') + expect(userInputButton.querySelector('.i-custom-vender-workflow-user-input')?.closest('.opacity-30')).toBeInTheDocument() + + await user.tab() + expect(userInputButton).toHaveFocus() + await user.keyboard('{Enter}') + + expect(onSelect).not.toHaveBeenCalled() + }) }) }) diff --git a/web/app/components/workflow/block-selector/all-start-blocks.tsx b/web/app/components/workflow/block-selector/all-start-blocks.tsx index 8f32f5e2d71..2e2e58bb106 100644 --- a/web/app/components/workflow/block-selector/all-start-blocks.tsx +++ b/web/app/components/workflow/block-selector/all-start-blocks.tsx @@ -33,7 +33,20 @@ import PluginList from './market-place-plugin/list' import StartBlocks from './start-blocks' import TriggerPluginList from './trigger-plugin/list' -const marketplaceFooterClassName = 'system-sm-medium z-10 flex h-8 flex-none cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg' +const popoverMarketplaceFooterClassName = 'system-sm-medium z-10 flex h-8 flex-none cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg' +const panelMarketplaceFooterClassName = 'system-xs-regular z-10 flex flex-none cursor-pointer flex-col items-start gap-2 px-4 pt-2 pb-4 text-text-tertiary hover:text-text-secondary' + +const SectionDivider = () => ( +
+ +
+) + +const MarketplaceFooterDivider = () => ( +
+ +
+) type AllStartBlocksProps = { className?: string @@ -42,6 +55,9 @@ type AllStartBlocksProps = { availableBlocksTypes?: BlockEnum[] tags?: string[] allowUserInputSelection?: boolean // Allow user input option even when trigger node already exists (e.g. when no Start node yet or changing node type). + hasUserInputNode?: boolean + hasTriggerNode?: boolean + variant?: 'popover' | 'panel' } const AllStartBlocks = ({ @@ -51,6 +67,9 @@ const AllStartBlocks = ({ availableBlocksTypes, tags = [], allowUserInputSelection = false, + hasUserInputNode = false, + hasTriggerNode = false, + variant = 'popover', }: AllStartBlocksProps) => { const { t } = useTranslation() const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false) @@ -100,9 +119,8 @@ const AllStartBlocks = ({ const shouldShowFeatured = enableTriggerPlugin && enable_marketplace && !hasFilter - const hasTriggerOptions = entryNodeTypes.some(type => type !== BlockEnumValue.Start) - const shouldShowTriggerListTitle = hasTriggerOptions && (hasStartBlocksContent || hasPluginContent) - const shouldShowMarketplaceFooter = enable_marketplace && !hasFilter + const shouldShowMarketplaceFooter = enable_marketplace + const isPanelVariant = variant === 'panel' const handleStartBlocksContentChange = useCallback((hasContent: boolean) => { setHasStartBlocksContent(hasContent) @@ -115,6 +133,11 @@ const AllStartBlocks = ({ const hasMarketplaceContent = enableTriggerPlugin && enable_marketplace && marketplacePlugins.length > 0 const hasAnyContent = hasStartBlocksContent || hasPluginContent || shouldShowFeatured || hasMarketplaceContent const shouldShowEmptyState = hasFilter && !hasAnyContent + const shouldShowInstalledTriggersDivider = isPanelVariant && hasStartBlocksContent && enableTriggerPlugin && hasPluginContent + const shouldShowMarketplaceSectionDivider = enableTriggerPlugin + && enable_marketplace + && (hasStartBlocksContent || hasPluginContent) + && (shouldShowFeatured || hasMarketplaceContent) useEffect(() => { if (!enableTriggerPlugin && hasPluginContent) @@ -134,16 +157,58 @@ const AllStartBlocks = ({ }, [enableTriggerPlugin, enable_marketplace, hasFilter, fetchPlugins, searchText, tags]) return ( -
-
+
+
pluginRef.current?.handleScroll()} >
- {shouldShowFeatured && ( - <> + {hasUserInputNode && ( +
+
+ + + +
+ {t('nodes.startPlaceholder.userInputConflictTip', { ns: 'workflow' })} +
+
+ )} + +
+ + + {shouldShowInstalledTriggersDivider && ( + + )} + + {enableTriggerPlugin && ( + + )} + + {shouldShowMarketplaceSectionDivider && ( + + )} + + {shouldShowFeatured && ( -
- -
- - )} - {shouldShowTriggerListTitle && ( -
- {t('tabs.allTriggers', { ns: 'workflow' })} -
- )} - - - {enableTriggerPlugin && ( - - )} - {enableTriggerPlugin && enable_marketplace && ( - } - list={marketplacePlugins} - searchText={trimmedSearchText} - category={PluginCategoryEnum.trigger} - tags={tags} - hideFindMoreFooter - /> - )} + )} + {enableTriggerPlugin && enable_marketplace && ( + } + list={marketplacePlugins} + searchText={trimmedSearchText} + category={PluginCategoryEnum.trigger} + tags={tags} + hideFindMoreFooter + /> + )} +
{shouldShowEmptyState && (
- {t('tabs.noPluginsFound', { ns: 'workflow' })} + {t('nodes.startPlaceholder.noTriggersFound', { ns: 'workflow' })}
- {shouldShowMarketplaceFooter && !shouldShowEmptyState && ( - // Footer - Same as Tools tab marketplace footer + {shouldShowMarketplaceFooter && ( - {t('findMoreInMarketplace', { ns: 'plugin' })} - + {isPanelVariant + ? ( + <> + + + + {t('nodes.startPlaceholder.browseMoreOnMarketplace', { ns: 'workflow' })} + + + ) + : ( + <> + {t('findMoreInMarketplace', { ns: 'plugin' })} + + + )} )}
diff --git a/web/app/components/workflow/block-selector/block-selector-row.tsx b/web/app/components/workflow/block-selector/block-selector-row.tsx new file mode 100644 index 00000000000..a9f1cf43b0d --- /dev/null +++ b/web/app/components/workflow/block-selector/block-selector-row.tsx @@ -0,0 +1,81 @@ +import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from 'react' +import { cn } from '@langgenius/dify-ui/cn' + +type SharedProps = { + children: ReactNode + className?: string + disabled?: boolean + hoverable?: boolean +} + +type ButtonRowProps = SharedProps + & Omit, 'className' | 'disabled'> + & { + as?: 'button' + nativeDisabled?: boolean + } + +type DivRowProps = SharedProps + & Omit, 'className'> + & { + as: 'div' + } + +type BlockSelectorRowProps = ButtonRowProps | DivRowProps + +const rowClassName = (className?: string, disabled = false, hoverable = true) => cn( + 'flex h-8 w-full items-center rounded-lg pr-2 pl-3', + !disabled && hoverable && 'hover:bg-state-base-hover', + disabled && 'cursor-not-allowed', + className, +) + +const buttonClassName = (className?: string, disabled = false, hoverable = true) => cn( + rowClassName(className, disabled, hoverable), + !disabled && 'cursor-pointer', + 'border-0 bg-transparent text-left focus-visible:ring-1 focus-visible:ring-components-input-border-hover focus-visible:outline-hidden', +) + +export function BlockSelectorRow(props: BlockSelectorRowProps) { + if (props.as === 'div') { + const { + as: _as, + children, + className, + disabled = false, + hoverable, + ...rest + } = props + + return ( +
+ {children} +
+ ) + } + + const { + as: _as, + children, + className, + disabled = false, + hoverable, + nativeDisabled, + type = 'button', + ...rest + } = props + + return ( + + ) +} diff --git a/web/app/components/workflow/block-selector/featured-triggers.tsx b/web/app/components/workflow/block-selector/featured-triggers.tsx index 13efb38c8f2..8b058feddb1 100644 --- a/web/app/components/workflow/block-selector/featured-triggers.tsx +++ b/web/app/components/workflow/block-selector/featured-triggers.tsx @@ -19,6 +19,7 @@ import { formatNumber } from '@/utils/format' import { getMarketplaceUrl } from '@/utils/var' import BlockIcon from '../block-icon' import { BlockEnum } from '../types' +import { BlockSelectorRow } from './block-selector-row' import { TriggerPluginActionPreviewCard } from './trigger-plugin/action-item' import TriggerPluginItem from './trigger-plugin/item' @@ -114,10 +115,10 @@ const FeaturedTriggers = ({ const showEmptyState = !isLoading && totalVisible === 0 return ( -
+
))} - {hasRes && ( -
-
- - - {t('searchInMarketplace', { ns: 'plugin' })} - -
-
- )}
) diff --git a/web/app/components/workflow/block-selector/start-blocks.tsx b/web/app/components/workflow/block-selector/start-blocks.tsx index 71323feeffa..dbd0129fd18 100644 --- a/web/app/components/workflow/block-selector/start-blocks.tsx +++ b/web/app/components/workflow/block-selector/start-blocks.tsx @@ -1,11 +1,13 @@ import type { BlockEnum, CommonNodeType } from '../types' import type { TriggerDefaultValue } from './types' +import { cn } from '@langgenius/dify-ui/cn' import { createPreviewCardHandle, PreviewCard, PreviewCardContent, PreviewCardTrigger, } from '@langgenius/dify-ui/preview-card' +import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip' import { memo, useCallback, @@ -16,6 +18,7 @@ import { useTranslation } from 'react-i18next' import useNodes from '@/app/components/workflow/store/workflow/use-nodes' import BlockIcon from '../block-icon' import { BlockEnum as BlockEnumValues } from '../types' +import { BlockSelectorRow } from './block-selector-row' // import { useNodeMetaData } from '../hooks' import { START_BLOCKS } from './constants' @@ -25,6 +28,10 @@ type StartBlocksProps = { availableBlocksTypes?: BlockEnum[] onContentStateChange?: (hasContent: boolean) => void hideUserInput?: boolean + showMostCommonBadge?: boolean + showUserInputAdded?: boolean + showUserInputDisabled?: boolean + disabled?: boolean } type StartBlockPreviewPayload = { block: typeof START_BLOCKS[number] @@ -36,6 +43,10 @@ const StartBlocks = ({ availableBlocksTypes = [], onContentStateChange, hideUserInput = false, // Allow parent to explicitly hide Start node option (e.g. when one already exists). + showMostCommonBadge = false, + showUserInputAdded = false, + showUserInputDisabled = false, + disabled = false, }: StartBlocksProps) => { const { t } = useTranslation() const nodes = useNodes() @@ -54,8 +65,9 @@ const StartBlocks = ({ } return START_BLOCKS.filter((block) => { - // Hide User Input (Start) if it already exists in workflow or if hideUserInput is true - if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput)) + // Hide User Input (Start) if it already exists in workflow or if hideUserInput is true. + // In read-only conflict modes, keep it visible so the row can show Added or disabled tooltip state. + if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput) && !showUserInputAdded && !showUserInputDisabled) return false // Filter by search text @@ -66,7 +78,7 @@ const StartBlocks = ({ // availableBlocksTypes now contains properly filtered entry node types from parent return availableBlocksTypes.includes(block.type) }) - }, [searchText, availableBlocksTypes, nodes, t, hideUserInput]) + }, [searchText, availableBlocksTypes, nodes, t, hideUserInput, showUserInputAdded, showUserInputDisabled]) const isEmpty = filteredBlocks.length === 0 @@ -78,43 +90,84 @@ const StartBlocks = ({ // reachable from the inspector + canvas once the row is clicked to insert // the start node, so hover/focus-only activation is a11y-safe. See // packages/dify-ui/AGENTS.md → Overlay Primitive Selection. - const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => ( - onSelect(block.type)} - > + const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => { + const isUserInput = block.type === BlockEnumValues.Start + const isUserInputDisabled = isUserInput && showUserInputDisabled + const isRowDisabled = disabled || (isUserInput && showUserInputAdded) || isUserInputDisabled + const label = t(`blocks.${block.type}`, { ns: 'workflow' }) + const disabledReason = t('nodes.startPlaceholder.userInputConflictTip', { ns: 'workflow' }) + const row = ( + { + if (isRowDisabled) + return + onSelect(block.type) + }} + > +
- {t(`blocks.${block.type}`, { ns: 'workflow' })} - {block.type === BlockEnumValues.Start && ( + {label} + {isUserInput && showUserInputAdded && ( + + {t('operation.added', { ns: 'common' })} + + )} + {isUserInput && showMostCommonBadge && !showUserInputAdded && ( + + {t('blocks.mostCommon', { ns: 'workflow' })} + + )} + {isUserInput && !showMostCommonBadge && !showUserInputAdded && !showUserInputDisabled && ( {t('blocks.originalStartNode', { ns: 'workflow' })} )}
- )} - /> - ), [onSelect, previewCardHandle, t]) +
+ ) + + if (isUserInputDisabled) { + return ( + + + +

+ {disabledReason} +

+
+
+ ) + } + + return ( + + ) + }, [disabled, onSelect, previewCardHandle, showMostCommonBadge, showUserInputAdded, showUserInputDisabled, t]) if (isEmpty) return null return (
-
+
{filteredBlocks.map((block, index) => (
{renderBlock(block)} - {block.type === BlockEnumValues.Start && index < filteredBlocks.length - 1 && ( + {block.type === BlockEnumValues.Start && !showMostCommonBadge && index < filteredBlocks.length - 1 && (
@@ -147,9 +200,17 @@ function StartBlockPreviewCard({ return null const { block } = payload + const description = block.type === BlockEnumValues.Start + ? t('nodes.start.userInputTipDescription', { ns: 'workflow' }) + : t(`blocksAbout.${block.type}`, { ns: 'workflow' }) + const showDifyTeamAuthor = [ + BlockEnumValues.Start, + BlockEnumValues.TriggerWebhook, + BlockEnumValues.TriggerSchedule, + ].includes(block.type) return ( - +
- {block.type === BlockEnumValues.TriggerWebhook - ? t('customWebhook', { ns: 'workflow' }) - : t(`blocks.${block.type}`, { ns: 'workflow' })} + {t(`blocks.${block.type}`, { ns: 'workflow' })}
- {t(`blocksAbout.${block.type}`, { ns: 'workflow' })} + {description}
- {(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && ( + {showDifyTeamAuthor && (
{t('author', { ns: 'tools' })} {' '} diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 1c960436fef..2145ce47f88 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -1,4 +1,4 @@ -import type { Dispatch, FC, SetStateAction } from 'react' +import type { Dispatch, FC, ReactNode, SetStateAction } from 'react' import type { BlockEnum, NodeDefault, @@ -10,6 +10,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too import { useSuspenseQuery } from '@tanstack/react-query' import { memo, useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { useDocLink } from '@/context/i18n' import { systemFeaturesQueryOptions } from '@/features/system-features/client' import { useFeaturedToolsRecommendations } from '@/service/use-plugins' import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools' @@ -35,13 +36,16 @@ type TabsProps = { key: TabsEnum name: string disabled?: boolean - disabledTip?: string + disabledTip?: ReactNode + disabledTipLinkKey?: 'startNodesDocs' }> filterElem: React.ReactNode noBlocks?: boolean noTools?: boolean forceShowStartContent?: boolean // Force show Start content even when noBlocks=true allowStartNodeSelection?: boolean // Allow user input option even when trigger node already exists (e.g. change-node flow or when no Start node yet). + hasUserInputNode?: boolean + hasTriggerNode?: boolean snippetsElem?: React.ReactNode } @@ -107,11 +111,15 @@ const TabHeaderItem = ({ activeTab, onActiveTabChange, disabledTip, + disabledTipLinkHref, + disabledTipLinkLabel, }: { tab: TabsProps['tabs'][number] activeTab: TabsEnum onActiveTabChange: (activeTab: TabsEnum) => void - disabledTip: string + disabledTip: ReactNode + disabledTipLinkHref?: string + disabledTipLinkLabel?: string }) => { const className = cn( 'relative mr-0.5 flex h-8 items-center rounded-t-lg px-3 system-sm-medium', @@ -144,8 +152,21 @@ const TabHeaderItem = ({ )} /> - - {disabledTip} + +
+

{disabledTip}

+ {disabledTipLinkHref && disabledTipLinkLabel && ( + e.stopPropagation()} + > + {disabledTipLinkLabel} + + )} +
) @@ -179,9 +200,12 @@ const Tabs: FC = ({ noTools, forceShowStartContent = false, allowStartNodeSelection = false, + hasUserInputNode = false, + hasTriggerNode = false, snippetsElem, }) => { const { t } = useTranslation() + const docLink = useDocLink() const { data: buildInTools } = useAllBuiltInTools() const { data: customTools } = useAllCustomTools() const { data: workflowTools } = useAllWorkflowTools() @@ -234,6 +258,8 @@ const Tabs: FC = ({ activeTab={activeTab} onActiveTabChange={onActiveTabChange} disabledTip={tab.disabledTip || disabledTip} + disabledTipLinkHref={tab.disabledTipLinkKey === 'startNodesDocs' ? docLink('/use-dify/nodes/trigger/overview') : undefined} + disabledTipLinkLabel={tab.disabledTipLinkKey === 'startNodesDocs' ? t('tabs.startDisabledTipLearnMore', { ns: 'workflow' }) : undefined} /> )) } @@ -246,6 +272,8 @@ const Tabs: FC = ({
{ })) }) + it('should select trigger plugin action items from the keyboard', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + const provider = createTriggerProvider() + const event = createEvent('on_created', 'On Created') + + render( + , + ) + + const action = screen.getByRole('button', { name: 'On Created' }) + await user.tab() + expect(action).toHaveFocus() + + await user.keyboard('{Enter}') + + expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerPlugin, expect.objectContaining({ + event_name: 'on_created', + event_label: 'On Created', + })) + }) + it('should expand providers and select workflow trigger providers directly', async () => { const user = userEvent.setup() const onSelect = vi.fn() @@ -135,6 +162,9 @@ describe('trigger plugin selector components', () => { ) await user.click(screen.getByText('Trigger Provider')) + + expect(screen.getByLabelText('workflow.tabs.allTriggers')).toHaveClass('max-h-[240px]', 'overscroll-contain') + await user.click(screen.getByText('Second Event')) expect(onSelect).toHaveBeenCalledWith(BlockEnum.TriggerPlugin, expect.objectContaining({ @@ -163,6 +193,34 @@ describe('trigger plugin selector components', () => { })) }) + it('should expand trigger providers from the keyboard', async () => { + const user = userEvent.setup() + const onSelect = vi.fn() + + render( + , + ) + + const provider = screen.getByRole('button', { name: /Trigger Provider/ }) + await user.tab() + expect(provider).toHaveFocus() + + await user.keyboard(' ') + + expect(screen.getByRole('region', { name: 'workflow.tabs.allTriggers' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Second Event' })).toBeInTheDocument() + }) + it('should filter trigger plugins and report whether content exists', async () => { const onContentStateChange = vi.fn() mockUseAllTriggerPlugins.mockReturnValue({ diff --git a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx index b4a243b8079..b5e283f4f8a 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx @@ -40,9 +40,14 @@ const TriggerPluginActionItem: FC = ({ const language = useGetLanguage() const row = ( -
{ if (disabled) return @@ -76,7 +81,7 @@ const TriggerPluginActionItem: FC = ({ {isAdded && (
{t('addToolModal.added', { ns: 'tools' })}
)} -
+ ) return ( diff --git a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx index 5932bbb94ad..75e747d4180 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx @@ -3,6 +3,13 @@ import type { FC } from 'react' import type { TriggerPluginActionPreviewCardHandle } from './action-item' import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types' import { cn } from '@langgenius/dify-ui/cn' +import { + ScrollAreaContent, + ScrollAreaRoot, + ScrollAreaScrollbar, + ScrollAreaThumb, + ScrollAreaViewport, +} from '@langgenius/dify-ui/scroll-area' import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react' import * as React from 'react' import { useMemo, useRef } from 'react' @@ -14,6 +21,7 @@ import { useGetLanguage } from '@/context/i18n' import useTheme from '@/hooks/use-theme' import { Theme } from '@/types/app' import { basePath } from '@/utils/var' +import { BlockSelectorRow } from '../block-selector-row' import TriggerPluginActionItem from './action-item' const normalizeProviderIcon = (icon?: TriggerWithProvider['icon']) => { @@ -30,6 +38,7 @@ type Props = Readonly<{ hasSearchText: boolean previewCardHandle: TriggerPluginActionPreviewCardHandle onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void + disabled?: boolean }> const TriggerPluginItem: FC = ({ @@ -38,6 +47,7 @@ const TriggerPluginItem: FC = ({ hasSearchText, previewCardHandle, onSelect, + disabled = false, }) => { const { t } = useTranslation() const language = useGetLanguage() @@ -93,9 +103,13 @@ const TriggerPluginItem: FC = ({ ref={ref} >
-
{ + if (disabled) + return if (hasAction) { setIsFold(!isFold) return @@ -125,13 +139,14 @@ const TriggerPluginItem: FC = ({ }) }} > -
+
-
+
{notShowProvider ? actions[0]?.label[language] : payload.label[language]} {groupName}
@@ -142,20 +157,33 @@ const TriggerPluginItem: FC = ({ )}
-
+ {!notShowProvider && hasAction && !isFold && ( - actions.map(action => ( - - )) + + + + {actions.map(action => ( + + ))} + + + + + + )}
diff --git a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx index 2d2752c4f67..d73ac3985dd 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx @@ -14,12 +14,14 @@ type TriggerPluginListProps = { searchText: string onContentStateChange?: (hasContent: boolean) => void tags?: string[] + disabled?: boolean } const TriggerPluginList = ({ onSelect, searchText, onContentStateChange, + disabled = false, }: TriggerPluginListProps) => { const { data: triggerPluginsData } = useAllTriggerPlugins() const language = useGetLanguage() @@ -99,6 +101,7 @@ const TriggerPluginList = ({ key={plugin.id} payload={plugin} onSelect={onSelect} + disabled={disabled} hasSearchText={!!searchText} previewCardHandle={previewCardHandle} /> diff --git a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts index 0989b08cbdb..955c514c81b 100644 --- a/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-available-blocks.spec.ts @@ -11,6 +11,7 @@ vi.mock('@/context/i18n', () => ({ useGetLanguage: () => 'en' })) const mockNodeTypes = [ BlockEnum.Start, + BlockEnum.StartPlaceholder, BlockEnum.End, BlockEnum.LLM, BlockEnum.Code, @@ -56,6 +57,11 @@ describe('useAvailableBlocks', () => { expect(result.current.availablePrevBlocks).toEqual([]) }) + it('should return empty array for StartPlaceholder node', () => { + const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.StartPlaceholder), { hooksStoreProps }) + expect(result.current.availablePrevBlocks).toEqual([]) + }) + it('should return empty array for trigger nodes', () => { for (const trigger of [BlockEnum.TriggerPlugin, BlockEnum.TriggerWebhook, BlockEnum.TriggerSchedule]) { const { result } = renderWorkflowHook(() => useAvailableBlocks(trigger), { hooksStoreProps }) @@ -97,9 +103,15 @@ describe('useAvailableBlocks', () => { expect(result.current.availableNextBlocks).toEqual([]) }) + it('should return empty array for StartPlaceholder node', () => { + const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.StartPlaceholder), { hooksStoreProps }) + expect(result.current.availableNextBlocks).toEqual([]) + }) + it('should return all available nodes for regular block types', () => { const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps }) expect(result.current.availableNextBlocks.length).toBeGreaterThan(0) + expect(result.current.availableNextBlocks).not.toContain(BlockEnum.StartPlaceholder) }) }) @@ -144,6 +156,14 @@ describe('useAvailableBlocks', () => { expect(blocks.availablePrevBlocks).toEqual([]) }) + it('should return no blocks for StartPlaceholder node', () => { + const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps }) + const blocks = result.current.getAvailableBlocks(BlockEnum.StartPlaceholder) + + expect(blocks.availablePrevBlocks).toEqual([]) + expect(blocks.availableNextBlocks).toEqual([]) + }) + it('should return empty nextBlocks for LoopEnd/KnowledgeBase and available nodes for End', () => { const { result } = renderWorkflowHook(() => useAvailableBlocks(BlockEnum.LLM), { hooksStoreProps }) diff --git a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts index ab9eb993d1f..ac7e11e113d 100644 --- a/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts +++ b/web/app/components/workflow/hooks/__tests__/use-checklist.spec.ts @@ -256,6 +256,31 @@ describe('useChecklist', () => { expect(startRequired!.canNavigate).toBe(false) }) + it('should not report the global missing start node item when a start placeholder is present', () => { + mockNodesMap[BlockEnum.StartPlaceholder] = { + checkValid: () => ({ errorMessage: 'workflow.nodes.startPlaceholder.validationRequired' }), + metaData: { isStart: false, isRequired: false }, + } + const placeholderNode = createNode({ + id: 'start-placeholder', + data: { type: BlockEnum.StartPlaceholder, title: 'Workflow start' }, + }) + + const { result } = renderWorkflowHook( + () => useChecklist([placeholderNode], []), + ) + + expect(result.current.find((item: ChecklistItem) => item.id === 'start-node-required')).toBeUndefined() + expect(result.current).toEqual([ + expect.objectContaining({ + id: 'start-placeholder', + type: BlockEnum.StartPlaceholder, + unConnected: false, + errorMessages: ['workflow.nodes.startPlaceholder.validationRequired'], + }), + ]) + }) + it('should detect plugin not installed', () => { const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } }) const toolNode = createNode({ diff --git a/web/app/components/workflow/hooks/use-available-blocks.ts b/web/app/components/workflow/hooks/use-available-blocks.ts index 888e1264744..c42eee5345f 100644 --- a/web/app/components/workflow/hooks/use-available-blocks.ts +++ b/web/app/components/workflow/hooks/use-available-blocks.ts @@ -6,6 +6,9 @@ import { BlockEnum } from '../types' import { useNodesMetaData } from './use-nodes-meta-data' const availableBlocksFilter = (nodeType: BlockEnum, inContainer?: boolean) => { + if (nodeType === BlockEnum.StartPlaceholder) + return false + if (inContainer && (nodeType === BlockEnum.Iteration || nodeType === BlockEnum.Loop || nodeType === BlockEnum.End || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.KnowledgeBase || nodeType === BlockEnum.HumanInput)) return false @@ -21,7 +24,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) } = useNodesMetaData() const availableNodesType = useMemo(() => availableNodes.map(node => node.metaData.type), [availableNodes]) const availablePrevBlocks = useMemo(() => { - if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource + if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.DataSource || nodeType === BlockEnum.TriggerPlugin || nodeType === BlockEnum.TriggerWebhook || nodeType === BlockEnum.TriggerSchedule) { return [] @@ -30,7 +33,7 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) return availableNodesType }, [availableNodesType, nodeType]) const availableNextBlocks = useMemo(() => { - if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase) + if (!nodeType || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase) return [] return availableNodesType @@ -38,11 +41,11 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean) const getAvailableBlocks = useCallback((nodeType?: BlockEnum, inContainer?: boolean) => { let availablePrevBlocks = availableNodesType - if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource) + if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.DataSource) availablePrevBlocks = [] let availableNextBlocks = availableNodesType - if (!nodeType || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase) + if (!nodeType || nodeType === BlockEnum.StartPlaceholder || nodeType === BlockEnum.LoopEnd || nodeType === BlockEnum.KnowledgeBase) availableNextBlocks = [] return { diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 2f74c66d5f4..2eb73ae2a2c 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -310,7 +310,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType? } const isStartNodeMeta = nodesExtraData?.[node!.data.type as BlockEnum]?.metaData.isStart ?? false - const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true + const isStartPlaceholderNode = node!.data.type === BlockEnum.StartPlaceholder + const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta || isStartPlaceholderNode : true const isUnconnected = !validNodes.some(n => n.id === node!.id) const shouldShowError = errorMessages.length > 0 || (isUnconnected && !canSkipConnectionCheck) @@ -337,7 +338,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[], options?: { flowType? // Check for start nodes (including triggers) if (shouldCheckStartNode) { const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum)) - if (startNodesFiltered.length === 0) { + const hasStartPlaceholderNode = nodes.some(node => node.data.type === BlockEnum.StartPlaceholder) + if (startNodesFiltered.length === 0 && !hasStartPlaceholderNode) { list.push({ id: 'start-node-required', type: BlockEnum.Start, diff --git a/web/app/components/workflow/hooks/use-inspect-vars-crud.ts b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts index 1bca16aea6a..b978455a075 100644 --- a/web/app/components/workflow/hooks/use-inspect-vars-crud.ts +++ b/web/app/components/workflow/hooks/use-inspect-vars-crud.ts @@ -12,7 +12,8 @@ const varsAppendStartNodeKeys = ['query', 'files'] const useInspectVarsCrud = () => { const partOfNodesWithInspectVars = useStore(s => s.nodesWithInspectVars) const configsMap = useHooksStore(s => s.configsMap) - const shouldSkipSharedVariableQueries = configsMap?.flowType === FlowType.ragPipeline || configsMap?.flowType === FlowType.snippet + const shouldSkipSharedVariableQueries = configsMap?.flowType === FlowType.ragPipeline + || configsMap?.flowType === FlowType.snippet const variableFlowId = shouldSkipSharedVariableQueries ? '' : configsMap?.flowId const { data: conversationVars } = useConversationVarValues(configsMap?.flowType, variableFlowId) const { data: allSystemVars } = useSysVarValues(configsMap?.flowType, variableFlowId) diff --git a/web/app/components/workflow/nodes/__tests__/index.spec.tsx b/web/app/components/workflow/nodes/__tests__/index.spec.tsx index 41eb853a99e..a65b3278382 100644 --- a/web/app/components/workflow/nodes/__tests__/index.spec.tsx +++ b/web/app/components/workflow/nodes/__tests__/index.spec.tsx @@ -8,9 +8,11 @@ import CustomNode, { Panel } from '../index' vi.mock('../components', () => ({ NodeComponentMap: { [BlockEnum.Start]: () =>
start-node-component
, + [BlockEnum.StartPlaceholder]: () =>
start-placeholder-node-component
, }, PanelComponentMap: { [BlockEnum.Start]: () =>
start-panel-component
, + [BlockEnum.StartPlaceholder]: () =>
start-placeholder-panel-component
, }, })) @@ -32,9 +34,10 @@ vi.mock('../_base/node', () => ({ ), })) -vi.mock('../_base/components/workflow-panel', () => ({ - __esModule: true, - default: ({ +vi.mock('../_base/components/workflow-panel', async () => { + const React = await vi.importActual('react') + + const MockWorkflowPanel = ({ id, data, children, @@ -42,13 +45,23 @@ vi.mock('../_base/components/workflow-panel', () => ({ id: string data: { type: BlockEnum } children: ReactElement - }) => ( -
-
{`base-panel:${id}:${data.type}`}
- {children} -
- ), -})) + }) => { + const [initialType] = React.useState(data.type) + + return ( +
+
{`base-panel:${id}:${data.type}`}
+
{`base-panel-initial:${initialType}`}
+ {children} +
+ ) + } + + return { + __esModule: true, + default: MockWorkflowPanel, + } +}) const createNodeData = (): WorkflowNode['data'] => ({ title: 'Start', @@ -56,6 +69,12 @@ const createNodeData = (): WorkflowNode['data'] => ({ type: BlockEnum.Start, }) +const createStartPlaceholderData = (): WorkflowNode['data'] => ({ + title: 'Pick a start node', + desc: '', + type: BlockEnum.StartPlaceholder, +}) + const baseNodeProps = { type: CUSTOM_NODE, selected: false, @@ -93,6 +112,29 @@ describe('workflow nodes index', () => { expect(screen.getByText('start-panel-component')).toBeInTheDocument() }) + it('should remount the base panel when a node keeps its id but changes type', () => { + const { rerender } = render( + , + ) + + expect(screen.getByText('base-panel-initial:start-placeholder')).toBeInTheDocument() + + rerender( + , + ) + + expect(screen.getByText('base-panel:node-1:start')).toBeInTheDocument() + expect(screen.getByText('base-panel-initial:start')).toBeInTheDocument() + }) + it('should return null for non-custom panel types', () => { const { container } = render( { expect(document.querySelector('.i-ri-pause-circle-fill')).toBeInTheDocument() }) - it('should render success icon when inspect vars exist without running status and hide description for loop nodes', () => { + it('should render success icon when inspect vars exist without running status and hide description for non-description nodes', () => { const t = ((key: string) => key) as unknown as TFunction const { rerender } = render( { rerender() expect(screen.queryByText('hidden')).not.toBeInTheDocument() + + rerender() + expect(screen.queryByText('old placeholder description')).not.toBeInTheDocument() }) }) diff --git a/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts b/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts index 78e1f938c55..01275cbcd33 100644 --- a/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts +++ b/web/app/components/workflow/nodes/_base/__tests__/node.helpers.spec.ts @@ -24,6 +24,7 @@ describe('node helpers', () => { it('should identify entry and container nodes', () => { expect(isEntryWorkflowNode(BlockEnum.Start)).toBe(true) + expect(isEntryWorkflowNode(BlockEnum.StartPlaceholder)).toBe(true) expect(isEntryWorkflowNode(BlockEnum.TriggerWebhook)).toBe(true) expect(isEntryWorkflowNode(BlockEnum.Tool)).toBe(false) diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index c73361e35c3..c6da272c937 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -91,6 +91,11 @@ import { } from './helpers' import LastRun from './last-run' import useLastRun from './last-run/use-last-run' +import { + StartPlaceholderPanelBody, + StartPlaceholderPanelDescription, + StartPlaceholderPanelTitle, +} from './start-placeholder-panel' import { TriggerSubscription } from './trigger-subscription' import { TabType } from './types' @@ -480,6 +485,19 @@ const BasePanel: FC = ({ const singleRunActionLabel = isSingleRunning ? t('debug.variableInspect.trigger.stop', { ns: 'workflow' }) : runThisStepLabel + const isStartPlaceholderPanel = data.type === BlockEnum.StartPlaceholder + const panelChildren = cloneElement(children as any, { + id, + data, + panelProps: { + getInputVars, + toVarInputs, + runInputData, + setRunInputData, + runResult, + runInputDataRef, + }, + }) const panelTabs = ( @@ -519,16 +537,24 @@ const BasePanel: FC = ({ >
- - + {!isStartPlaceholderPanel && ( + + )} + {isStartPlaceholderPanel + ? ( + + ) + : ( + + )} {viewingUsers.length > 0 && (
= ({
-
- -
- { - needsToolAuth && ( - -
- {panelTabs} - + ) + : ( +
+ +
+ )} + {!isStartPlaceholderPanel && ( + <> + { + needsToolAuth && ( + -
-
- ) - } - { - !!currentDataSource && ( - -
- {panelTabs} - +
+ {panelTabs} + +
+ + ) + } + { + !!currentDataSource && ( + -
-
- ) - } - { - currentTriggerPlugin && ( - - {panelTabs} - - ) - } - { - !needsToolAuth && !currentDataSource && !currentTriggerPlugin && ( -
- {panelTabs} -
- ) - } - + isAuthorized={currentDataSource.is_authorized} + > +
+ {panelTabs} + +
+ + ) + } + { + currentTriggerPlugin && ( + + {panelTabs} + + ) + } + { + !needsToolAuth && !currentDataSource && !currentTriggerPlugin && ( +
+ {panelTabs} +
+ ) + } + + + )}
- -
- {cloneElement(children as any, { - id, - data, - panelProps: { - getInputVars, - toVarInputs, - runInputData, - setRunInputData, - runResult, - runInputDataRef, - }, - })} -
- - { - hasRetryNode(data.type) && ( - - ) - } - { - hasErrorHandleNode(data.type) && ( - - ) - } - { - !!availableNextBlocks.length && ( -
-
- {t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()} -
-
- {t('panel.addNextStep', { ns: 'workflow' })} -
- -
- ) - } - {readmeEntranceComponent} -
- - - + {isStartPlaceholderPanel && ( + + {panelChildren} + + )} + + {!isStartPlaceholderPanel && ( + +
+ {panelChildren} +
+ + { + hasRetryNode(data.type) && ( + + ) + } + { + hasErrorHandleNode(data.type) && ( + + ) + } + { + !!availableNextBlocks.length && ( +
+
+ {t('panel.nextStep', { ns: 'workflow' }).toLocaleUpperCase()} +
+
+ {t('panel.addNextStep', { ns: 'workflow' })} +
+ +
+ ) + } + {readmeEntranceComponent} +
+ )} + + {!isStartPlaceholderPanel && ( + + + + )}
diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts index b0bbc98d7e4..a538bcf29e3 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/last-run/use-last-run.ts @@ -54,6 +54,7 @@ const singleRunFormParamsHooks: Record = { [BlockEnum.DocExtractor]: useDocExtractorSingleRunFormParams, [BlockEnum.Loop]: useLoopSingleRunFormParams, [BlockEnum.Start]: useStartSingleRunFormParams, + [BlockEnum.StartPlaceholder]: undefined, [BlockEnum.IfElse]: useIfElseSingleRunFormParams, [BlockEnum.VariableAggregator]: useVariableAggregatorSingleRunFormParams, [BlockEnum.Assigner]: useVariableAssignerSingleRunFormParams, @@ -93,6 +94,7 @@ const getDataForCheckMoreHooks: Record = { [BlockEnum.DocExtractor]: undefined, [BlockEnum.Loop]: undefined, [BlockEnum.Start]: undefined, + [BlockEnum.StartPlaceholder]: undefined, [BlockEnum.IfElse]: undefined, [BlockEnum.VariableAggregator]: undefined, [BlockEnum.End]: undefined, diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/start-placeholder-panel.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/start-placeholder-panel.tsx new file mode 100644 index 00000000000..fa8e6f41bc3 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/start-placeholder-panel.tsx @@ -0,0 +1,34 @@ +import type { ReactNode } from 'react' +import { useTranslation } from 'react-i18next' + +export function StartPlaceholderPanelTitle() { + const { t } = useTranslation() + + return ( +
+ {t('nodes.startPlaceholder.panelTitle', { ns: 'workflow' })} +
+ ) +} + +export function StartPlaceholderPanelDescription() { + const { t } = useTranslation() + + return ( +
+ {t('nodes.startPlaceholder.panelDescription', { ns: 'workflow' })} +
+ ) +} + +export function StartPlaceholderPanelBody({ + children, +}: { + children: ReactNode +}) { + return ( +
+ {children} +
+ ) +} diff --git a/web/app/components/workflow/nodes/_base/node-sections.tsx b/web/app/components/workflow/nodes/_base/node-sections.tsx index 8b60c103c7f..3916c054d03 100644 --- a/web/app/components/workflow/nodes/_base/node-sections.tsx +++ b/web/app/components/workflow/nodes/_base/node-sections.tsx @@ -83,7 +83,7 @@ export const NodeBody = ({ } export const NodeDescription = ({ data }: { data: NodeProps['data'] }) => { - if (!data.desc || data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop) + if (!data.desc || data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop || data.type === BlockEnum.StartPlaceholder) return null return ( diff --git a/web/app/components/workflow/nodes/_base/node.helpers.tsx b/web/app/components/workflow/nodes/_base/node.helpers.tsx index 2019517133d..2614d4b058f 100644 --- a/web/app/components/workflow/nodes/_base/node.helpers.tsx +++ b/web/app/components/workflow/nodes/_base/node.helpers.tsx @@ -24,7 +24,7 @@ export const getLoopIndexTextKey = (runningStatus: NodeRunningStatus | undefined } export const isEntryWorkflowNode = (type: NodeProps['data']['type']) => { - return isTriggerNode(type) || type === BlockEnum.Start + return isTriggerNode(type) || type === BlockEnum.Start || type === BlockEnum.StartPlaceholder } export const isContainerNode = (type: NodeProps['data']['type']) => { diff --git a/web/app/components/workflow/nodes/_base/node.tsx b/web/app/components/workflow/nodes/_base/node.tsx index 13ec43f3a75..680a8894d0e 100644 --- a/web/app/components/workflow/nodes/_base/node.tsx +++ b/web/app/components/workflow/nodes/_base/node.tsx @@ -229,7 +229,7 @@ const BaseNode: FC = ({ ) } { - !data._isCandidate && ( + data.type !== BlockEnum.StartPlaceholder && !data._isCandidate && ( = ({ ) } { - data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && ( + data.type !== BlockEnum.StartPlaceholder && data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && data.type !== BlockEnum.HumanInput && !data._isCandidate && ( = ({
) - const isStartNode = data.type === BlockEnum.Start + const isStartNode = data.type === BlockEnum.Start || data.type === BlockEnum.StartPlaceholder const isEntryNode = isEntryWorkflowNode(data.type) return isEntryNode diff --git a/web/app/components/workflow/nodes/components.ts b/web/app/components/workflow/nodes/components.ts index ec1e4422bf9..7c196e3f2f5 100644 --- a/web/app/components/workflow/nodes/components.ts +++ b/web/app/components/workflow/nodes/components.ts @@ -36,6 +36,8 @@ import ParameterExtractorNode from './parameter-extractor/node' import ParameterExtractorPanel from './parameter-extractor/panel' import QuestionClassifierNode from './question-classifier/node' import QuestionClassifierPanel from './question-classifier/panel' +import StartPlaceholderNode from './start-placeholder/node' +import StartPlaceholderPanel from './start-placeholder/panel' import StartNode from './start/node' import StartPanel from './start/panel' import TemplateTransformNode from './template-transform/node' @@ -53,6 +55,7 @@ import VariableAssignerPanel from './variable-assigner/panel' export const NodeComponentMap: Record> = { [BlockEnum.Start]: StartNode, + [BlockEnum.StartPlaceholder]: StartPlaceholderNode, [BlockEnum.End]: EndNode, [BlockEnum.Answer]: AnswerNode, [BlockEnum.LLM]: LLMNode, @@ -82,6 +85,7 @@ export const NodeComponentMap: Record> = { export const PanelComponentMap: Record> = { [BlockEnum.Start]: StartPanel, + [BlockEnum.StartPlaceholder]: StartPlaceholderPanel, [BlockEnum.End]: EndPanel, [BlockEnum.Answer]: AnswerPanel, [BlockEnum.LLM]: LLMPanel, diff --git a/web/app/components/workflow/nodes/index.tsx b/web/app/components/workflow/nodes/index.tsx index 7575a156cf9..51893953d41 100644 --- a/web/app/components/workflow/nodes/index.tsx +++ b/web/app/components/workflow/nodes/index.tsx @@ -46,7 +46,7 @@ export const Panel = memo((props: PanelProps) => { if (nodeClass === CUSTOM_NODE) { return ( diff --git a/web/app/components/workflow/nodes/start-placeholder/__tests__/node.spec.tsx b/web/app/components/workflow/nodes/start-placeholder/__tests__/node.spec.tsx new file mode 100644 index 00000000000..6d3dcfcce41 --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/__tests__/node.spec.tsx @@ -0,0 +1,31 @@ +import { render, screen } from '@testing-library/react' +import { BlockEnum } from '@/app/components/workflow/types' +import Node from '../node' + +describe('StartPlaceholderNode', () => { + it('should show the right-panel hint while selected and the click hint after the panel closes', () => { + const { rerender } = render( + , + ) + + expect(screen.getByText('workflow.nodes.startPlaceholder.nodeDescription')).toBeInTheDocument() + + rerender( + , + ) + + expect(screen.getByText('workflow.nodes.startPlaceholder.nodeCollapsedDescription')).toBeInTheDocument() + }) +}) diff --git a/web/app/components/workflow/nodes/start-placeholder/__tests__/panel.spec.tsx b/web/app/components/workflow/nodes/start-placeholder/__tests__/panel.spec.tsx new file mode 100644 index 00000000000..5e98de3b043 --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/__tests__/panel.spec.tsx @@ -0,0 +1,139 @@ +import type { Node } from '@/app/components/workflow/types' +import { fireEvent, render, screen } from '@testing-library/react' +import { BlockEnum } from '@/app/components/workflow/types' +import Panel from '../panel' + +const mocks = vi.hoisted(() => ({ + autoGenerateWebhookUrl: vi.fn(), + handleSyncWorkflowDraft: vi.fn(), + setHasSelectedStartNode: vi.fn(), + setNodes: vi.fn(), + setShouldAutoOpenStartNodeSelector: vi.fn(), +})) + +let currentNodes: Node[] = [] + +vi.mock('reactflow', () => ({ + useStoreApi: () => ({ + getState: () => ({ + getNodes: () => currentNodes, + setNodes: mocks.setNodes, + }), + }), +})) + +vi.mock('@/app/components/plugins/marketplace/search-box', () => ({ + default: () => , +})) + +vi.mock('@/app/components/workflow/block-selector/all-start-blocks', () => ({ + default: ({ onSelect }: { onSelect: (type: BlockEnum) => void }) => ( + + ), +})) + +vi.mock('@/app/components/workflow/hooks', () => ({ + useAutoGenerateWebhookUrl: () => mocks.autoGenerateWebhookUrl, +})) + +vi.mock('@/app/components/workflow/hooks-store', () => ({ + useHooksStore: (selector: (state: unknown) => unknown) => selector({ + availableNodesMetaData: { + nodesMap: { + [BlockEnum.Start]: { + defaultValue: { + title: 'User Input', + desc: '', + variables: [], + }, + }, + }, + }, + }), +})) + +vi.mock('@/app/components/workflow/hooks/use-nodes-sync-draft', () => ({ + useNodesSyncDraft: () => ({ + handleSyncWorkflowDraft: mocks.handleSyncWorkflowDraft, + }), +})) + +vi.mock('@/app/components/workflow/store', () => ({ + useStore: (selector: (state: unknown) => unknown) => selector({ + setHasSelectedStartNode: mocks.setHasSelectedStartNode, + setShouldAutoOpenStartNodeSelector: mocks.setShouldAutoOpenStartNodeSelector, + }), +})) + +const createPlaceholderNode = (): Node => ({ + id: 'placeholder-1', + type: 'custom', + position: { x: 0, y: 0 }, + data: { + type: BlockEnum.StartPlaceholder, + title: 'Pick a start node', + desc: '', + selected: true, + }, +}) + +describe('StartPlaceholderPanel', () => { + beforeEach(() => { + vi.clearAllMocks() + currentNodes = [ + createPlaceholderNode(), + { + id: 'other-node', + type: 'custom', + position: { x: 100, y: 0 }, + data: { + type: BlockEnum.LLM, + title: 'LLM', + desc: '', + selected: true, + }, + }, + ] + mocks.setNodes.mockImplementation((nodes: Node[]) => { + currentNodes = nodes + }) + }) + + describe('Start node selection', () => { + it('should replace the placeholder with user input and auto-open the next node selector', () => { + render( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'Select User Input' })) + + expect(mocks.setNodes).toHaveBeenCalledTimes(1) + expect(currentNodes[0]).toMatchObject({ + id: 'placeholder-1', + data: { + type: BlockEnum.Start, + title: 'User Input', + selected: true, + variables: [], + }, + }) + expect(currentNodes[1]?.data.selected).toBe(false) + expect(mocks.setHasSelectedStartNode).toHaveBeenCalledWith(true) + expect(mocks.setShouldAutoOpenStartNodeSelector).toHaveBeenCalledWith(true) + expect(mocks.handleSyncWorkflowDraft).toHaveBeenCalledWith(true, false, expect.objectContaining({ + onSuccess: expect.any(Function), + })) + + const callback = mocks.handleSyncWorkflowDraft.mock.calls[0]?.[2] as { onSuccess: () => void } + callback.onSuccess() + + expect(mocks.autoGenerateWebhookUrl).toHaveBeenCalledWith('placeholder-1') + }) + }) +}) diff --git a/web/app/components/workflow/nodes/start-placeholder/default.ts b/web/app/components/workflow/nodes/start-placeholder/default.ts new file mode 100644 index 00000000000..9a4eb2aad90 --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/default.ts @@ -0,0 +1,29 @@ +import type { NodeDefault } from '../../types' +import type { StartPlaceholderNodeType } from './types' +import { BlockEnum } from '@/app/components/workflow/types' +import { genNodeMetaData } from '@/app/components/workflow/utils' + +const metaData = genNodeMetaData({ + sort: 0.05, + type: BlockEnum.StartPlaceholder, + isRequired: false, + isSingleton: true, + isTypeFixed: true, + helpLinkUri: 'user-input', +}) + +const nodeDefault: NodeDefault = { + metaData, + defaultValue: { + title: 'Workflow start', + desc: '', + }, + checkValid(_payload, t) { + return { + isValid: false, + errorMessage: t('nodes.startPlaceholder.validationRequired', { ns: 'workflow' }), + } + }, +} + +export default nodeDefault diff --git a/web/app/components/workflow/nodes/start-placeholder/node.tsx b/web/app/components/workflow/nodes/start-placeholder/node.tsx new file mode 100644 index 00000000000..498fd89cabf --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/node.tsx @@ -0,0 +1,25 @@ +import type { FC } from 'react' +import type { NodeProps } from '@/app/components/workflow/types' +import * as React from 'react' +import { useTranslation } from 'react-i18next' + +const i18nPrefix = 'nodes.startPlaceholder' + +const Node: FC = ({ + data, +}) => { + const { t } = useTranslation() + const descriptionKey = data.selected ? 'nodeDescription' : 'nodeCollapsedDescription' + + return ( +
+
+
+ {t(`${i18nPrefix}.${descriptionKey}`, { ns: 'workflow' })} +
+
+
+ ) +} + +export default React.memo(Node) diff --git a/web/app/components/workflow/nodes/start-placeholder/panel.tsx b/web/app/components/workflow/nodes/start-placeholder/panel.tsx new file mode 100644 index 00000000000..86b13a568ea --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/panel.tsx @@ -0,0 +1,167 @@ +import type { FC } from 'react' +import type { StartPlaceholderNodeType } from './types' +import type { + PluginDefaultValue, + TriggerDefaultValue, +} from '@/app/components/workflow/block-selector/types' +import type { NodePanelProps } from '@/app/components/workflow/types' +import * as React from 'react' +import { + useCallback, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import { useStoreApi } from 'reactflow' +import SearchBox from '@/app/components/plugins/marketplace/search-box' +import AllStartBlocks from '@/app/components/workflow/block-selector/all-start-blocks' +import { useAutoGenerateWebhookUrl } from '@/app/components/workflow/hooks' +import { useHooksStore } from '@/app/components/workflow/hooks-store' +import { useNodesSyncDraft } from '@/app/components/workflow/hooks/use-nodes-sync-draft' +import { useStore as useWorkflowStore } from '@/app/components/workflow/store' +import { BlockEnum } from '@/app/components/workflow/types' + +const getTriggerPluginNodeData = ( + triggerConfig: TriggerDefaultValue, + fallbackTitle?: string, + fallbackDesc?: string, +) => { + return { + plugin_id: triggerConfig.plugin_id, + provider_id: triggerConfig.provider_name, + provider_type: triggerConfig.provider_type, + provider_name: triggerConfig.provider_name, + event_name: triggerConfig.event_name, + event_label: triggerConfig.event_label, + event_description: triggerConfig.event_description, + title: triggerConfig.event_label || triggerConfig.title || fallbackTitle, + desc: triggerConfig.event_description || fallbackDesc, + output_schema: { ...triggerConfig.output_schema }, + parameters_schema: triggerConfig.paramSchemas ? [...triggerConfig.paramSchemas] : [], + config: { ...triggerConfig.params }, + subscription_id: triggerConfig.subscription_id, + plugin_unique_identifier: triggerConfig.plugin_unique_identifier, + is_team_authorization: triggerConfig.is_team_authorization, + meta: triggerConfig.meta ? { ...triggerConfig.meta } : undefined, + } +} + +const Panel: FC> = ({ + id, +}) => { + const { t } = useTranslation() + const [searchText, setSearchText] = useState('') + const [tags, setTags] = useState([]) + const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData) + const setHasSelectedStartNode = useWorkflowStore(s => s.setHasSelectedStartNode) + const setShouldAutoOpenStartNodeSelector = useWorkflowStore(s => s.setShouldAutoOpenStartNodeSelector) + const reactFlowStore = useStoreApi() + const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl() + const { handleSyncWorkflowDraft } = useNodesSyncDraft() + + const handleSelectStartNode = useCallback((nodeType: BlockEnum, toolConfig?: PluginDefaultValue) => { + const nodeDefault = availableNodesMetaData?.nodesMap?.[nodeType] + if (!nodeDefault?.defaultValue) + return + + const baseNodeData = { ...nodeDefault.defaultValue } + const mergedNodeData = (() => { + if (nodeType !== BlockEnum.TriggerPlugin || !toolConfig) { + return { + ...baseNodeData, + ...toolConfig, + } + } + + const triggerNodeData = getTriggerPluginNodeData( + toolConfig as TriggerDefaultValue, + baseNodeData.title, + baseNodeData.desc, + ) + + return { + ...baseNodeData, + ...triggerNodeData, + config: { + ...(baseNodeData as { config?: Record }).config, + ...triggerNodeData.config, + }, + } + })() + + const { getNodes, setNodes } = reactFlowStore.getState() + const nextNodes = getNodes().map((node) => { + if (node.id !== id) { + return { + ...node, + data: { + ...node.data, + selected: false, + }, + } + } + + return { + ...node, + data: { + ...mergedNodeData, + type: nodeType, + selected: true, + }, + } + }) + + setNodes(nextNodes) + setHasSelectedStartNode?.(true) + setShouldAutoOpenStartNodeSelector?.(true) + + handleSyncWorkflowDraft(true, false, { + onSuccess: () => { + autoGenerateWebhookUrl(id) + }, + onError: () => { + console.error('Failed to save start node selection to draft') + }, + }) + }, [ + autoGenerateWebhookUrl, + availableNodesMetaData?.nodesMap, + handleSyncWorkflowDraft, + id, + reactFlowStore, + setHasSelectedStartNode, + setShouldAutoOpenStartNodeSelector, + ]) + + return ( +
+
+ +
+
+ +
+
+ ) +} + +export default React.memo(Panel) diff --git a/web/app/components/workflow/nodes/start-placeholder/types.ts b/web/app/components/workflow/nodes/start-placeholder/types.ts new file mode 100644 index 00000000000..30f354f5b08 --- /dev/null +++ b/web/app/components/workflow/nodes/start-placeholder/types.ts @@ -0,0 +1,3 @@ +import type { CommonNodeType } from '@/app/components/workflow/types' + +export type StartPlaceholderNodeType = CommonNodeType diff --git a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx index 5086b148702..84bbe149e60 100644 --- a/web/app/components/workflow/operator/__tests__/add-block.spec.tsx +++ b/web/app/components/workflow/operator/__tests__/add-block.spec.tsx @@ -3,6 +3,7 @@ import { act, screen, waitFor } from '@testing-library/react' import { FlowType } from '@/types/common' import { createNode } from '../../__tests__/fixtures' import { renderWorkflowFlowComponent } from '../../__tests__/workflow-test-env' +import { TabsEnum } from '../../block-selector/types' import { BlockEnum } from '../../types' import AddBlock from '../add-block' @@ -20,6 +21,7 @@ type BlockSelectorMockProps = { popupClassName: string availableBlocksTypes: BlockEnum[] showStartTab: boolean + defaultActiveTab?: TabsEnum } const { @@ -127,6 +129,7 @@ describe('AddBlock', () => { disabled: false, availableBlocksTypes: mockAvailableNextBlocks, showStartTab: true, + defaultActiveTab: TabsEnum.Start, placement: 'right-start', popupClassName: 'min-w-[256px]!', }) @@ -151,6 +154,20 @@ describe('AddBlock', () => { expect(latestBlockSelectorProps?.showStartTab).toBe(false) }) + + it.each([ + BlockEnum.Start, + BlockEnum.TriggerWebhook, + ])('should keep the normal default tab when a %s node already exists', async (type) => { + renderWithReactFlow([ + createNode({ id: 'entry-node', position: { x: 0, y: 0 }, data: { type } }), + ]) + + await waitFor(() => expect(latestBlockSelectorProps).not.toBeNull()) + + expect(latestBlockSelectorProps?.showStartTab).toBe(true) + expect(latestBlockSelectorProps?.defaultActiveTab).toBeUndefined() + }) }) // User interactions that bridge selector state and workflow state. diff --git a/web/app/components/workflow/operator/add-block.tsx b/web/app/components/workflow/operator/add-block.tsx index 03e2dc9c468..6a20c10c805 100644 --- a/web/app/components/workflow/operator/add-block.tsx +++ b/web/app/components/workflow/operator/add-block.tsx @@ -10,12 +10,17 @@ import { useState, } from 'react' import { useTranslation } from 'react-i18next' -import { useStoreApi } from 'reactflow' +import { + useNodes, + useStoreApi, +} from 'reactflow' import BlockSelector from '@/app/components/workflow/block-selector' import { BlockEnum, + isTriggerNode, } from '@/app/components/workflow/types' import { FlowType } from '@/types/common' +import { TabsEnum } from '../block-selector/types' import { useAvailableBlocks, useIsChatMode, @@ -50,10 +55,18 @@ const AddBlock = ({ const { nodesReadOnly } = useNodesReadOnly() const { handlePaneContextmenuCancel } = usePanelInteractions() const [open, setOpen] = useState(false) + const nodes = useNodes() const { availableNextBlocks } = useAvailableBlocks(BlockEnum.Start, false) const { nodesMap: nodesMetaDataMap } = useNodesMetaData() const flowType = useHooksStore(s => s.configsMap?.flowType) const showStartTab = flowType !== FlowType.ragPipeline && !isChatMode + const hasEntryNode = nodes.some((node) => { + const nodeData = node.data as { type?: BlockEnum } + const nodeType = nodeData.type + return nodeType === BlockEnum.Start || (nodeType ? isTriggerNode(nodeType) : false) + }) + + const defaultActiveTab = showStartTab && !hasEntryNode ? TabsEnum.Start : undefined const handleOpenChange = useCallback((open: boolean) => { setOpen(open) @@ -121,6 +134,7 @@ const AddBlock = ({ popupClassName="min-w-[256px]!" availableBlocksTypes={availableNextBlocks} showStartTab={showStartTab} + defaultActiveTab={defaultActiveTab} /> ) } diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index 77457b379f9..8835d3e45b2 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -27,6 +27,7 @@ import type { export enum BlockEnum { Start = 'start', + StartPlaceholder = 'start-placeholder', End = 'end', Answer = 'answer', LLM = 'llm', diff --git a/web/i18n/ar-TN/workflow.json b/web/i18n/ar-TN/workflow.json index fcfeba0ccd8..6c876e51149 100644 --- a/web/i18n/ar-TN/workflow.json +++ b/web/i18n/ar-TN/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "حلقة", "blocks.loop-end": "خروج من الحلقة", "blocks.loop-start": "بداية الحلقة", + "blocks.mostCommon": "الأكثر شيوعًا", "blocks.originalStartNode": "عقدة البداية الأصلية", "blocks.parameter-extractor": "مستخرج المعلمات", "blocks.question-classifier": "مصنف الأسئلة", "blocks.start": "إدخال المستخدم", + "blocks.start-placeholder": "بداية workflow", "blocks.template-transform": "قالب", "blocks.tool": "أداة", "blocks.trigger-plugin": "مشغل الإضافة", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "استخدم LLM لاستخراج المعلمات الهيكلية من اللغة الطبيعية لاستدعاء الأدوات أو طلبات HTTP.", "blocksAbout.question-classifier": "تحديد شروط تصنيف أسئلة المستخدم، يمكن لـ LLM تحديد كيفية تقدم المحادثة بناءً على وصف التصنيف", "blocksAbout.start": "تحديد المعلمات الأولية لبدء سير العمل", + "blocksAbout.start-placeholder": "اختر كيف يبدأ هذا workflow", "blocksAbout.template-transform": "تحويل البيانات إلى سلسلة باستخدام بنية قالب Jinja", "blocksAbout.tool": "استخدم الأدوات الخارجية لتوسيع قدرات سير العمل", "blocksAbout.trigger-plugin": "مشغل تكامل تابع لجهة خارجية يبدأ سير العمل من أحداث النظام الأساسي الخارجي", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "نوع الرسالة", "nodes.start.outputVars.query": "إدخال المستخدم", "nodes.start.required": "مطلوب", + "nodes.start.userInputTipDescription": "حدّد المدخلات التي سيتم جمعها من المستخدمين النهائيين عند بدء workflow عند الطلب.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "تصفّح المزيد في Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "ابحث عن المزيد من الأدوات في Marketplace", + "nodes.startPlaceholder.noTriggersFound": "لم يتم العثور على أي مشغلات", + "nodes.startPlaceholder.nodeCollapsedDescription": "انقر لتكوين عقدة البداية", + "nodes.startPlaceholder.nodeDescription": "اختر عقدة بداية من اللوحة اليمنى", + "nodes.startPlaceholder.nodeTitle": "بداية workflow", + "nodes.startPlaceholder.panelDescription": "تحدد عقدة البداية ما الذي يشغّل workflow لديك", + "nodes.startPlaceholder.panelTitle": "اختر عقدة بداية", + "nodes.startPlaceholder.userInputConflictTip": "لا يمكن دمج إدخال المستخدم مع مشغلات أخرى", + "nodes.startPlaceholder.validationRequired": "اختر عقدة بداية أولًا.", "nodes.templateTransform.code": "الكود", "nodes.templateTransform.codeSupportTip": "يدعم Jinja2 فقط", "nodes.templateTransform.inputVars": "متغيرات الإدخال", @@ -1209,9 +1223,11 @@ "tabs.sources": "المصادر", "tabs.start": "البداية", "tabs.startDisabledTip": "تتعارض عقدة المشغل وعقدة إدخال المستخدم.", + "tabs.startDisabledTipLearnMore": "تعرّف على المزيد حول عقد البداية", "tabs.startNotSupportedTip": "علامة التبويب \"ابدأ\" غير مدعومة في المقتطفات.", "tabs.tools": "الأدوات", "tabs.transform": "تحويل", + "tabs.unconfiguredStartDisabledTip": "تمت إضافة عقدة بداية غير مكوّنة إلى اللوحة. أكمل الإعداد قبل المتابعة.", "tabs.usePlugin": "حدد الأداة", "tabs.utilities": "الأدوات المساعدة", "tabs.workflowTool": "سير العمل", diff --git a/web/i18n/de-DE/workflow.json b/web/i18n/de-DE/workflow.json index 7d3bdf3fd22..4cf68da10e3 100644 --- a/web/i18n/de-DE/workflow.json +++ b/web/i18n/de-DE/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Schleife", "blocks.loop-end": "Schleife beenden", "blocks.loop-start": "Schleifenbeginn", + "blocks.mostCommon": "Am häufigsten", "blocks.originalStartNode": "ursprünglicher Startknoten", "blocks.parameter-extractor": "Parameter-Extraktor", "blocks.question-classifier": "Fragenklassifizierer", "blocks.start": "Start", + "blocks.start-placeholder": "Workflow-Start", "blocks.template-transform": "Vorlage", "blocks.tool": "Werkzeug", "blocks.trigger-plugin": "Plugin-Auslöser", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Verwenden Sie LLM, um strukturierte Parameter aus natürlicher Sprache für Werkzeugaufrufe oder HTTP-Anfragen zu extrahieren.", "blocksAbout.question-classifier": "Definieren Sie die Klassifizierungsbedingungen von Benutzerfragen, LLM kann basierend auf der Klassifikationsbeschreibung festlegen, wie die Konversation fortschreitet", "blocksAbout.start": "Definieren Sie die Anfangsparameter zum Starten eines Workflows", + "blocksAbout.start-placeholder": "Wählen Sie aus, wie dieser Workflow startet", "blocksAbout.template-transform": "Daten in Zeichenfolgen mit Jinja-Vorlagensyntax umwandeln", "blocksAbout.tool": "Verwenden Sie externe Tools, um die Workflow-Funktionen zu erweitern", "blocksAbout.trigger-plugin": "Auslöser für die Integration von Drittanbietern, der Workflows anhand von Ereignissen externer Plattformen startet", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "Nachrichtentyp", "nodes.start.outputVars.query": "Benutzereingabe", "nodes.start.required": "erforderlich", + "nodes.start.userInputTipDescription": "Definieren Sie Eingaben, die von Endbenutzern erfasst werden, wenn Ihr Workflow bei Bedarf gestartet wird.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Mehr im Marketplace durchsuchen", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Weitere Tools im Marketplace finden", + "nodes.startPlaceholder.noTriggersFound": "Keine Trigger gefunden", + "nodes.startPlaceholder.nodeCollapsedDescription": "Klicken, um den Startknoten zu konfigurieren", + "nodes.startPlaceholder.nodeDescription": "Wählen Sie im rechten Bereich einen Startknoten aus", + "nodes.startPlaceholder.nodeTitle": "Workflow-Start", + "nodes.startPlaceholder.panelDescription": "Der Startknoten legt fest, wodurch Ihr Workflow ausgeführt wird", + "nodes.startPlaceholder.panelTitle": "Startknoten auswählen", + "nodes.startPlaceholder.userInputConflictTip": "Benutzereingabe kann nicht mit anderen Triggern kombiniert werden", + "nodes.startPlaceholder.validationRequired": "Wählen Sie zuerst einen Startknoten aus.", "nodes.templateTransform.code": "Code", "nodes.templateTransform.codeSupportTip": "Unterstützt nur Jinja2", "nodes.templateTransform.inputVars": "Eingabevariablen", @@ -1209,9 +1223,11 @@ "tabs.sources": "Quellen", "tabs.start": "Start", "tabs.startDisabledTip": "Trigger-Knoten und Benutzereingabeknoten schließen sich gegenseitig aus.", + "tabs.startDisabledTipLearnMore": "Mehr über Startknoten erfahren", "tabs.startNotSupportedTip": "Die Registerkarte „Start“ wird in Snippets nicht unterstützt.", "tabs.tools": "Werkzeuge", "tabs.transform": "Transformieren", + "tabs.unconfiguredStartDisabledTip": "Ein nicht konfigurierter Startknoten wurde zur Arbeitsfläche hinzugefügt. Schließen Sie die Einrichtung ab, bevor Sie fortfahren.", "tabs.usePlugin": "Werkzeug auswählen", "tabs.utilities": "Dienstprogramme", "tabs.workflowTool": "Arbeitsablauf", diff --git a/web/i18n/en-US/workflow.json b/web/i18n/en-US/workflow.json index 52b79654fdc..de2f0d0b805 100644 --- a/web/i18n/en-US/workflow.json +++ b/web/i18n/en-US/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Loop", "blocks.loop-end": "Exit Loop", "blocks.loop-start": "Loop Start", + "blocks.mostCommon": "Most common", "blocks.originalStartNode": "original start node", "blocks.parameter-extractor": "Parameter Extractor", "blocks.question-classifier": "Question Classifier", "blocks.start": "User Input", + "blocks.start-placeholder": "Workflow start", "blocks.template-transform": "Template", "blocks.tool": "Tool", "blocks.trigger-plugin": "Plugin Trigger", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.", "blocksAbout.question-classifier": "Define the classification conditions of user questions, LLM can define how the conversation progresses based on the classification description", "blocksAbout.start": "Define the initial parameters for launching a workflow", + "blocksAbout.start-placeholder": "Choose how this workflow starts", "blocksAbout.template-transform": "Convert data to string using Jinja template syntax", "blocksAbout.tool": "Use external tools to extend workflow capabilities", "blocksAbout.trigger-plugin": "Third-party integration trigger that starts workflows from external platform events", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "message type", "nodes.start.outputVars.query": "User input", "nodes.start.required": "required", + "nodes.start.userInputTipDescription": "Define inputs to collect from end users when your workflow starts on demand.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Browse more in Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Find more tools in Marketplace", + "nodes.startPlaceholder.noTriggersFound": "No triggers were found", + "nodes.startPlaceholder.nodeCollapsedDescription": "Click to configure the start node", + "nodes.startPlaceholder.nodeDescription": "Pick a start node from the right panel", + "nodes.startPlaceholder.nodeTitle": "Workflow start", + "nodes.startPlaceholder.panelDescription": "The start node defines what triggers your workflow to run", + "nodes.startPlaceholder.panelTitle": "Pick a start node", + "nodes.startPlaceholder.userInputConflictTip": "User Input cannot be combined with other triggers", + "nodes.startPlaceholder.validationRequired": "Choose a start node first.", "nodes.templateTransform.code": "Code", "nodes.templateTransform.codeSupportTip": "Only supports Jinja2", "nodes.templateTransform.inputVars": "Input Variables", @@ -1209,9 +1223,11 @@ "tabs.sources": "Sources", "tabs.start": "Start", "tabs.startDisabledTip": "Trigger node and user input node are mutually exclusive.", + "tabs.startDisabledTipLearnMore": "Learn more about start nodes", "tabs.startNotSupportedTip": "The Start tab is not supported in snippets.", "tabs.tools": "Tools", "tabs.transform": "Transform", + "tabs.unconfiguredStartDisabledTip": "An unconfigured start node has been added to canvas. Please complete the setup before continuing.", "tabs.usePlugin": "Select tool", "tabs.utilities": "Utilities", "tabs.workflowTool": "Workflow", diff --git a/web/i18n/es-ES/workflow.json b/web/i18n/es-ES/workflow.json index d205d96e617..c929051f612 100644 --- a/web/i18n/es-ES/workflow.json +++ b/web/i18n/es-ES/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Bucle", "blocks.loop-end": "Salir del bucle", "blocks.loop-start": "Inicio del bucle", + "blocks.mostCommon": "Más común", "blocks.originalStartNode": "nodo inicial original", "blocks.parameter-extractor": "Extractor de parámetros", "blocks.question-classifier": "Clasificador de preguntas", "blocks.start": "Inicio", + "blocks.start-placeholder": "Inicio del workflow", "blocks.template-transform": "Plantilla", "blocks.tool": "Herramienta", "blocks.trigger-plugin": "Disparador de complemento", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Utiliza LLM para extraer parámetros estructurados del lenguaje natural para invocaciones de herramientas o solicitudes HTTP.", "blocksAbout.question-classifier": "Define las condiciones de clasificación de las preguntas de los usuarios, LLM puede definir cómo progresa la conversación en función de la descripción de clasificación", "blocksAbout.start": "Define los parámetros iniciales para iniciar un flujo de trabajo", + "blocksAbout.start-placeholder": "Elige cómo empieza este workflow", "blocksAbout.template-transform": "Convierte datos en una cadena utilizando la sintaxis de plantillas Jinja", "blocksAbout.tool": "Utiliza herramientas externas para ampliar las capacidades del flujo de trabajo", "blocksAbout.trigger-plugin": "Disparador de integración de terceros que inicia flujos de trabajo a partir de eventos de plataformas externas", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "tipo de mensaje", "nodes.start.outputVars.query": "Entrada del usuario", "nodes.start.required": "requerido", + "nodes.start.userInputTipDescription": "Define las entradas que se recopilarán de los usuarios finales cuando tu workflow se inicie bajo demanda.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Explorar más en Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Encontrar más herramientas en Marketplace", + "nodes.startPlaceholder.noTriggersFound": "No se encontraron disparadores", + "nodes.startPlaceholder.nodeCollapsedDescription": "Haz clic para configurar el nodo de inicio", + "nodes.startPlaceholder.nodeDescription": "Elige un nodo de inicio en el panel derecho", + "nodes.startPlaceholder.nodeTitle": "Inicio del workflow", + "nodes.startPlaceholder.panelDescription": "El nodo de inicio define qué activa la ejecución de tu workflow", + "nodes.startPlaceholder.panelTitle": "Elige un nodo de inicio", + "nodes.startPlaceholder.userInputConflictTip": "La entrada de usuario no se puede combinar con otros disparadores", + "nodes.startPlaceholder.validationRequired": "Elige primero un nodo de inicio.", "nodes.templateTransform.code": "Código", "nodes.templateTransform.codeSupportTip": "Solo admite Jinja2", "nodes.templateTransform.inputVars": "Variables de entrada", @@ -1209,9 +1223,11 @@ "tabs.sources": "Fuentes", "tabs.start": "Iniciar", "tabs.startDisabledTip": "El nodo activador y el nodo de entrada del usuario son mutuamente excluyentes.", + "tabs.startDisabledTipLearnMore": "Más información sobre los nodos de inicio", "tabs.startNotSupportedTip": "La pestaña Inicio no se admite en fragmentos.", "tabs.tools": "Herramientas", "tabs.transform": "Transformar", + "tabs.unconfiguredStartDisabledTip": "Se ha añadido al lienzo un nodo de inicio sin configurar. Completa la configuración antes de continuar.", "tabs.usePlugin": "Seleccionar herramienta", "tabs.utilities": "Utilidades", "tabs.workflowTool": "Flujo de trabajo", diff --git a/web/i18n/fa-IR/workflow.json b/web/i18n/fa-IR/workflow.json index 9fbe2d7b3cc..5e9767e1cf0 100644 --- a/web/i18n/fa-IR/workflow.json +++ b/web/i18n/fa-IR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "حلقه", "blocks.loop-end": "خروج از حلقه", "blocks.loop-start": "شروع حلقه", + "blocks.mostCommon": "رایج‌ترین", "blocks.originalStartNode": "گره شروع اصلی", "blocks.parameter-extractor": "استخراج‌کننده پارامتر", "blocks.question-classifier": "دسته‌بندی‌کننده سؤال", "blocks.start": "شروع", + "blocks.start-placeholder": "شروع workflow", "blocks.template-transform": "مبدل الگو", "blocks.tool": "ابزار", "blocks.trigger-plugin": "راه‌انداز پلاگین", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "استخراج پارامترهای ساختاریافته از زبان طبیعی توسط مدل زبانی بزرگ برای فراخوانی ابزارها یا درخواست‌های HTTP", "blocksAbout.question-classifier": "تعریف شرایط دسته‌بندی سؤالات کاربر؛ مدل زبانی بزرگ بر اساس توضیحات دسته‌بندی، مسیر مکالمه را تعیین می‌کند", "blocksAbout.start": "تعریف پارامترهای اولیه برای آغاز گردش کار", + "blocksAbout.start-placeholder": "نحوه شروع این workflow را انتخاب کنید", "blocksAbout.template-transform": "تبدیل داده‌ها به رشته با نحو الگوی Jinja", "blocksAbout.tool": "استفاده از ابزارهای خارجی برای گسترش قابلیت‌های گردش کار", "blocksAbout.trigger-plugin": "یکپارچه‌سازی با سرویس‌های ثالث برای آغاز گردش کار از رویدادهای پلتفرم خارجی", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "نوع پیام", "nodes.start.outputVars.query": "ورودی کاربر", "nodes.start.required": "الزامی", + "nodes.start.userInputTipDescription": "ورودی‌هایی را تعریف کنید که هنگام شروع workflow به‌صورت درخواستی از کاربران نهایی جمع‌آوری می‌شوند.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "موارد بیشتری را در Marketplace مرور کنید", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "ابزارهای بیشتری را در Marketplace پیدا کنید", + "nodes.startPlaceholder.noTriggersFound": "هیچ تریگری یافت نشد", + "nodes.startPlaceholder.nodeCollapsedDescription": "برای پیکربندی گره شروع کلیک کنید", + "nodes.startPlaceholder.nodeDescription": "یک گره شروع را از پنل سمت راست انتخاب کنید", + "nodes.startPlaceholder.nodeTitle": "شروع workflow", + "nodes.startPlaceholder.panelDescription": "گره شروع مشخص می‌کند چه چیزی اجرای workflow شما را فعال می‌کند", + "nodes.startPlaceholder.panelTitle": "یک گره شروع انتخاب کنید", + "nodes.startPlaceholder.userInputConflictTip": "ورودی کاربر نمی‌تواند با تریگرهای دیگر ترکیب شود", + "nodes.startPlaceholder.validationRequired": "ابتدا یک گره شروع انتخاب کنید.", "nodes.templateTransform.code": "کد", "nodes.templateTransform.codeSupportTip": "فقط از Jinja2 پشتیبانی می‌شود", "nodes.templateTransform.inputVars": "متغیرهای ورودی", @@ -1209,9 +1223,11 @@ "tabs.sources": "منابع", "tabs.start": "شروع", "tabs.startDisabledTip": "گره تریگر و گره ورودی کاربر نمی‌توانند همزمان فعال باشند.", + "tabs.startDisabledTipLearnMore": "درباره گره‌های شروع بیشتر بدانید", "tabs.startNotSupportedTip": "تب Start در قطعه‌ها پشتیبانی نمی‌شود.", "tabs.tools": "ابزارها", "tabs.transform": "تبدیل", + "tabs.unconfiguredStartDisabledTip": "یک گره شروع پیکربندی‌نشده به بوم اضافه شده است. پیش از ادامه، تنظیمات را کامل کنید.", "tabs.usePlugin": "انتخاب ابزار", "tabs.utilities": "ابزارهای کاربردی", "tabs.workflowTool": "گردش کار", diff --git a/web/i18n/fr-FR/workflow.json b/web/i18n/fr-FR/workflow.json index 8cb68762575..fa7d3a9302a 100644 --- a/web/i18n/fr-FR/workflow.json +++ b/web/i18n/fr-FR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Boucle", "blocks.loop-end": "Sortir de la boucle", "blocks.loop-start": "Début de boucle", + "blocks.mostCommon": "Le plus courant", "blocks.originalStartNode": "nœud de départ original", "blocks.parameter-extractor": "Extracteur de paramètres", "blocks.question-classifier": "Classificateur de questions", "blocks.start": "Début", + "blocks.start-placeholder": "Début du workflow", "blocks.template-transform": "Modèle", "blocks.tool": "Outil", "blocks.trigger-plugin": "Déclencheur de plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Utiliser LLM pour extraire des paramètres structurés du langage naturel pour les invocations d'outils ou les requêtes HTTP.", "blocksAbout.question-classifier": "Définir les conditions de classification des questions des utilisateurs, LLM peut définir comment la conversation progresse en fonction de la description de la classification", "blocksAbout.start": "Définir les paramètres initiaux pour lancer un flux de travail", + "blocksAbout.start-placeholder": "Choisissez comment ce workflow démarre", "blocksAbout.template-transform": "Convertir les données en chaîne en utilisant la syntaxe du template Jinja", "blocksAbout.tool": "Utilisez des outils externes pour étendre les capacités du flux de travail", "blocksAbout.trigger-plugin": "Déclencheur d’intégration tierce qui démarre des flux de travail à partir d’événements d’une plateforme externe", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "type de message", "nodes.start.outputVars.query": "Saisie utilisateur", "nodes.start.required": "requis", + "nodes.start.userInputTipDescription": "Définissez les entrées à collecter auprès des utilisateurs finaux lorsque votre workflow démarre à la demande.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Parcourir plus sur le Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Trouver plus d’outils sur le Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Aucun déclencheur trouvé", + "nodes.startPlaceholder.nodeCollapsedDescription": "Cliquez pour configurer le nœud de départ", + "nodes.startPlaceholder.nodeDescription": "Choisissez un nœud de départ dans le panneau de droite", + "nodes.startPlaceholder.nodeTitle": "Début du workflow", + "nodes.startPlaceholder.panelDescription": "Le nœud de départ définit ce qui déclenche l’exécution de votre workflow", + "nodes.startPlaceholder.panelTitle": "Choisissez un nœud de départ", + "nodes.startPlaceholder.userInputConflictTip": "L’entrée utilisateur ne peut pas être combinée avec d’autres déclencheurs", + "nodes.startPlaceholder.validationRequired": "Choisissez d’abord un nœud de départ.", "nodes.templateTransform.code": "Code", "nodes.templateTransform.codeSupportTip": "Prend en charge uniquement Jinja2", "nodes.templateTransform.inputVars": "Variables de saisie", @@ -1209,9 +1223,11 @@ "tabs.sources": "Sources", "tabs.start": "Démarrer", "tabs.startDisabledTip": "Le nœud de déclenchement et le nœud d'entrée utilisateur sont mutuellement exclusifs.", + "tabs.startDisabledTipLearnMore": "En savoir plus sur les nœuds de départ", "tabs.startNotSupportedTip": "L'onglet Démarrer n'est pas pris en charge dans les extraits de code.", "tabs.tools": "Outils", "tabs.transform": "Transformer", + "tabs.unconfiguredStartDisabledTip": "Un nœud de départ non configuré a été ajouté au canevas. Terminez la configuration avant de continuer.", "tabs.usePlugin": "Sélectionner l'outil", "tabs.utilities": "Utilitaires", "tabs.workflowTool": "Flux de travail", diff --git a/web/i18n/hi-IN/workflow.json b/web/i18n/hi-IN/workflow.json index 09565b9a974..d08b569b6ae 100644 --- a/web/i18n/hi-IN/workflow.json +++ b/web/i18n/hi-IN/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "लूप", "blocks.loop-end": "लूप से बाहर निकलें", "blocks.loop-start": "लूप प्रारंभ", + "blocks.mostCommon": "सबसे आम", "blocks.originalStartNode": "मूल प्रारंभ नोड", "blocks.parameter-extractor": "पैरामीटर निष्कर्षक", "blocks.question-classifier": "प्रश्न वर्गीकरण", "blocks.start": "प्रारंभ", + "blocks.start-placeholder": "workflow प्रारंभ", "blocks.template-transform": "टेम्पलेट", "blocks.tool": "उपकरण", "blocks.trigger-plugin": "प्लगइन ट्रिगर", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "टूल आमंत्रणों या HTTP अनुरोधों के लिए प्राकृतिक भाषा से संरचित पैरामीटर निकालने के लिए LLM का उपयोग करें।", "blocksAbout.question-classifier": "उपयोगकर्ता प्रश्नों की वर्गीकरण शर्तों को परिभाषित करें, LLM वर्गीकरण विवरण के आधार पर संवाद कैसे आगे बढ़ता है, इसे परिभाषित कर सकता है", "blocksAbout.start": "वर्कफ़्लो लॉन्च करने के लिए प्रारंभिक पैरामीटर को परिभाषित करें", + "blocksAbout.start-placeholder": "चुनें कि यह workflow कैसे शुरू होता है", "blocksAbout.template-transform": "Jinja टेम्पलेट सिंटैक्स का उपयोग करके डेटा को स्ट्रिंग में परिवर्तित करें", "blocksAbout.tool": "कार्यप्रवाह क्षमताओं को बढ़ाने के लिए बाहरी उपकरणों का उपयोग करें", "blocksAbout.trigger-plugin": "थर्ड-पार्टी इंटीग्रेशन ट्रिगर जो बाहरी प्लेटफ़ॉर्म घटनाओं से वर्कफ़्लो शुरू करता है", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "संदेश प्रकार", "nodes.start.outputVars.query": "यूजर इनपुट", "nodes.start.required": "आवश्यक", + "nodes.start.userInputTipDescription": "जब आपका workflow मांग पर शुरू होता है, तब अंतिम उपयोगकर्ताओं से एकत्र किए जाने वाले इनपुट परिभाषित करें।", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace में और ब्राउज़ करें", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace में और टूल खोजें", + "nodes.startPlaceholder.noTriggersFound": "कोई ट्रिगर नहीं मिला", + "nodes.startPlaceholder.nodeCollapsedDescription": "प्रारंभ नोड कॉन्फ़िगर करने के लिए क्लिक करें", + "nodes.startPlaceholder.nodeDescription": "दाएँ पैनल से एक प्रारंभ नोड चुनें", + "nodes.startPlaceholder.nodeTitle": "workflow प्रारंभ", + "nodes.startPlaceholder.panelDescription": "प्रारंभ नोड यह परिभाषित करता है कि आपका workflow किससे चलेगा", + "nodes.startPlaceholder.panelTitle": "एक प्रारंभ नोड चुनें", + "nodes.startPlaceholder.userInputConflictTip": "उपयोगकर्ता इनपुट को अन्य ट्रिगर के साथ संयोजित नहीं किया जा सकता", + "nodes.startPlaceholder.validationRequired": "पहले एक प्रारंभ नोड चुनें।", "nodes.templateTransform.code": "कोड", "nodes.templateTransform.codeSupportTip": "केवल Jinja2 का समर्थन करता है", "nodes.templateTransform.inputVars": "इनपुट वेरिएबल्स", @@ -1209,9 +1223,11 @@ "tabs.sources": "स्रोत", "tabs.start": "शुरू करें", "tabs.startDisabledTip": "ट्रिगर नोड और उपयोगकर्ता इनपुट नोड परस्पर विशेष हैं।", + "tabs.startDisabledTipLearnMore": "प्रारंभ नोड्स के बारे में और जानें", "tabs.startNotSupportedTip": "स्निपेट्स में स्टार्ट टैब समर्थित नहीं है।", "tabs.tools": "टूल्स", "tabs.transform": "परिवर्तन", + "tabs.unconfiguredStartDisabledTip": "कैनवास में एक असंरचित प्रारंभ नोड जोड़ा गया है। जारी रखने से पहले सेटअप पूरा करें।", "tabs.usePlugin": "उपकरण चुनें", "tabs.utilities": "उपयोगिताएं", "tabs.workflowTool": "कार्यप्रवाह", diff --git a/web/i18n/id-ID/workflow.json b/web/i18n/id-ID/workflow.json index 99cd1968112..bf7e1fd4c8c 100644 --- a/web/i18n/id-ID/workflow.json +++ b/web/i18n/id-ID/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Perulangan", "blocks.loop-end": "Keluar Loop", "blocks.loop-start": "Mulai Loop", + "blocks.mostCommon": "Paling umum", "blocks.originalStartNode": "node awal asli", "blocks.parameter-extractor": "Ekstraktor Parameter", "blocks.question-classifier": "Pengklasifikasi Pertanyaan", "blocks.start": "Mulai", + "blocks.start-placeholder": "Awal workflow", "blocks.template-transform": "Templat", "blocks.tool": "Alat", "blocks.trigger-plugin": "Pemicu Plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Gunakan LLM untuk mengekstrak parameter terstruktur dari bahasa alami untuk pemanggilan alat atau permintaan HTTP.", "blocksAbout.question-classifier": "Tentukan kondisi klasifikasi pertanyaan pengguna, LLM dapat menentukan bagaimana percakapan berlangsung berdasarkan deskripsi klasifikasi", "blocksAbout.start": "Menentukan parameter awal untuk meluncurkan alur kerja", + "blocksAbout.start-placeholder": "Pilih bagaimana workflow ini dimulai", "blocksAbout.template-transform": "Mengonversi data menjadi string menggunakan sintaks templat Jinja", "blocksAbout.tool": "Gunakan alat eksternal untuk memperluas kemampuan alur kerja", "blocksAbout.trigger-plugin": "Pemicu integrasi pihak ketiga yang memulai alur kerja dari kejadian platform eksternal", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "Jenis pesan", "nodes.start.outputVars.query": "Masukan pengguna", "nodes.start.required": "Diperlukan", + "nodes.start.userInputTipDescription": "Tentukan input yang akan dikumpulkan dari pengguna akhir saat workflow Anda dimulai sesuai permintaan.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Jelajahi lebih banyak di Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Temukan lebih banyak alat di Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Tidak ada pemicu yang ditemukan", + "nodes.startPlaceholder.nodeCollapsedDescription": "Klik untuk mengonfigurasi node awal", + "nodes.startPlaceholder.nodeDescription": "Pilih node awal dari panel kanan", + "nodes.startPlaceholder.nodeTitle": "Awal workflow", + "nodes.startPlaceholder.panelDescription": "Node awal menentukan apa yang memicu workflow Anda berjalan", + "nodes.startPlaceholder.panelTitle": "Pilih node awal", + "nodes.startPlaceholder.userInputConflictTip": "Input pengguna tidak dapat digabungkan dengan pemicu lain", + "nodes.startPlaceholder.validationRequired": "Pilih node awal terlebih dahulu.", "nodes.templateTransform.code": "Kode", "nodes.templateTransform.codeSupportTip": "Hanya mendukung Jinja2", "nodes.templateTransform.inputVars": "Variabel Masukan", @@ -1209,9 +1223,11 @@ "tabs.sources": "Sumber", "tabs.start": "Mulai", "tabs.startDisabledTip": "Node pemicu dan node input pengguna saling eksklusif.", + "tabs.startDisabledTipLearnMore": "Pelajari lebih lanjut tentang node awal", "tabs.startNotSupportedTip": "Tab Mulai tidak didukung dalam cuplikan.", "tabs.tools": "Perkakas", "tabs.transform": "Mengubah", + "tabs.unconfiguredStartDisabledTip": "Node awal yang belum dikonfigurasi telah ditambahkan ke kanvas. Selesaikan penyiapan sebelum melanjutkan.", "tabs.usePlugin": "Pilih alat", "tabs.utilities": "Utilitas", "tabs.workflowTool": "Alur Kerja", diff --git a/web/i18n/it-IT/workflow.json b/web/i18n/it-IT/workflow.json index 853ceb3dfa5..f8b36fdefc3 100644 --- a/web/i18n/it-IT/workflow.json +++ b/web/i18n/it-IT/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Anello", "blocks.loop-end": "Uscire dal ciclo", "blocks.loop-start": "Inizio ciclo", + "blocks.mostCommon": "Più comune", "blocks.originalStartNode": "nodo iniziale originale", "blocks.parameter-extractor": "Estrattore Parametri", "blocks.question-classifier": "Classificatore Domande", "blocks.start": "Inizio", + "blocks.start-placeholder": "Avvio del workflow", "blocks.template-transform": "Template", "blocks.tool": "Strumento", "blocks.trigger-plugin": "Attivatore del plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Usa LLM per estrarre parametri strutturati dal linguaggio naturale per invocazioni di strumenti o richieste HTTP.", "blocksAbout.question-classifier": "Definisci le condizioni di classificazione delle domande dell'utente, LLM può definire come prosegue la conversazione in base alla descrizione della classificazione", "blocksAbout.start": "Definisci i parametri iniziali per l'avvio di un flusso di lavoro", + "blocksAbout.start-placeholder": "Scegli come inizia questo workflow", "blocksAbout.template-transform": "Converti i dati in stringa usando la sintassi del template Jinja", "blocksAbout.tool": "Usa strumenti esterni per estendere le capacità del flusso di lavoro", "blocksAbout.trigger-plugin": "Trigger di integrazione di terze parti che avvia flussi di lavoro da eventi di piattaforme esterne", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "tipo di messaggio", "nodes.start.outputVars.query": "Input Utente", "nodes.start.required": "richiesto", + "nodes.start.userInputTipDescription": "Definisci gli input da raccogliere dagli utenti finali quando il workflow viene avviato su richiesta.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Sfoglia altro nel Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Trova altri strumenti nel Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Nessun trigger trovato", + "nodes.startPlaceholder.nodeCollapsedDescription": "Fai clic per configurare il nodo iniziale", + "nodes.startPlaceholder.nodeDescription": "Scegli un nodo iniziale dal pannello a destra", + "nodes.startPlaceholder.nodeTitle": "Avvio del workflow", + "nodes.startPlaceholder.panelDescription": "Il nodo iniziale definisce cosa attiva l’esecuzione del workflow", + "nodes.startPlaceholder.panelTitle": "Scegli un nodo iniziale", + "nodes.startPlaceholder.userInputConflictTip": "L’input utente non può essere combinato con altri trigger", + "nodes.startPlaceholder.validationRequired": "Scegli prima un nodo iniziale.", "nodes.templateTransform.code": "Codice", "nodes.templateTransform.codeSupportTip": "Supporta solo Jinja2", "nodes.templateTransform.inputVars": "Variabili di Input", @@ -1209,9 +1223,11 @@ "tabs.sources": "Fonti", "tabs.start": "Inizia", "tabs.startDisabledTip": "Il nodo di attivazione e il nodo di input utente sono mutualmente esclusivi.", + "tabs.startDisabledTipLearnMore": "Scopri di più sui nodi iniziali", "tabs.startNotSupportedTip": "La scheda Start non è supportata negli snippet.", "tabs.tools": "Strumenti", "tabs.transform": "Trasforma", + "tabs.unconfiguredStartDisabledTip": "Un nodo iniziale non configurato è stato aggiunto alla tela. Completa la configurazione prima di continuare.", "tabs.usePlugin": "Strumento di selezione", "tabs.utilities": "Utility", "tabs.workflowTool": "Flusso di lavoro", diff --git a/web/i18n/ja-JP/workflow.json b/web/i18n/ja-JP/workflow.json index 938d1b1567e..ae7b5f1dc31 100644 --- a/web/i18n/ja-JP/workflow.json +++ b/web/i18n/ja-JP/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "ループ", "blocks.loop-end": "ループ完了", "blocks.loop-start": "ループ開始", + "blocks.mostCommon": "最も一般的", "blocks.originalStartNode": "元の開始ノード", "blocks.parameter-extractor": "パラメータ抽出", "blocks.question-classifier": "質問分類器", "blocks.start": "ユーザー入力", + "blocks.start-placeholder": "ワークフロー開始", "blocks.template-transform": "テンプレート", "blocks.tool": "ツール", "blocks.trigger-plugin": "プラグイントリガー", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "自然言語から構造化パラメータを抽出し、後続処理で利用します。", "blocksAbout.question-classifier": "質問の分類条件を定義し、LLM が分類に基づいて対話フローを制御します。", "blocksAbout.start": "ワークフロー開始時の初期パラメータを定義します。", + "blocksAbout.start-placeholder": "このワークフローの開始方法を選択します", "blocksAbout.template-transform": "Jinja テンプレート構文でデータを文字列に変換します。", "blocksAbout.tool": "外部ツールを使用してワークフローの機能を拡張する", "blocksAbout.trigger-plugin": "サードパーティ統合トリガー、外部プラットフォームのイベントによってワークフローを開始します", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "メッセージ種別", "nodes.start.outputVars.query": "ユーザー入力", "nodes.start.required": "必須", + "nodes.start.userInputTipDescription": "ワークフローがオンデマンドで開始されるときにエンドユーザーから収集する入力を定義します。", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace でもっと探す", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace でさらにツールを探す", + "nodes.startPlaceholder.noTriggersFound": "トリガーが見つかりませんでした", + "nodes.startPlaceholder.nodeCollapsedDescription": "クリックして開始ノードを設定", + "nodes.startPlaceholder.nodeDescription": "右側のパネルから開始ノードを選択", + "nodes.startPlaceholder.nodeTitle": "ワークフロー開始", + "nodes.startPlaceholder.panelDescription": "開始ノードはワークフローを実行するトリガーを定義します", + "nodes.startPlaceholder.panelTitle": "開始ノードを選択", + "nodes.startPlaceholder.userInputConflictTip": "ユーザー入力は他のトリガーと組み合わせることはできません", + "nodes.startPlaceholder.validationRequired": "最初に開始ノードを選択してください。", "nodes.templateTransform.code": "コード", "nodes.templateTransform.codeSupportTip": "Jinja2 のみをサポートしています", "nodes.templateTransform.inputVars": "入力変数", @@ -1209,9 +1223,11 @@ "tabs.sources": "ソース", "tabs.start": "始める", "tabs.startDisabledTip": "トリガーノードとユーザー入力ノードは互いに排他です。", + "tabs.startDisabledTipLearnMore": "開始ノードの詳細を見る", "tabs.startNotSupportedTip": "[スタート] タブはスニペットではサポートされていません。", "tabs.tools": "ツール", "tabs.transform": "変換", + "tabs.unconfiguredStartDisabledTip": "未設定の開始ノードがキャンバスに追加されています。続行する前に設定を完了してください。", "tabs.usePlugin": "ツールを選択", "tabs.utilities": "ツール", "tabs.workflowTool": "ワークフロー", diff --git a/web/i18n/ko-KR/workflow.json b/web/i18n/ko-KR/workflow.json index 44373a0ee4c..d2547712244 100644 --- a/web/i18n/ko-KR/workflow.json +++ b/web/i18n/ko-KR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "루프", "blocks.loop-end": "루프 종료", "blocks.loop-start": "루프 시작", + "blocks.mostCommon": "가장 일반적", "blocks.originalStartNode": "원래 시작 노드", "blocks.parameter-extractor": "매개변수 추출기", "blocks.question-classifier": "질문 분류기", "blocks.start": "시작", + "blocks.start-placeholder": "워크플로 시작", "blocks.template-transform": "템플릿", "blocks.tool": "도구", "blocks.trigger-plugin": "플러그인 트리거", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "도구 호출 또는 HTTP 요청을 위해 자연어에서 구조화된 매개변수를 추출하기 위해 LLM 을 사용합니다.", "blocksAbout.question-classifier": "사용자 질문의 분류 조건을 정의합니다. LLM 은 분류 설명을 기반으로 대화의 진행 방식을 정의할 수 있습니다", "blocksAbout.start": "워크플로우를 시작하기 위한 초기 매개변수를 정의합니다", + "blocksAbout.start-placeholder": "이 워크플로가 시작되는 방식을 선택하세요", "blocksAbout.template-transform": "Jinja 템플릿 구문을 사용하여 데이터를 문자열로 변환합니다", "blocksAbout.tool": "외부 도구를 사용하여 워크플로우 기능을 확장하세요", "blocksAbout.trigger-plugin": "외부 플랫폼 이벤트로 워크플로를 시작하는 타사 통합 트리거", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "메시지 유형", "nodes.start.outputVars.query": "사용자 입력", "nodes.start.required": "필수", + "nodes.start.userInputTipDescription": "워크플로가 필요할 때 시작될 때 최종 사용자에게서 수집할 입력을 정의합니다.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace 에서 더 찾아보기", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace 에서 더 많은 도구 찾기", + "nodes.startPlaceholder.noTriggersFound": "트리거를 찾을 수 없습니다", + "nodes.startPlaceholder.nodeCollapsedDescription": "시작 노드를 구성하려면 클릭하세요", + "nodes.startPlaceholder.nodeDescription": "오른쪽 패널에서 시작 노드를 선택하세요", + "nodes.startPlaceholder.nodeTitle": "워크플로 시작", + "nodes.startPlaceholder.panelDescription": "시작 노드는 워크플로 실행을 트리거하는 항목을 정의합니다", + "nodes.startPlaceholder.panelTitle": "시작 노드 선택", + "nodes.startPlaceholder.userInputConflictTip": "사용자 입력은 다른 트리거와 함께 사용할 수 없습니다", + "nodes.startPlaceholder.validationRequired": "먼저 시작 노드를 선택하세요.", "nodes.templateTransform.code": "코드", "nodes.templateTransform.codeSupportTip": "Jinja2 만 지원합니다", "nodes.templateTransform.inputVars": "입력 변수", @@ -1209,9 +1223,11 @@ "tabs.sources": "소스", "tabs.start": "시작", "tabs.startDisabledTip": "트리거 노드와 사용자 입력 노드는 상호 배타적입니다.", + "tabs.startDisabledTipLearnMore": "시작 노드 자세히 알아보기", "tabs.startNotSupportedTip": "시작 탭은 조각에서 지원되지 않습니다.", "tabs.tools": "도구", "tabs.transform": "변환", + "tabs.unconfiguredStartDisabledTip": "구성되지 않은 시작 노드가 캔버스에 추가되었습니다. 계속하기 전에 설정을 완료하세요.", "tabs.usePlugin": "도구 선택", "tabs.utilities": "유틸리티", "tabs.workflowTool": "워크플로우", diff --git a/web/i18n/nl-NL/workflow.json b/web/i18n/nl-NL/workflow.json index 2dcf6ade439..f6760bf68bc 100644 --- a/web/i18n/nl-NL/workflow.json +++ b/web/i18n/nl-NL/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Loop", "blocks.loop-end": "Exit Loop", "blocks.loop-start": "Loop Start", + "blocks.mostCommon": "Meest gebruikt", "blocks.originalStartNode": "original start node", "blocks.parameter-extractor": "Parameter Extractor", "blocks.question-classifier": "Question Classifier", "blocks.start": "User Input", + "blocks.start-placeholder": "Workflow-start", "blocks.template-transform": "Template", "blocks.tool": "Tool", "blocks.trigger-plugin": "Plugin Trigger", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Use LLM to extract structured parameters from natural language for tool invocations or HTTP requests.", "blocksAbout.question-classifier": "Define the classification conditions of user questions, LLM can define how the conversation progresses based on the classification description", "blocksAbout.start": "Define the initial parameters for launching a workflow", + "blocksAbout.start-placeholder": "Kies hoe deze workflow start", "blocksAbout.template-transform": "Convert data to string using Jinja template syntax", "blocksAbout.tool": "Use external tools to extend workflow capabilities", "blocksAbout.trigger-plugin": "Third-party integration trigger that starts workflows from external platform events", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "message type", "nodes.start.outputVars.query": "User input", "nodes.start.required": "required", + "nodes.start.userInputTipDescription": "Definieer invoer die bij eindgebruikers wordt verzameld wanneer je workflow op aanvraag start.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Meer bekijken in Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Meer tools vinden in Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Geen triggers gevonden", + "nodes.startPlaceholder.nodeCollapsedDescription": "Klik om de startnode te configureren", + "nodes.startPlaceholder.nodeDescription": "Kies een startnode in het rechterpaneel", + "nodes.startPlaceholder.nodeTitle": "Workflow-start", + "nodes.startPlaceholder.panelDescription": "De startnode bepaalt wat je workflow activeert", + "nodes.startPlaceholder.panelTitle": "Kies een startnode", + "nodes.startPlaceholder.userInputConflictTip": "Gebruikersinvoer kan niet worden gecombineerd met andere triggers", + "nodes.startPlaceholder.validationRequired": "Kies eerst een startnode.", "nodes.templateTransform.code": "Code", "nodes.templateTransform.codeSupportTip": "Only supports Jinja2", "nodes.templateTransform.inputVars": "Input Variables", @@ -1209,9 +1223,11 @@ "tabs.sources": "Sources", "tabs.start": "Start", "tabs.startDisabledTip": "Trigger node and user input node are mutually exclusive.", + "tabs.startDisabledTipLearnMore": "Meer informatie over startnodes", "tabs.startNotSupportedTip": "Het tabblad Start wordt niet ondersteund in fragmenten.", "tabs.tools": "Tools", "tabs.transform": "Transform", + "tabs.unconfiguredStartDisabledTip": "Er is een niet-geconfigureerde startnode aan het canvas toegevoegd. Voltooi de configuratie voordat je doorgaat.", "tabs.usePlugin": "Select tool", "tabs.utilities": "Utilities", "tabs.workflowTool": "Workflow", diff --git a/web/i18n/pl-PL/workflow.json b/web/i18n/pl-PL/workflow.json index 36aee9c524a..46395005f99 100644 --- a/web/i18n/pl-PL/workflow.json +++ b/web/i18n/pl-PL/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Pętla", "blocks.loop-end": "Wyjście z pętli", "blocks.loop-start": "Początek pętli", + "blocks.mostCommon": "Najczęstsze", "blocks.originalStartNode": "oryginalny węzeł początkowy", "blocks.parameter-extractor": "Ekstraktor parametrów", "blocks.question-classifier": "Klasyfikator pytań", "blocks.start": "Start", + "blocks.start-placeholder": "Start workflow", "blocks.template-transform": "Szablon", "blocks.tool": "Narzędzie", "blocks.trigger-plugin": "Wyzwalacz wtyczki", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Użyj LLM do wyodrębnienia strukturalnych parametrów z języka naturalnego do wywołań narzędzi lub żądań HTTP.", "blocksAbout.question-classifier": "Zdefiniuj warunki klasyfikacji pytań użytkowników, LLM może definiować, jak rozmowa postępuje na podstawie opisu klasyfikacji", "blocksAbout.start": "Zdefiniuj początkowe parametry uruchamiania przepływu pracy", + "blocksAbout.start-placeholder": "Wybierz, jak rozpoczyna się ten workflow", "blocksAbout.template-transform": "Konwertuj dane na ciąg znaków przy użyciu składni szablonu Jinja", "blocksAbout.tool": "Używaj zewnętrznych narzędzi, aby rozszerzyć możliwości przepływu pracy", "blocksAbout.trigger-plugin": "Wyzwalacz integracji zewnętrznej, który uruchamia przepływy pracy na podstawie zdarzeń z platformy zewnętrznej", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "typ wiadomości", "nodes.start.outputVars.query": "Wprowadzenie użytkownika", "nodes.start.required": "wymagane", + "nodes.start.userInputTipDescription": "Określ dane wejściowe zbierane od użytkowników końcowych, gdy workflow uruchamia się na żądanie.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Przeglądaj więcej w Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Znajdź więcej narzędzi w Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Nie znaleziono wyzwalaczy", + "nodes.startPlaceholder.nodeCollapsedDescription": "Kliknij, aby skonfigurować węzeł startowy", + "nodes.startPlaceholder.nodeDescription": "Wybierz węzeł startowy z prawego panelu", + "nodes.startPlaceholder.nodeTitle": "Start workflow", + "nodes.startPlaceholder.panelDescription": "Węzeł startowy określa, co uruchamia workflow", + "nodes.startPlaceholder.panelTitle": "Wybierz węzeł startowy", + "nodes.startPlaceholder.userInputConflictTip": "Danych wejściowych użytkownika nie można łączyć z innymi wyzwalaczami", + "nodes.startPlaceholder.validationRequired": "Najpierw wybierz węzeł startowy.", "nodes.templateTransform.code": "Kod", "nodes.templateTransform.codeSupportTip": "Obsługuje tylko Jinja2", "nodes.templateTransform.inputVars": "Zmienne wejściowe", @@ -1209,9 +1223,11 @@ "tabs.sources": "Źródeł", "tabs.start": "Start", "tabs.startDisabledTip": "Węzeł wyzwalacza i węzeł wprowadzania danych przez użytkownika wzajemnie się wykluczają.", + "tabs.startDisabledTipLearnMore": "Dowiedz się więcej o węzłach startowych", "tabs.startNotSupportedTip": "Karta Start nie jest obsługiwana we fragmentach.", "tabs.tools": "Narzędzia", "tabs.transform": "Transformacja", + "tabs.unconfiguredStartDisabledTip": "Do obszaru roboczego dodano nieskonfigurowany węzeł startowy. Przed kontynuowaniem dokończ konfigurację.", "tabs.usePlugin": "Wybierz narzędzie", "tabs.utilities": "Narzędzia pomocnicze", "tabs.workflowTool": "Przepływ pracy", diff --git a/web/i18n/pt-BR/workflow.json b/web/i18n/pt-BR/workflow.json index 02de8500543..db4d85ce17a 100644 --- a/web/i18n/pt-BR/workflow.json +++ b/web/i18n/pt-BR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Laço", "blocks.loop-end": "Sair do Loop", "blocks.loop-start": "Início do Loop", + "blocks.mostCommon": "Mais comum", "blocks.originalStartNode": "nó inicial original", "blocks.parameter-extractor": "Extrator de parâmetros", "blocks.question-classifier": "Classificador de perguntas", "blocks.start": "Iniciar", + "blocks.start-placeholder": "Início do workflow", "blocks.template-transform": "Modelo", "blocks.tool": "Ferramenta", "blocks.trigger-plugin": "Acionador de Plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Use LLM para extrair parâmetros estruturados da linguagem natural para invocações de ferramentas ou requisições HTTP.", "blocksAbout.question-classifier": "Definir as condições de classificação das perguntas dos usuários, LLM pode definir como a conversa progride com base na descrição da classificação", "blocksAbout.start": "Definir os parâmetros iniciais para iniciar um fluxo de trabalho", + "blocksAbout.start-placeholder": "Escolha como este workflow começa", "blocksAbout.template-transform": "Converter dados em string usando a sintaxe de template Jinja", "blocksAbout.tool": "Use ferramentas externas para ampliar as capacidades do fluxo de trabalho", "blocksAbout.trigger-plugin": "Gatilho de integração de terceiros que inicia fluxos de trabalho a partir de eventos de plataformas externas", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "tipo de mensagem", "nodes.start.outputVars.query": "Entrada do usuário", "nodes.start.required": "requerido", + "nodes.start.userInputTipDescription": "Defina entradas para coletar dos usuários finais quando seu workflow iniciar sob demanda.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Procurar mais no Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Encontrar mais ferramentas no Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Nenhum gatilho foi encontrado", + "nodes.startPlaceholder.nodeCollapsedDescription": "Clique para configurar o nó inicial", + "nodes.startPlaceholder.nodeDescription": "Escolha um nó inicial no painel à direita", + "nodes.startPlaceholder.nodeTitle": "Início do workflow", + "nodes.startPlaceholder.panelDescription": "O nó inicial define o que aciona a execução do seu workflow", + "nodes.startPlaceholder.panelTitle": "Escolha um nó inicial", + "nodes.startPlaceholder.userInputConflictTip": "Entrada do usuário não pode ser combinada com outros gatilhos", + "nodes.startPlaceholder.validationRequired": "Escolha primeiro um nó inicial.", "nodes.templateTransform.code": "Código", "nodes.templateTransform.codeSupportTip": "Suporta apenas Jinja2", "nodes.templateTransform.inputVars": "Variáveis de entrada", @@ -1209,9 +1223,11 @@ "tabs.sources": "Fontes", "tabs.start": "Começar", "tabs.startDisabledTip": "O nó de gatilho e o nó de entrada do usuário são mutuamente exclusivos.", + "tabs.startDisabledTipLearnMore": "Saiba mais sobre nós iniciais", "tabs.startNotSupportedTip": "A guia Iniciar não é compatível com snippets.", "tabs.tools": "Ferramentas", "tabs.transform": "Transformar", + "tabs.unconfiguredStartDisabledTip": "Um nó inicial não configurado foi adicionado à tela. Conclua a configuração antes de continuar.", "tabs.usePlugin": "Selecionar ferramenta", "tabs.utilities": "Utilitários", "tabs.workflowTool": "Fluxo de trabalho", diff --git a/web/i18n/ro-RO/workflow.json b/web/i18n/ro-RO/workflow.json index e5f4464ca62..5d255f9852b 100644 --- a/web/i18n/ro-RO/workflow.json +++ b/web/i18n/ro-RO/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Loop", "blocks.loop-end": "Ieșire din buclă", "blocks.loop-start": "Întreținere buclă", + "blocks.mostCommon": "Cel mai frecvent", "blocks.originalStartNode": "nod de start original", "blocks.parameter-extractor": "Extractor de parametri", "blocks.question-classifier": "Clasificator de întrebări", "blocks.start": "Începe", + "blocks.start-placeholder": "Pornire workflow", "blocks.template-transform": "Șablon", "blocks.tool": "Unealtă", "blocks.trigger-plugin": "Declanșator plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Utilizați LLM pentru a extrage parametrii structurați din limbajul natural pentru invocările de instrumente sau cererile HTTP.", "blocksAbout.question-classifier": "Definiți condițiile de clasificare a întrebărilor utilizatorului, LLM poate defini cum progresează conversația pe baza descrierii clasificării", "blocksAbout.start": "Definiți parametrii inițiali pentru lansarea unui flux de lucru", + "blocksAbout.start-placeholder": "Alegeți cum începe acest workflow", "blocksAbout.template-transform": "Convertiți datele în șiruri de caractere folosind sintaxa șablonului Jinja", "blocksAbout.tool": "Utilizați instrumente externe pentru a extinde capacitățile fluxului de lucru", "blocksAbout.trigger-plugin": "Declanșator de integrare terță parte care pornește fluxuri de lucru din evenimente ale platformelor externe", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "tip mesaj", "nodes.start.outputVars.query": "Intrare utilizator", "nodes.start.required": "necesar", + "nodes.start.userInputTipDescription": "Definiți intrările de colectat de la utilizatorii finali când workflow-ul pornește la cerere.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Răsfoiți mai multe în Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Găsiți mai multe instrumente în Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Nu au fost găsite declanșatoare", + "nodes.startPlaceholder.nodeCollapsedDescription": "Faceți clic pentru a configura nodul de pornire", + "nodes.startPlaceholder.nodeDescription": "Alegeți un nod de pornire din panoul din dreapta", + "nodes.startPlaceholder.nodeTitle": "Pornire workflow", + "nodes.startPlaceholder.panelDescription": "Nodul de pornire definește ce declanșează rularea workflow-ului", + "nodes.startPlaceholder.panelTitle": "Alegeți un nod de pornire", + "nodes.startPlaceholder.userInputConflictTip": "Intrarea utilizatorului nu poate fi combinată cu alte declanșatoare", + "nodes.startPlaceholder.validationRequired": "Alegeți mai întâi un nod de pornire.", "nodes.templateTransform.code": "Cod", "nodes.templateTransform.codeSupportTip": "Suportă doar Jinja2", "nodes.templateTransform.inputVars": "Variabile de intrare", @@ -1209,9 +1223,11 @@ "tabs.sources": "Surse", "tabs.start": "Începe", "tabs.startDisabledTip": "Nodul de declanșare și nodul de intrare a utilizatorului se exclud reciproc.", + "tabs.startDisabledTipLearnMore": "Aflați mai multe despre nodurile de pornire", "tabs.startNotSupportedTip": "Fila Start nu este acceptată în fragmente.", "tabs.tools": "Instrumente", "tabs.transform": "Transformare", + "tabs.unconfiguredStartDisabledTip": "Un nod de pornire neconfigurat a fost adăugat pe canvas. Finalizați configurarea înainte de a continua.", "tabs.usePlugin": "Selectează instrumentul", "tabs.utilities": "Utilități", "tabs.workflowTool": "Flux de lucru", diff --git a/web/i18n/ru-RU/workflow.json b/web/i18n/ru-RU/workflow.json index e5ca0c9dc59..5d485efc384 100644 --- a/web/i18n/ru-RU/workflow.json +++ b/web/i18n/ru-RU/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Цикл", "blocks.loop-end": "Конец цикла", "blocks.loop-start": "Начало цикла", + "blocks.mostCommon": "Самый распространенный", "blocks.originalStartNode": "исходный начальный узел", "blocks.parameter-extractor": "Экстрактор параметров", "blocks.question-classifier": "Классификатор вопросов", "blocks.start": "Начало", + "blocks.start-placeholder": "Запуск workflow", "blocks.template-transform": "Шаблон", "blocks.tool": "Инструмент", "blocks.trigger-plugin": "Триггер плагина", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Используйте LLM для извлечения структурированных параметров из естественного языка для вызова инструментов или HTTP-запросов.", "blocksAbout.question-classifier": "Определите условия классификации вопросов пользователей, LLM может определить, как будет развиваться разговор на основе описания классификации", "blocksAbout.start": "Определите начальные параметры для запуска рабочего процесса", + "blocksAbout.start-placeholder": "Выберите, как запускается этот workflow", "blocksAbout.template-transform": "Преобразование данных в строку с использованием синтаксиса шаблонов Jinja", "blocksAbout.tool": "Используйте внешние инструменты для расширения возможностей рабочего процесса", "blocksAbout.trigger-plugin": "Триггер интеграции с третьими сторонами, который запускает рабочие процессы на основе событий внешней платформы", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "тип сообщения", "nodes.start.outputVars.query": "Ввод пользователя", "nodes.start.required": "обязательно", + "nodes.start.userInputTipDescription": "Задайте входные данные, которые нужно собрать у конечных пользователей при запуске workflow по запросу.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Найти больше в Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Найти больше инструментов в Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Триггеры не найдены", + "nodes.startPlaceholder.nodeCollapsedDescription": "Нажмите, чтобы настроить начальный узел", + "nodes.startPlaceholder.nodeDescription": "Выберите начальный узел на правой панели", + "nodes.startPlaceholder.nodeTitle": "Запуск workflow", + "nodes.startPlaceholder.panelDescription": "Начальный узел определяет, что запускает выполнение вашего workflow", + "nodes.startPlaceholder.panelTitle": "Выберите начальный узел", + "nodes.startPlaceholder.userInputConflictTip": "Пользовательский ввод нельзя сочетать с другими триггерами", + "nodes.startPlaceholder.validationRequired": "Сначала выберите начальный узел.", "nodes.templateTransform.code": "Код", "nodes.templateTransform.codeSupportTip": "Поддерживает только Jinja2", "nodes.templateTransform.inputVars": "Входные переменные", @@ -1209,9 +1223,11 @@ "tabs.sources": "Источников", "tabs.start": "Начать", "tabs.startDisabledTip": "Узел триггера и узел ввода пользователя исключают друг друга.", + "tabs.startDisabledTipLearnMore": "Подробнее о начальных узлах", "tabs.startNotSupportedTip": "Вкладка «Пуск» не поддерживается во фрагментах.", "tabs.tools": "Инструменты", "tabs.transform": "Преобразование", + "tabs.unconfiguredStartDisabledTip": "На холст добавлен ненастроенный начальный узел. Завершите настройку, прежде чем продолжить.", "tabs.usePlugin": "Выбрать инструмент", "tabs.utilities": "Утилиты", "tabs.workflowTool": "Рабочий процесс", diff --git a/web/i18n/sl-SI/workflow.json b/web/i18n/sl-SI/workflow.json index e1d3cd028f1..fc17ac6bde2 100644 --- a/web/i18n/sl-SI/workflow.json +++ b/web/i18n/sl-SI/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Zanka", "blocks.loop-end": "Izhod iz zanke", "blocks.loop-start": "Začetek zanke", + "blocks.mostCommon": "Najpogostejše", "blocks.originalStartNode": "izvorna začetna točka", "blocks.parameter-extractor": "Ekstraktor parametrov", "blocks.question-classifier": "Razvrščevalec vprašanj", "blocks.start": "Začni", + "blocks.start-placeholder": "Začetek workflowa", "blocks.template-transform": "Predloga", "blocks.tool": "Orodje", "blocks.trigger-plugin": "Sprožilec vtičnika", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Uporabite LLM za pridobivanje strukturiranih parametrov iz naravnega jezika za klice orodij ali HTTP zahtev.", "blocksAbout.question-classifier": "Določite pogoje klasifikacije uporabniških vprašanj, LLM lahko določi, kako se pogovor razvija na podlagi opisa klasifikacije.", "blocksAbout.start": "Določite začetne parametre za zagon delovnega toka", + "blocksAbout.start-placeholder": "Izberite, kako se ta workflow začne", "blocksAbout.template-transform": "Pretvori podatke v niz z uporabo Jinja predloge", "blocksAbout.tool": "Uporabite zunanja orodja za razširitev zmogljivosti delovnega toka", "blocksAbout.trigger-plugin": "Sprožilec integracije tretje osebe, ki začne delovne tokove iz dogodkov na zunanji platformi", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "vrsta sporočila", "nodes.start.outputVars.query": "Uporabniški vnos", "nodes.start.required": "zahtevano", + "nodes.start.userInputTipDescription": "Določite vnose, ki jih želite zbrati od končnih uporabnikov, ko se workflow zažene na zahtevo.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Prebrskajte več v Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Poiščite več orodij v Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Ni najdenih sprožilcev", + "nodes.startPlaceholder.nodeCollapsedDescription": "Kliknite za konfiguracijo začetnega vozlišča", + "nodes.startPlaceholder.nodeDescription": "Izberite začetno vozlišče na desni plošči", + "nodes.startPlaceholder.nodeTitle": "Začetek workflowa", + "nodes.startPlaceholder.panelDescription": "Začetno vozlišče določa, kaj sproži zagon vašega workflowa", + "nodes.startPlaceholder.panelTitle": "Izberite začetno vozlišče", + "nodes.startPlaceholder.userInputConflictTip": "Uporabniškega vnosa ni mogoče kombinirati z drugimi sprožilci", + "nodes.startPlaceholder.validationRequired": "Najprej izberite začetno vozlišče.", "nodes.templateTransform.code": "Koda", "nodes.templateTransform.codeSupportTip": "Podpira samo Jinja2", "nodes.templateTransform.inputVars": "Vhodne spremenljivke", @@ -1209,9 +1223,11 @@ "tabs.sources": "Virov", "tabs.start": "Začni", "tabs.startDisabledTip": "Vozlišče sprožilca in vozlišče vnosa uporabnika se med seboj izključujeta.", + "tabs.startDisabledTipLearnMore": "Več o začetnih vozliščih", "tabs.startNotSupportedTip": "Zavihek Start ni podprt v izrezkih.", "tabs.tools": "Orodja", "tabs.transform": "Pretvori", + "tabs.unconfiguredStartDisabledTip": "Na platno je bilo dodano nekonfigurirano začetno vozlišče. Pred nadaljevanjem dokončajte nastavitev.", "tabs.usePlugin": "Izberi orodje", "tabs.utilities": "Komunalne storitve", "tabs.workflowTool": "Delovni tok", diff --git a/web/i18n/th-TH/workflow.json b/web/i18n/th-TH/workflow.json index 7fa8d0bf684..91d073fe32e 100644 --- a/web/i18n/th-TH/workflow.json +++ b/web/i18n/th-TH/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "ลูป", "blocks.loop-end": "ออกจากลูป", "blocks.loop-start": "เริ่มลูป", + "blocks.mostCommon": "พบบ่อยที่สุด", "blocks.originalStartNode": "โหนดเริ่มต้นเดิม", "blocks.parameter-extractor": "ตัวแยกพารามิเตอร์", "blocks.question-classifier": "ตัวจําแนกคําถาม", "blocks.start": "เริ่ม", + "blocks.start-placeholder": "เริ่มต้นเวิร์กโฟลว์", "blocks.template-transform": "แม่ แบบ", "blocks.tool": "เครื่องมือ", "blocks.trigger-plugin": "ทริกเกอร์ปลั๊กอิน", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "ใช้ LLM เพื่อแยกพารามิเตอร์ที่มีโครงสร้างจากภาษาธรรมชาติสําหรับการเรียกใช้เครื่องมือหรือคําขอ HTTP", "blocksAbout.question-classifier": "กําหนดเงื่อนไขการจําแนกประเภทของคําถามของผู้ใช้ LLM สามารถกําหนดความคืบหน้าของการสนทนาตามคําอธิบายการจําแนกประเภท", "blocksAbout.start": "กําหนดพารามิเตอร์เริ่มต้นสําหรับการเปิดใช้เวิร์กโฟลว์", + "blocksAbout.start-placeholder": "เลือกวิธีเริ่มต้นเวิร์กโฟลว์นี้", "blocksAbout.template-transform": "แปลงข้อมูลเป็นสตริงโดยใช้ไวยากรณ์เทมเพลต Jinja", "blocksAbout.tool": "ใช้เครื่องมือภายนอกเพื่อขยายความสามารถของเวิร์กโฟลว์", "blocksAbout.trigger-plugin": "ทริกเกอร์การรวมจากบุคคลที่สามที่เริ่มการทำงานอัตโนมัติจากเหตุการณ์ของแพลตฟอร์มภายนอก", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "ประเภทข้อความ", "nodes.start.outputVars.query": "การป้อนข้อมูลของผู้ใช้", "nodes.start.required": "ต้องระบุ", + "nodes.start.userInputTipDescription": "กำหนดอินพุตที่จะรวบรวมจากผู้ใช้ปลายทางเมื่อเวิร์กโฟลว์เริ่มทำงานตามคำขอ", + "nodes.startPlaceholder.browseMoreOnMarketplace": "เรียกดูเพิ่มเติมใน Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "ค้นหาเครื่องมือเพิ่มเติมใน Marketplace", + "nodes.startPlaceholder.noTriggersFound": "ไม่พบทริกเกอร์", + "nodes.startPlaceholder.nodeCollapsedDescription": "คลิกเพื่อกำหนดค่าโหนดเริ่มต้น", + "nodes.startPlaceholder.nodeDescription": "เลือกโหนดเริ่มต้นจากแผงด้านขวา", + "nodes.startPlaceholder.nodeTitle": "เริ่มต้นเวิร์กโฟลว์", + "nodes.startPlaceholder.panelDescription": "โหนดเริ่มต้นกำหนดสิ่งที่จะทริกเกอร์ให้เวิร์กโฟลว์ทำงาน", + "nodes.startPlaceholder.panelTitle": "เลือกโหนดเริ่มต้น", + "nodes.startPlaceholder.userInputConflictTip": "อินพุตผู้ใช้ไม่สามารถใช้ร่วมกับทริกเกอร์อื่นได้", + "nodes.startPlaceholder.validationRequired": "โปรดเลือกโหนดเริ่มต้นก่อน", "nodes.templateTransform.code": "รหัส", "nodes.templateTransform.codeSupportTip": "รองรับเฉพาะ Jinja2", "nodes.templateTransform.inputVars": "ตัวแปรอินพุต", @@ -1209,9 +1223,11 @@ "tabs.sources": "แหล่ง", "tabs.start": "เริ่ม", "tabs.startDisabledTip": "โหนดทริกเกอร์และโหนดป้อนข้อมูลของผู้ใช้ไม่สามารถใช้ร่วมกันได้", + "tabs.startDisabledTipLearnMore": "เรียนรู้เพิ่มเติมเกี่ยวกับโหนดเริ่มต้น", "tabs.startNotSupportedTip": "ตัวอย่างข้อมูลไม่รองรับแท็บเริ่มต้น", "tabs.tools": "เครื่อง มือ", "tabs.transform": "แปลง", + "tabs.unconfiguredStartDisabledTip": "มีการเพิ่มโหนดเริ่มต้นที่ยังไม่ได้กำหนดค่าลงในแคนวาส โปรดตั้งค่าให้เสร็จก่อนดำเนินการต่อ", "tabs.usePlugin": "เลือกเครื่องมือ", "tabs.utilities": "สาธารณูปโภค", "tabs.workflowTool": "เวิร์กโฟลว์", diff --git a/web/i18n/tr-TR/workflow.json b/web/i18n/tr-TR/workflow.json index b478e8c9c04..8aa69757957 100644 --- a/web/i18n/tr-TR/workflow.json +++ b/web/i18n/tr-TR/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Döngü", "blocks.loop-end": "Döngüden Çık", "blocks.loop-start": "Döngü Başlangıcı", + "blocks.mostCommon": "En yaygın", "blocks.originalStartNode": "orijinal başlangıç düğümü", "blocks.parameter-extractor": "Parametre Çıkarıcı", "blocks.question-classifier": "Soru Sınıflandırıcı", "blocks.start": "Başlat", + "blocks.start-placeholder": "Workflow başlangıcı", "blocks.template-transform": "Şablon", "blocks.tool": "Araç", "blocks.trigger-plugin": "Eklenti Tetikleyicisi", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Aracı çağırmak veya HTTP istekleri için doğal dilden yapılandırılmış parametreler çıkarmak için LLM kullanın.", "blocksAbout.question-classifier": "Kullanıcı sorularının sınıflandırma koşullarını tanımlayın, LLM sınıflandırma açıklamasına dayalı olarak konuşmanın nasıl ilerleyeceğini tanımlayabilir", "blocksAbout.start": "Bir iş akışını başlatmak için başlangıç parametrelerini tanımlayın", + "blocksAbout.start-placeholder": "Bu workflow’un nasıl başlayacağını seçin", "blocksAbout.template-transform": "Jinja şablon sözdizimini kullanarak verileri stringe dönüştürün", "blocksAbout.tool": "İş akışı yeteneklerini genişletmek için dış araçlar kullanın", "blocksAbout.trigger-plugin": "Üçüncü taraf entegrasyon tetikleyicisi, dış platform olaylarından iş akışlarını başlatır", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "mesaj türü", "nodes.start.outputVars.query": "Kullanıcı girişi", "nodes.start.required": "gerekli", + "nodes.start.userInputTipDescription": "Workflow isteğe bağlı başladığında son kullanıcılardan toplanacak girişleri tanımlayın.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Marketplace’te daha fazlasına göz atın", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Marketplace’te daha fazla araç bulun", + "nodes.startPlaceholder.noTriggersFound": "Tetikleyici bulunamadı", + "nodes.startPlaceholder.nodeCollapsedDescription": "Başlangıç düğümünü yapılandırmak için tıklayın", + "nodes.startPlaceholder.nodeDescription": "Sağ panelden bir başlangıç düğümü seçin", + "nodes.startPlaceholder.nodeTitle": "Workflow başlangıcı", + "nodes.startPlaceholder.panelDescription": "Başlangıç düğümü workflow’unuzu neyin çalıştıracağını tanımlar", + "nodes.startPlaceholder.panelTitle": "Bir başlangıç düğümü seçin", + "nodes.startPlaceholder.userInputConflictTip": "Kullanıcı girişi diğer tetikleyicilerle birleştirilemez", + "nodes.startPlaceholder.validationRequired": "Önce bir başlangıç düğümü seçin.", "nodes.templateTransform.code": "Kod", "nodes.templateTransform.codeSupportTip": "Sadece Jinja2 destekler", "nodes.templateTransform.inputVars": "Giriş Değişkenleri", @@ -1209,9 +1223,11 @@ "tabs.sources": "Kaynak", "tabs.start": "Başlat", "tabs.startDisabledTip": "Tetikleyici düğümü ve kullanıcı girişi düğümü birbirini dışlar.", + "tabs.startDisabledTipLearnMore": "Başlangıç düğümleri hakkında daha fazla bilgi edinin", "tabs.startNotSupportedTip": "Başlangıç sekmesi parçacıklarda desteklenmez.", "tabs.tools": "Araçlar", "tabs.transform": "Dönüştür", + "tabs.unconfiguredStartDisabledTip": "Tuvale yapılandırılmamış bir başlangıç düğümü eklendi. Devam etmeden önce kurulumu tamamlayın.", "tabs.usePlugin": "Araç seç", "tabs.utilities": "Yardımcı Araçlar", "tabs.workflowTool": "İş Akışı", diff --git a/web/i18n/uk-UA/workflow.json b/web/i18n/uk-UA/workflow.json index 948f9e1dab0..1b4b8e7a289 100644 --- a/web/i18n/uk-UA/workflow.json +++ b/web/i18n/uk-UA/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Петля", "blocks.loop-end": "Вихід з циклу", "blocks.loop-start": "Початок циклу", + "blocks.mostCommon": "Найпоширеніше", "blocks.originalStartNode": "оригінальний початковий вузол", "blocks.parameter-extractor": "Екстрактор параметрів", "blocks.question-classifier": "Класифікатор питань", "blocks.start": "Початок", + "blocks.start-placeholder": "Початок workflow", "blocks.template-transform": "Шаблон", "blocks.tool": "Інструмент", "blocks.trigger-plugin": "Тригер плагіна", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Використовуйте LLM для вилучення структурованих параметрів з природної мови для викликів інструментів або HTTP-запитів.", "blocksAbout.question-classifier": "Визначте умови класифікації запитань користувачів, LLM може визначати, як розвивається розмова на основі опису класифікації", "blocksAbout.start": "Визначте початкові параметри для запуску робочого потоку", + "blocksAbout.start-placeholder": "Виберіть, як запускається цей workflow", "blocksAbout.template-transform": "Перетворіть дані на рядок за допомогою синтаксису шаблону Jinja", "blocksAbout.tool": "Використовуйте зовнішні інструменти для розширення можливостей робочого процесу", "blocksAbout.trigger-plugin": "Тригер інтеграції сторонніх розробників, який запускає робочі процеси з подій зовнішньої платформи", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "тип повідомлення", "nodes.start.outputVars.query": "Введення користувача", "nodes.start.required": "обов'язковий", + "nodes.start.userInputTipDescription": "Визначте вхідні дані, які потрібно збирати від кінцевих користувачів, коли workflow запускається на вимогу.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Переглянути більше в Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Знайти більше інструментів у Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Тригери не знайдено", + "nodes.startPlaceholder.nodeCollapsedDescription": "Натисніть, щоб налаштувати початковий вузол", + "nodes.startPlaceholder.nodeDescription": "Виберіть початковий вузол на правій панелі", + "nodes.startPlaceholder.nodeTitle": "Початок workflow", + "nodes.startPlaceholder.panelDescription": "Початковий вузол визначає, що запускає виконання вашого workflow", + "nodes.startPlaceholder.panelTitle": "Виберіть початковий вузол", + "nodes.startPlaceholder.userInputConflictTip": "Користувацьке введення не можна поєднувати з іншими тригерами", + "nodes.startPlaceholder.validationRequired": "Спочатку виберіть початковий вузол.", "nodes.templateTransform.code": "Код", "nodes.templateTransform.codeSupportTip": "Підтримує лише Jinja2", "nodes.templateTransform.inputVars": "Вхідні змінні", @@ -1209,9 +1223,11 @@ "tabs.sources": "Джерел", "tabs.start": "Почати", "tabs.startDisabledTip": "Вузол тригера та вузол введення користувача взаємовиключні.", + "tabs.startDisabledTipLearnMore": "Докладніше про початкові вузли", "tabs.startNotSupportedTip": "Вкладка «Пуск» не підтримується у фрагментах.", "tabs.tools": "Інструменти", "tabs.transform": "Трансформація", + "tabs.unconfiguredStartDisabledTip": "На полотно додано неналаштований початковий вузол. Завершіть налаштування, перш ніж продовжити.", "tabs.usePlugin": "Вибрати інструмент", "tabs.utilities": "Утиліти", "tabs.workflowTool": "Робочий потік", diff --git a/web/i18n/vi-VN/workflow.json b/web/i18n/vi-VN/workflow.json index 139f378b2d8..4187e46b9da 100644 --- a/web/i18n/vi-VN/workflow.json +++ b/web/i18n/vi-VN/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "Vòng", "blocks.loop-end": "Thoát vòng lặp", "blocks.loop-start": "Bắt đầu vòng lặp", + "blocks.mostCommon": "Phổ biến nhất", "blocks.originalStartNode": "nút bắt đầu gốc", "blocks.parameter-extractor": "Trình trích xuất tham số", "blocks.question-classifier": "Phân loại câu hỏi", "blocks.start": "Bắt đầu", + "blocks.start-placeholder": "Bắt đầu workflow", "blocks.template-transform": "Mẫu", "blocks.tool": "Công cụ", "blocks.trigger-plugin": "Kích hoạt Plugin", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "Sử dụng LLM để trích xuất các tham số có cấu trúc từ ngôn ngữ tự nhiên để gọi công cụ hoặc yêu cầu HTTP.", "blocksAbout.question-classifier": "Định nghĩa các điều kiện phân loại câu hỏi của người dùng, LLM có thể định nghĩa cách cuộc trò chuyện tiến triển dựa trên mô tả phân loại", "blocksAbout.start": "Định nghĩa các tham số ban đầu để khởi chạy quy trình làm việc", + "blocksAbout.start-placeholder": "Chọn cách workflow này bắt đầu", "blocksAbout.template-transform": "Chuyển đổi dữ liệu thành chuỗi bằng cú pháp mẫu Jinja", "blocksAbout.tool": "Sử dụng các công cụ bên ngoài để mở rộng khả năng quy trình làm việc", "blocksAbout.trigger-plugin": "Kích hoạt tích hợp bên thứ ba khởi chạy quy trình từ các sự kiện trên nền tảng bên ngoài", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "loại tin nhắn", "nodes.start.outputVars.query": "Đầu vào của người dùng", "nodes.start.required": "bắt buộc", + "nodes.start.userInputTipDescription": "Xác định các đầu vào cần thu thập từ người dùng cuối khi workflow của bạn bắt đầu theo yêu cầu.", + "nodes.startPlaceholder.browseMoreOnMarketplace": "Duyệt thêm trên Marketplace", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "Tìm thêm công cụ trên Marketplace", + "nodes.startPlaceholder.noTriggersFound": "Không tìm thấy trình kích hoạt nào", + "nodes.startPlaceholder.nodeCollapsedDescription": "Nhấp để cấu hình nút bắt đầu", + "nodes.startPlaceholder.nodeDescription": "Chọn một nút bắt đầu từ bảng bên phải", + "nodes.startPlaceholder.nodeTitle": "Bắt đầu workflow", + "nodes.startPlaceholder.panelDescription": "Nút bắt đầu xác định điều gì kích hoạt workflow của bạn chạy", + "nodes.startPlaceholder.panelTitle": "Chọn một nút bắt đầu", + "nodes.startPlaceholder.userInputConflictTip": "Đầu vào người dùng không thể kết hợp với các trình kích hoạt khác", + "nodes.startPlaceholder.validationRequired": "Trước tiên hãy chọn một nút bắt đầu.", "nodes.templateTransform.code": "Mã", "nodes.templateTransform.codeSupportTip": "Chỉ hỗ trợ Jinja2", "nodes.templateTransform.inputVars": "Biến đầu vào", @@ -1209,9 +1223,11 @@ "tabs.sources": "Nguồn", "tabs.start": "Bắt đầu", "tabs.startDisabledTip": "Nút kích hoạt và nút nhập liệu của người dùng là loại trừ lẫn nhau.", + "tabs.startDisabledTipLearnMore": "Tìm hiểu thêm về các nút bắt đầu", "tabs.startNotSupportedTip": "Tab bắt đầu không được hỗ trợ trong đoạn trích.", "tabs.tools": "Công cụ", "tabs.transform": "Chuyển đổi", + "tabs.unconfiguredStartDisabledTip": "Một nút bắt đầu chưa được cấu hình đã được thêm vào canvas. Vui lòng hoàn tất thiết lập trước khi tiếp tục.", "tabs.usePlugin": "Chọn công cụ", "tabs.utilities": "Tiện ích", "tabs.workflowTool": "Quy trình làm việc", diff --git a/web/i18n/zh-Hans/workflow.json b/web/i18n/zh-Hans/workflow.json index 272e852e2e0..77d65b9eb43 100644 --- a/web/i18n/zh-Hans/workflow.json +++ b/web/i18n/zh-Hans/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "循环", "blocks.loop-end": "退出循环", "blocks.loop-start": "循环开始", + "blocks.mostCommon": "最常用", "blocks.originalStartNode": "原始开始节点", "blocks.parameter-extractor": "参数提取器", "blocks.question-classifier": "问题分类器", "blocks.start": "用户输入", + "blocks.start-placeholder": "工作流开始", "blocks.template-transform": "模板转换", "blocks.tool": "工具", "blocks.trigger-plugin": "插件触发器", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "利用 LLM 从自然语言内推理提取出结构化参数,用于后置的工具调用或 HTTP 请求。", "blocksAbout.question-classifier": "定义用户问题的分类条件,LLM 能够根据分类描述定义对话的进展方式", "blocksAbout.start": "定义一个 workflow 流程启动的初始参数", + "blocksAbout.start-placeholder": "选择这个 workflow 的启动方式", "blocksAbout.template-transform": "使用 Jinja 模板语法将数据转换为字符串", "blocksAbout.tool": "使用外部工具扩展工作流功能", "blocksAbout.trigger-plugin": "从外部平台事件启动工作流的第三方集成触发器", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "消息类型", "nodes.start.outputVars.query": "用户输入", "nodes.start.required": "必填", + "nodes.start.userInputTipDescription": "定义当 workflow 按需启动时需要向终端用户收集的输入。", + "nodes.startPlaceholder.browseMoreOnMarketplace": "在 Marketplace 浏览更多", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "在 Marketplace 查找更多工具", + "nodes.startPlaceholder.noTriggersFound": "未找到触发器", + "nodes.startPlaceholder.nodeCollapsedDescription": "点击配置开始节点", + "nodes.startPlaceholder.nodeDescription": "从右侧面板选择开始节点", + "nodes.startPlaceholder.nodeTitle": "工作流开始", + "nodes.startPlaceholder.panelDescription": "开始节点定义 workflow 的触发方式", + "nodes.startPlaceholder.panelTitle": "选择开始节点", + "nodes.startPlaceholder.userInputConflictTip": "用户输入不能和其他触发器组合使用", + "nodes.startPlaceholder.validationRequired": "请先选择开始节点。", "nodes.templateTransform.code": "代码", "nodes.templateTransform.codeSupportTip": "只支持 Jinja2", "nodes.templateTransform.inputVars": "输入变量", @@ -1209,9 +1223,11 @@ "tabs.sources": "数据源", "tabs.start": "开始", "tabs.startDisabledTip": "触发节点与用户输入节点互斥。", + "tabs.startDisabledTipLearnMore": "了解更多开始节点", "tabs.startNotSupportedTip": "Snippet 暂不支持 Start 标签。", "tabs.tools": "工具", "tabs.transform": "转换", + "tabs.unconfiguredStartDisabledTip": "画布上已有未配置的开始节点。请先完成设置后再继续。", "tabs.usePlugin": "选择工具", "tabs.utilities": "工具", "tabs.workflowTool": "工作流", diff --git a/web/i18n/zh-Hant/workflow.json b/web/i18n/zh-Hant/workflow.json index 71b13c8eee3..0f98827c890 100644 --- a/web/i18n/zh-Hant/workflow.json +++ b/web/i18n/zh-Hant/workflow.json @@ -19,10 +19,12 @@ "blocks.loop": "循環", "blocks.loop-end": "退出循環", "blocks.loop-start": "循環開始", + "blocks.mostCommon": "最常用", "blocks.originalStartNode": "原始起始節點", "blocks.parameter-extractor": "參數提取器", "blocks.question-classifier": "問題分類器", "blocks.start": "開始", + "blocks.start-placeholder": "工作流程開始", "blocks.template-transform": "模板轉換", "blocks.tool": "工具", "blocks.trigger-plugin": "插件觸發器", @@ -53,6 +55,7 @@ "blocksAbout.parameter-extractor": "利用 LLM 從自然語言內推理提取出結構化參數,用於後置的工具調用或 HTTP 請求。", "blocksAbout.question-classifier": "定義用戶問題的分類條件,LLM 能夠根據分類描述定義對話的進展方式", "blocksAbout.start": "定義一個 workflow 流程啟動的參數", + "blocksAbout.start-placeholder": "選擇此工作流程的啟動方式", "blocksAbout.template-transform": "使用 Jinja 模板語法將資料轉換為字符串", "blocksAbout.tool": "使用外部工具來擴展工作流程功能", "blocksAbout.trigger-plugin": "第三方整合觸發器,從外部平台事件啟動工作流程", @@ -934,6 +937,17 @@ "nodes.start.outputVars.memories.type": "消息類型", "nodes.start.outputVars.query": "用戶輸入", "nodes.start.required": "必填", + "nodes.start.userInputTipDescription": "定義工作流程按需啟動時要向終端使用者收集的輸入。", + "nodes.startPlaceholder.browseMoreOnMarketplace": "在 Marketplace 瀏覽更多", + "nodes.startPlaceholder.findMoreToolsInMarketplace": "在 Marketplace 中尋找更多工具", + "nodes.startPlaceholder.noTriggersFound": "找不到觸發器", + "nodes.startPlaceholder.nodeCollapsedDescription": "點擊以設定開始節點", + "nodes.startPlaceholder.nodeDescription": "從右側面板選擇開始節點", + "nodes.startPlaceholder.nodeTitle": "工作流程開始", + "nodes.startPlaceholder.panelDescription": "開始節點定義工作流程的觸發方式", + "nodes.startPlaceholder.panelTitle": "選擇開始節點", + "nodes.startPlaceholder.userInputConflictTip": "使用者輸入不能與其他觸發器組合使用", + "nodes.startPlaceholder.validationRequired": "請先選擇開始節點。", "nodes.templateTransform.code": "模板程式碼", "nodes.templateTransform.codeSupportTip": "只支持 Jinja2", "nodes.templateTransform.inputVars": "輸入變數", @@ -1209,9 +1223,11 @@ "tabs.sources": "來源", "tabs.start": "開始", "tabs.startDisabledTip": "觸發節點與使用者輸入節點是互斥的。", + "tabs.startDisabledTipLearnMore": "了解更多開始節點", "tabs.startNotSupportedTip": "代码段中不支持“开始”选项卡。", "tabs.tools": "工具", "tabs.transform": "轉換", + "tabs.unconfiguredStartDisabledTip": "畫布上已有未設定的開始節點。請先完成設定後再繼續。", "tabs.usePlugin": "選取工具", "tabs.utilities": "工具", "tabs.workflowTool": "工作流",