mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 08:58:09 +08:00
Merge remote-tracking branch 'myori/main' into feat/collaboration2
This commit is contained in:
@ -1,15 +1,19 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import type { MockedFunction } from 'vitest'
|
||||
import type { EntryNodeStatus } from '../store/trigger-status'
|
||||
import type { BlockEnum } from '../types'
|
||||
import { act, render } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTriggerStatusStore } from '../store/trigger-status'
|
||||
import { isTriggerNode } from '../types'
|
||||
import type { EntryNodeStatus } from '../store/trigger-status'
|
||||
|
||||
// Mock the isTriggerNode function
|
||||
jest.mock('../types', () => ({
|
||||
isTriggerNode: jest.fn(),
|
||||
// Mock the isTriggerNode function while preserving BlockEnum
|
||||
vi.mock('../types', async importOriginal => ({
|
||||
...await importOriginal<typeof import('../types')>(),
|
||||
isTriggerNode: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockIsTriggerNode = isTriggerNode as jest.MockedFunction<typeof isTriggerNode>
|
||||
const mockIsTriggerNode = isTriggerNode as MockedFunction<typeof isTriggerNode>
|
||||
|
||||
// Test component that mimics BaseNode's usage pattern
|
||||
const TestTriggerNode: React.FC<{
|
||||
@ -17,12 +21,14 @@ const TestTriggerNode: React.FC<{
|
||||
nodeType: string
|
||||
}> = ({ nodeId, nodeType }) => {
|
||||
const triggerStatus = useTriggerStatusStore(state =>
|
||||
mockIsTriggerNode(nodeType) ? (state.triggerStatuses[nodeId] || 'disabled') : 'enabled',
|
||||
mockIsTriggerNode(nodeType as BlockEnum) ? (state.triggerStatuses[nodeId] || 'disabled') : 'enabled',
|
||||
)
|
||||
|
||||
return (
|
||||
<div data-testid={`node-${nodeId}`} data-status={triggerStatus}>
|
||||
Status: {triggerStatus}
|
||||
Status:
|
||||
{' '}
|
||||
{triggerStatus}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -77,7 +83,7 @@ describe('Trigger Status Synchronization Integration', () => {
|
||||
})
|
||||
|
||||
// Reset mocks
|
||||
jest.clearAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Real-time Status Synchronization', () => {
|
||||
@ -271,14 +277,14 @@ describe('Trigger Status Synchronization Integration', () => {
|
||||
nodeType: string
|
||||
}> = ({ nodeId, nodeType }) => {
|
||||
const triggerStatusSelector = useCallback((state: any) =>
|
||||
mockIsTriggerNode(nodeType) ? (state.triggerStatuses[nodeId] || 'disabled') : 'enabled',
|
||||
[nodeId, nodeType],
|
||||
)
|
||||
mockIsTriggerNode(nodeType as BlockEnum) ? (state.triggerStatuses[nodeId] || 'disabled') : 'enabled', [nodeId, nodeType])
|
||||
const triggerStatus = useTriggerStatusStore(triggerStatusSelector)
|
||||
|
||||
return (
|
||||
<div data-testid={`optimized-node-${nodeId}`} data-status={triggerStatus}>
|
||||
Status: {triggerStatus}
|
||||
Status:
|
||||
{' '}
|
||||
{triggerStatus}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -312,9 +318,10 @@ describe('Trigger Status Synchronization Integration', () => {
|
||||
mockIsTriggerNode.mockImplementation(nodeType => nodeType === 'trigger-webhook')
|
||||
|
||||
const TestComponent: React.FC<{ nodeType: string }> = ({ nodeType }) => {
|
||||
const triggerStatusSelector = useCallback((state: any) =>
|
||||
mockIsTriggerNode(nodeType) ? (state.triggerStatuses['test-node'] || 'disabled') : 'enabled',
|
||||
['test-node', nodeType], // Dependencies should match implementation
|
||||
const triggerStatusSelector = useCallback(
|
||||
(state: any) =>
|
||||
mockIsTriggerNode(nodeType as BlockEnum) ? (state.triggerStatuses['test-node'] || 'disabled') : 'enabled',
|
||||
['test-node', nodeType], // Dependencies should match implementation
|
||||
)
|
||||
const status = useTriggerStatusStore(triggerStatusSelector)
|
||||
return <div data-testid="test-component" data-status={status} />
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { BlockEnum } from './types'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import {
|
||||
Agent,
|
||||
Answer,
|
||||
@ -26,14 +26,14 @@ import {
|
||||
VariableX,
|
||||
WebhookLine,
|
||||
} from '@/app/components/base/icons/src/vender/workflow'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { BlockEnum } from './types'
|
||||
|
||||
type BlockIconProps = {
|
||||
type: BlockEnum
|
||||
size?: string
|
||||
className?: string
|
||||
toolIcon?: string | { content: string; background: string }
|
||||
toolIcon?: string | { content: string, background: string }
|
||||
}
|
||||
const ICON_CONTAINER_CLASSNAME_SIZE_MAP: Record<string, string> = {
|
||||
xs: 'w-4 h-4 rounded-[5px] shadow-xs',
|
||||
@ -125,15 +125,14 @@ const BlockIcon: FC<BlockIconProps> = ({
|
||||
showDefaultIcon && ICON_CONTAINER_BG_COLOR_MAP[type],
|
||||
toolIcon && '!shadow-none',
|
||||
className,
|
||||
)}
|
||||
)
|
||||
}
|
||||
>
|
||||
{
|
||||
showDefaultIcon && (
|
||||
getIcon(type,
|
||||
(type === BlockEnum.TriggerSchedule || type === BlockEnum.TriggerWebhook)
|
||||
? (size === 'xs' ? 'w-4 h-4' : 'w-4.5 h-4.5')
|
||||
: (size === 'xs' ? 'w-3 h-3' : 'w-3.5 h-3.5'),
|
||||
)
|
||||
getIcon(type, (type === BlockEnum.TriggerSchedule || type === BlockEnum.TriggerWebhook)
|
||||
? (size === 'xs' ? 'w-4 h-4' : 'w-4.5 h-4.5')
|
||||
: (size === 'xs' ? 'w-3 h-3' : 'w-3.5 h-3.5'))
|
||||
)
|
||||
}
|
||||
{
|
||||
@ -142,21 +141,22 @@ const BlockIcon: FC<BlockIconProps> = ({
|
||||
{
|
||||
typeof toolIcon === 'string'
|
||||
? (
|
||||
<div
|
||||
className='h-full w-full shrink-0 rounded-md bg-cover bg-center'
|
||||
style={{
|
||||
backgroundImage: `url(${toolIcon})`,
|
||||
}}
|
||||
></div>
|
||||
)
|
||||
<div
|
||||
className="h-full w-full shrink-0 rounded-md bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: `url(${toolIcon})`,
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<AppIcon
|
||||
className='!h-full !w-full shrink-0'
|
||||
size='tiny'
|
||||
icon={toolIcon?.content}
|
||||
background={toolIcon?.background}
|
||||
/>
|
||||
)
|
||||
<AppIcon
|
||||
className="!h-full !w-full shrink-0"
|
||||
size="tiny"
|
||||
icon={toolIcon?.content}
|
||||
background={toolIcon?.background}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
|
||||
@ -1,28 +1,36 @@
|
||||
'use client'
|
||||
import type {
|
||||
RefObject,
|
||||
} from 'react'
|
||||
import type { BlockEnum, OnSelectBlock } from '../types'
|
||||
import type { ListRef } from './market-place-plugin/list'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from './types'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { BlockEnum, OnSelectBlock } from '../types'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from './types'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
|
||||
import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||
import { PluginCategoryEnum } from '../../plugins/types'
|
||||
import { BlockEnum as BlockEnumValue } from '../types'
|
||||
import { ENTRY_NODE_TYPES } from './constants'
|
||||
import FeaturedTriggers from './featured-triggers'
|
||||
import PluginList from './market-place-plugin/list'
|
||||
import StartBlocks from './start-blocks'
|
||||
import TriggerPluginList from './trigger-plugin/list'
|
||||
import { ENTRY_NODE_TYPES } from './constants'
|
||||
import cn from '@/utils/classnames'
|
||||
import Link from 'next/link'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { BlockEnum as BlockEnumValue } from '../types'
|
||||
import FeaturedTriggers from './featured-triggers'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useAllTriggerPlugins, useInvalidateAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { useFeaturedTriggersRecommendations } from '@/service/use-plugins'
|
||||
|
||||
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'
|
||||
|
||||
@ -47,6 +55,8 @@ const AllStartBlocks = ({
|
||||
const [hasStartBlocksContent, setHasStartBlocksContent] = useState(false)
|
||||
const [hasPluginContent, setHasPluginContent] = useState(false)
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
const pluginRef = useRef<ListRef>(null)
|
||||
const wrapElemRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const entryNodeTypes = availableBlocksTypes?.length
|
||||
? availableBlocksTypes
|
||||
@ -71,14 +81,21 @@ const AllStartBlocks = ({
|
||||
const invalidateTriggers = useInvalidateAllTriggerPlugins()
|
||||
const trimmedSearchText = searchText.trim()
|
||||
const hasSearchText = trimmedSearchText.length > 0
|
||||
const hasFilter = hasSearchText || tags.length > 0
|
||||
const {
|
||||
plugins: featuredPlugins = [],
|
||||
isLoading: featuredLoading,
|
||||
} = useFeaturedTriggersRecommendations(enableTriggerPlugin && enable_marketplace && !hasSearchText)
|
||||
} = useFeaturedTriggersRecommendations(enableTriggerPlugin && enable_marketplace && !hasFilter)
|
||||
const {
|
||||
queryPluginsWithDebounced: fetchPlugins,
|
||||
plugins: marketplacePlugins = [],
|
||||
} = useMarketplacePlugins()
|
||||
|
||||
const shouldShowFeatured = enableTriggerPlugin
|
||||
&& enable_marketplace
|
||||
&& !hasSearchText
|
||||
&& !hasFilter
|
||||
const shouldShowTriggerListTitle = hasStartBlocksContent || hasPluginContent
|
||||
const shouldShowMarketplaceFooter = enable_marketplace && !hasFilter
|
||||
|
||||
const handleStartBlocksContentChange = useCallback((hasContent: boolean) => {
|
||||
setHasStartBlocksContent(hasContent)
|
||||
@ -88,18 +105,35 @@ const AllStartBlocks = ({
|
||||
setHasPluginContent(hasContent)
|
||||
}, [])
|
||||
|
||||
const hasAnyContent = hasStartBlocksContent || hasPluginContent || shouldShowFeatured
|
||||
const shouldShowEmptyState = hasSearchText && !hasAnyContent
|
||||
const hasMarketplaceContent = enableTriggerPlugin && enable_marketplace && marketplacePlugins.length > 0
|
||||
const hasAnyContent = hasStartBlocksContent || hasPluginContent || shouldShowFeatured || hasMarketplaceContent
|
||||
const shouldShowEmptyState = hasFilter && !hasAnyContent
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableTriggerPlugin && hasPluginContent)
|
||||
setHasPluginContent(false)
|
||||
}, [enableTriggerPlugin, hasPluginContent])
|
||||
|
||||
useEffect(() => {
|
||||
if (!enableTriggerPlugin || !enable_marketplace)
|
||||
return
|
||||
if (hasFilter) {
|
||||
fetchPlugins({
|
||||
query: searchText,
|
||||
tags,
|
||||
category: PluginCategoryEnum.trigger,
|
||||
})
|
||||
}
|
||||
}, [enableTriggerPlugin, enable_marketplace, hasFilter, fetchPlugins, searchText, tags])
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
||||
<div className='flex max-h-[640px] flex-col'>
|
||||
<div className='flex-1 overflow-y-auto'>
|
||||
<div className="flex max-h-[640px] flex-col">
|
||||
<div
|
||||
ref={wrapElemRef}
|
||||
className="flex-1 overflow-y-auto"
|
||||
onScroll={() => pluginRef.current?.handleScroll()}
|
||||
>
|
||||
<div className={cn(shouldShowEmptyState && 'hidden')}>
|
||||
{shouldShowFeatured && (
|
||||
<>
|
||||
@ -112,14 +146,16 @@ const AllStartBlocks = ({
|
||||
invalidateTriggers()
|
||||
}}
|
||||
/>
|
||||
<div className='px-3'>
|
||||
<Divider className='!h-px' />
|
||||
<div className="px-3">
|
||||
<Divider className="!h-px" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className='px-3 pb-1 pt-2'>
|
||||
<span className='system-xs-medium text-text-primary'>{t('workflow.tabs.allTriggers')}</span>
|
||||
</div>
|
||||
{shouldShowTriggerListTitle && (
|
||||
<div className="px-3 pb-1 pt-2">
|
||||
<span className="system-xs-medium text-text-primary">{t('tabs.allTriggers', { ns: 'workflow' })}</span>
|
||||
</div>
|
||||
)}
|
||||
<StartBlocks
|
||||
searchText={trimmedSearchText}
|
||||
onSelect={onSelect as OnSelectBlock}
|
||||
@ -136,39 +172,50 @@ const AllStartBlocks = ({
|
||||
tags={tags}
|
||||
/>
|
||||
)}
|
||||
{enableTriggerPlugin && enable_marketplace && (
|
||||
<PluginList
|
||||
ref={pluginRef}
|
||||
wrapElemRef={wrapElemRef as RefObject<HTMLElement>}
|
||||
list={marketplacePlugins}
|
||||
searchText={trimmedSearchText}
|
||||
category={PluginCategoryEnum.trigger}
|
||||
tags={tags}
|
||||
hideFindMoreFooter
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldShowEmptyState && (
|
||||
<div className='flex h-full flex-col items-center justify-center gap-3 py-12 text-center'>
|
||||
<SearchMenu className='h-8 w-8 text-text-quaternary' />
|
||||
<div className='text-sm font-medium text-text-secondary'>
|
||||
{t('workflow.tabs.noPluginsFound')}
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 py-12 text-center">
|
||||
<SearchMenu className="h-8 w-8 text-text-quaternary" />
|
||||
<div className="text-sm font-medium text-text-secondary">
|
||||
{t('tabs.noPluginsFound', { ns: 'workflow' })}
|
||||
</div>
|
||||
<Link
|
||||
href='https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml'
|
||||
target='_blank'
|
||||
href="https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml"
|
||||
target="_blank"
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
variant='secondary-accent'
|
||||
className='h-6 cursor-pointer px-3 text-xs'
|
||||
size="small"
|
||||
variant="secondary-accent"
|
||||
className="h-6 cursor-pointer px-3 text-xs"
|
||||
>
|
||||
{t('workflow.tabs.requestToCommunity')}
|
||||
{t('tabs.requestToCommunity', { ns: 'workflow' })}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!shouldShowEmptyState && (
|
||||
{shouldShowMarketplaceFooter && !shouldShowEmptyState && (
|
||||
// Footer - Same as Tools tab marketplace footer
|
||||
<Link
|
||||
className={marketplaceFooterClassName}
|
||||
href={getMarketplaceUrl('')}
|
||||
target='_blank'
|
||||
href={getMarketplaceUrl('', { category: PluginCategoryEnum.trigger })}
|
||||
target="_blank"
|
||||
>
|
||||
<span>{t('plugin.findMoreInMarketplace')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
<span>{t('findMoreInMarketplace', { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="ml-0.5 h-3 w-3" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -3,34 +3,34 @@ import type {
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
} from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Plugin } from '../../plugins/types'
|
||||
import type {
|
||||
BlockEnum,
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import { ToolTypeEnum } from './types'
|
||||
import Tools from './tools'
|
||||
import { useToolTabs } from './hooks'
|
||||
import ViewTypeSelect, { ViewType } from './view-type-select'
|
||||
import cn from '@/utils/classnames'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import PluginList, { type ListProps } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import type { Plugin } from '../../plugins/types'
|
||||
import { PluginCategoryEnum } from '../../plugins/types'
|
||||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import RAGToolRecommendations from './rag-tool-recommendations'
|
||||
import FeaturedTools from './featured-tools'
|
||||
import Link from 'next/link'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import type { ListProps, ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import type { OnSelectBlock } from '@/app/components/workflow/types'
|
||||
import { RiArrowRightUpLine } from '@remixicon/react'
|
||||
import Link from 'next/link'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||
import { PluginCategoryEnum } from '../../plugins/types'
|
||||
import FeaturedTools from './featured-tools'
|
||||
import { useToolTabs } from './hooks'
|
||||
import RAGToolRecommendations from './rag-tool-recommendations'
|
||||
import Tools from './tools'
|
||||
import { ToolTypeEnum } from './types'
|
||||
import ViewTypeSelect, { ViewType } from './view-type-select'
|
||||
|
||||
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'
|
||||
|
||||
@ -47,7 +47,6 @@ type AllToolsProps = {
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
onTagsChange?: Dispatch<SetStateAction<string[]>>
|
||||
isInRAGPipeline?: boolean
|
||||
featuredPlugins?: Plugin[]
|
||||
@ -71,7 +70,6 @@ const AllTools = ({
|
||||
customTools,
|
||||
mcpTools = [],
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
onTagsChange,
|
||||
isInRAGPipeline = false,
|
||||
featuredPlugins = [],
|
||||
@ -172,7 +170,8 @@ const AllTools = ({
|
||||
const { enable_marketplace } = useGlobalPublicStore(s => s.systemFeatures)
|
||||
|
||||
useEffect(() => {
|
||||
if (!enable_marketplace) return
|
||||
if (!enable_marketplace)
|
||||
return
|
||||
if (hasFilter) {
|
||||
fetchPlugins({
|
||||
query: searchText,
|
||||
@ -204,9 +203,9 @@ const AllTools = ({
|
||||
}, [onSelect])
|
||||
|
||||
return (
|
||||
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
|
||||
<div className='flex items-center justify-between border-b border-divider-subtle px-3'>
|
||||
<div className='flex h-8 items-center space-x-1'>
|
||||
<div className={cn('max-w-[500px]', className)}>
|
||||
<div className="flex items-center justify-between border-b border-divider-subtle px-3">
|
||||
<div className="flex h-8 items-center space-x-1">
|
||||
{
|
||||
tabs.map(tab => (
|
||||
<div
|
||||
@ -227,11 +226,11 @@ const AllTools = ({
|
||||
<ViewTypeSelect viewType={activeView} onChange={setActiveView} />
|
||||
)}
|
||||
</div>
|
||||
<div className='flex max-h-[464px] flex-col'>
|
||||
<div className="flex max-h-[464px] flex-col">
|
||||
<div
|
||||
ref={wrapElemRef}
|
||||
className='flex-1 overflow-y-auto'
|
||||
onScroll={pluginRef.current?.handleScroll}
|
||||
className="flex-1 overflow-y-auto"
|
||||
onScroll={() => pluginRef.current?.handleScroll()}
|
||||
>
|
||||
<div className={cn(shouldShowEmptyState && 'hidden')}>
|
||||
{isShowRAGRecommendations && onTagsChange && (
|
||||
@ -248,21 +247,20 @@ const AllTools = ({
|
||||
providerMap={providerMap}
|
||||
onSelect={onSelect}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
isLoading={featuredLoading}
|
||||
onInstallSuccess={async () => {
|
||||
await onFeaturedInstallSuccess?.()
|
||||
}}
|
||||
/>
|
||||
<div className='px-3'>
|
||||
<Divider className='!h-px' />
|
||||
<div className="px-3">
|
||||
<Divider className="!h-px" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{hasToolsListContent && (
|
||||
<>
|
||||
<div className='px-3 pb-1 pt-2'>
|
||||
<span className='system-xs-medium text-text-primary'>{t('tools.allTools')}</span>
|
||||
<div className="px-3 pb-1 pt-2">
|
||||
<span className="system-xs-medium text-text-primary">{t('allTools', { ns: 'tools' })}</span>
|
||||
</div>
|
||||
<Tools
|
||||
className={toolContentClassName}
|
||||
@ -274,7 +272,6 @@ const AllTools = ({
|
||||
viewType={isSupportGroupView ? activeView : ViewType.flat}
|
||||
hasSearchText={hasSearchText}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -284,6 +281,7 @@ const AllTools = ({
|
||||
wrapElemRef={wrapElemRef as RefObject<HTMLElement>}
|
||||
list={notInstalledPlugins}
|
||||
searchText={searchText}
|
||||
category={PluginCategoryEnum.tool}
|
||||
toolContentClassName={toolContentClassName}
|
||||
tags={tags}
|
||||
hideFindMoreFooter
|
||||
@ -292,21 +290,21 @@ const AllTools = ({
|
||||
</div>
|
||||
|
||||
{shouldShowEmptyState && (
|
||||
<div className='flex h-full flex-col items-center justify-center gap-3 py-12 text-center'>
|
||||
<SearchMenu className='h-8 w-8 text-text-quaternary' />
|
||||
<div className='text-sm font-medium text-text-secondary'>
|
||||
{t('workflow.tabs.noPluginsFound')}
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 py-12 text-center">
|
||||
<SearchMenu className="h-8 w-8 text-text-quaternary" />
|
||||
<div className="text-sm font-medium text-text-secondary">
|
||||
{t('tabs.noPluginsFound', { ns: 'workflow' })}
|
||||
</div>
|
||||
<Link
|
||||
href='https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml'
|
||||
target='_blank'
|
||||
href="https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml"
|
||||
target="_blank"
|
||||
>
|
||||
<Button
|
||||
size='small'
|
||||
variant='secondary-accent'
|
||||
className='h-6 cursor-pointer px-3 text-xs'
|
||||
size="small"
|
||||
variant="secondary-accent"
|
||||
className="h-6 cursor-pointer px-3 text-xs"
|
||||
>
|
||||
{t('workflow.tabs.requestToCommunity')}
|
||||
{t('tabs.requestToCommunity', { ns: 'workflow' })}
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
@ -315,11 +313,11 @@ const AllTools = ({
|
||||
{shouldShowMarketplaceFooter && (
|
||||
<Link
|
||||
className={marketplaceFooterClassName}
|
||||
href={getMarketplaceUrl('')}
|
||||
target='_blank'
|
||||
href={getMarketplaceUrl('', { category: PluginCategoryEnum.tool })}
|
||||
target="_blank"
|
||||
>
|
||||
<span>{t('plugin.findMoreInMarketplace')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
<span>{t('findMoreInMarketplace', { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="ml-0.5 h-3 w-3" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,18 +1,19 @@
|
||||
import type { NodeDefault } from '../types'
|
||||
import type { BlockClassificationEnum } from './types'
|
||||
import { groupBy } from 'es-toolkit/compat'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { groupBy } from 'lodash-es'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import type { NodeDefault } from '../types'
|
||||
import { BLOCK_CLASSIFICATIONS } from './constants'
|
||||
import { useBlocks } from './hooks'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import Badge from '@/app/components/base/badge'
|
||||
|
||||
type BlocksProps = {
|
||||
searchText: string
|
||||
@ -38,6 +39,7 @@ const Blocks = ({
|
||||
type: block.type,
|
||||
title: block.title,
|
||||
author: 'Dify',
|
||||
// @ts-expect-error Fix this missing field later
|
||||
description: block.description,
|
||||
},
|
||||
defaultValue: {},
|
||||
@ -50,9 +52,10 @@ const Blocks = ({
|
||||
const list = (grouped[classification] || []).filter((block) => {
|
||||
// Filter out trigger types from Blocks tab
|
||||
if (block.metaData.type === BlockEnum.TriggerWebhook
|
||||
|| block.metaData.type === BlockEnum.TriggerSchedule
|
||||
|| block.metaData.type === BlockEnum.TriggerPlugin)
|
||||
|| block.metaData.type === BlockEnum.TriggerSchedule
|
||||
|| block.metaData.type === BlockEnum.TriggerPlugin) {
|
||||
return false
|
||||
}
|
||||
|
||||
return block.metaData.title.toLowerCase().includes(searchText.toLowerCase()) && availableBlocksTypes.includes(block.metaData.type)
|
||||
})
|
||||
@ -65,7 +68,7 @@ const Blocks = ({
|
||||
}, [blocks, searchText, availableBlocksTypes])
|
||||
const isEmpty = Object.values(groups).every(list => !list.length)
|
||||
|
||||
const renderGroup = useCallback((classification: string) => {
|
||||
const renderGroup = useCallback((classification: BlockClassificationEnum) => {
|
||||
const list = groups[classification].sort((a, b) => (a.metaData.sort || 0) - (b.metaData.sort || 0))
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
@ -79,12 +82,12 @@ const Blocks = ({
|
||||
return (
|
||||
<div
|
||||
key={classification}
|
||||
className='mb-1 last-of-type:mb-0'
|
||||
className="mb-1 last-of-type:mb-0"
|
||||
>
|
||||
{
|
||||
classification !== '-' && !!filteredList.length && (
|
||||
<div className='flex h-[22px] items-start px-3 text-xs font-medium text-text-tertiary'>
|
||||
{t(`workflow.tabs.${classification}`)}
|
||||
<div className="flex h-[22px] items-start px-3 text-xs font-medium text-text-tertiary">
|
||||
{t(`tabs.${classification}`, { ns: 'workflow' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -92,36 +95,36 @@ const Blocks = ({
|
||||
filteredList.map(block => (
|
||||
<Tooltip
|
||||
key={block.metaData.type}
|
||||
position='right'
|
||||
popupClassName='w-[200px] rounded-xl'
|
||||
position="right"
|
||||
popupClassName="w-[200px] rounded-xl"
|
||||
needsDelay={false}
|
||||
popupContent={(
|
||||
<div>
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mb-2'
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={block.metaData.type}
|
||||
/>
|
||||
<div className='system-md-medium mb-1 text-text-primary'>{block.metaData.title}</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>{block.metaData.description}</div>
|
||||
<div className="system-md-medium mb-1 text-text-primary">{block.metaData.title}</div>
|
||||
<div className="system-xs-regular text-text-tertiary">{block.metaData.description}</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
key={block.metaData.type}
|
||||
className='flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover'
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={() => onSelect(block.metaData.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
className='mr-2 shrink-0'
|
||||
className="mr-2 shrink-0"
|
||||
type={block.metaData.type}
|
||||
/>
|
||||
<div className='grow text-sm text-text-secondary'>{block.metaData.title}</div>
|
||||
<div className="grow text-sm text-text-secondary">{block.metaData.title}</div>
|
||||
{
|
||||
block.metaData.type === BlockEnum.LoopEnd && (
|
||||
<Badge
|
||||
text={t('workflow.nodes.loop.loopNode')}
|
||||
className='ml-2 shrink-0'
|
||||
text={t('nodes.loop.loopNode', { ns: 'workflow' })}
|
||||
className="ml-2 shrink-0"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -134,10 +137,10 @@ const Blocks = ({
|
||||
}, [groups, onSelect, t, store])
|
||||
|
||||
return (
|
||||
<div className='max-h-[480px] min-w-[400px] max-w-[500px] overflow-y-auto p-1'>
|
||||
<div className="max-h-[480px] max-w-[500px] overflow-y-auto p-1">
|
||||
{
|
||||
isEmpty && (
|
||||
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>{t('workflow.tabs.noResult')}</div>
|
||||
<div className="flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary">{t('tabs.noResult', { ns: 'workflow' })}</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
|
||||
@ -2,13 +2,13 @@ import type { Block } from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import { BlockClassificationEnum } from './types'
|
||||
|
||||
export const BLOCK_CLASSIFICATIONS: string[] = [
|
||||
export const BLOCK_CLASSIFICATIONS = [
|
||||
BlockClassificationEnum.Default,
|
||||
BlockClassificationEnum.QuestionUnderstand,
|
||||
BlockClassificationEnum.Logic,
|
||||
BlockClassificationEnum.Transform,
|
||||
BlockClassificationEnum.Utilities,
|
||||
]
|
||||
] as const
|
||||
|
||||
export const DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE = [
|
||||
'txt',
|
||||
@ -32,7 +32,7 @@ export const DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE = [
|
||||
'md',
|
||||
]
|
||||
|
||||
export const START_BLOCKS: Block[] = [
|
||||
export const START_BLOCKS = [
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.Start,
|
||||
@ -51,7 +51,7 @@ export const START_BLOCKS: Block[] = [
|
||||
title: 'Webhook Trigger',
|
||||
description: 'HTTP callback trigger',
|
||||
},
|
||||
]
|
||||
] as const satisfies readonly Block[]
|
||||
|
||||
export const ENTRY_NODE_TYPES = [
|
||||
BlockEnum.Start,
|
||||
@ -60,7 +60,7 @@ export const ENTRY_NODE_TYPES = [
|
||||
BlockEnum.TriggerPlugin,
|
||||
] as const
|
||||
|
||||
export const BLOCKS: Block[] = [
|
||||
export const BLOCKS = [
|
||||
{
|
||||
classification: BlockClassificationEnum.Default,
|
||||
type: BlockEnum.LLM,
|
||||
@ -152,4 +152,4 @@ export const BLOCKS: Block[] = [
|
||||
type: BlockEnum.Agent,
|
||||
title: 'Agent',
|
||||
},
|
||||
]
|
||||
] as const satisfies readonly Block[]
|
||||
|
||||
@ -1,24 +1,25 @@
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
import type { DataSourceDefaultValue, ToolDefaultValue } from './types'
|
||||
import type { ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { BlockEnum } from '../types'
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
import type { DataSourceDefaultValue, ToolDefaultValue } from './types'
|
||||
import Tools from './tools'
|
||||
import { ViewType } from './view-type-select'
|
||||
import cn from '@/utils/classnames'
|
||||
import PluginList, { type ListRef } from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import PluginList from '@/app/components/workflow/block-selector/market-place-plugin/list'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE } from './constants'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useMarketplacePlugins } from '../../plugins/marketplace/hooks'
|
||||
import { PluginCategoryEnum } from '../../plugins/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { BlockEnum } from '../types'
|
||||
import { DEFAULT_FILE_EXTENSIONS_IN_LOCAL_FILE_DATA_SOURCE } from './constants'
|
||||
import Tools from './tools'
|
||||
import { ViewType } from './view-type-select'
|
||||
|
||||
type AllToolsProps = {
|
||||
className?: string
|
||||
@ -83,7 +84,8 @@ const DataSources = ({
|
||||
} = useMarketplacePlugins()
|
||||
|
||||
useEffect(() => {
|
||||
if (!enable_marketplace) return
|
||||
if (!enable_marketplace)
|
||||
return
|
||||
if (searchText) {
|
||||
fetchPlugins({
|
||||
query: searchText,
|
||||
@ -96,7 +98,7 @@ const DataSources = ({
|
||||
<div className={cn('w-[400px] min-w-0 max-w-full', className)}>
|
||||
<div
|
||||
ref={wrapElemRef}
|
||||
className='max-h-[464px] overflow-y-auto overflow-x-hidden'
|
||||
className="max-h-[464px] overflow-y-auto overflow-x-hidden"
|
||||
onScroll={pluginRef.current?.handleScroll}
|
||||
>
|
||||
<Tools
|
||||
@ -115,6 +117,7 @@ const DataSources = ({
|
||||
list={notInstalledPlugins}
|
||||
tags={[]}
|
||||
searchText={searchText}
|
||||
category={PluginCategoryEnum.datasource}
|
||||
toolContentClassName={toolContentClassName}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -1,23 +1,26 @@
|
||||
'use client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum, type ToolWithProvider } from '../types'
|
||||
import type { ToolWithProvider } from '../types'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import BlockIcon from '../block-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { isServer } from '@/utils/client'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import Tools from './tools'
|
||||
import { ToolTypeEnum } from './types'
|
||||
import { ViewType } from './view-type-select'
|
||||
import Tools from './tools'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
||||
import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
|
||||
const MAX_RECOMMENDED_COUNT = 15
|
||||
const INITIAL_VISIBLE_COUNT = 5
|
||||
@ -27,7 +30,6 @@ type FeaturedToolsProps = {
|
||||
providerMap: Map<string, ToolWithProvider>
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
isLoading?: boolean
|
||||
onInstallSuccess?: () => void
|
||||
}
|
||||
@ -39,7 +41,6 @@ const FeaturedTools = ({
|
||||
providerMap,
|
||||
onSelect,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
isLoading = false,
|
||||
onInstallSuccess,
|
||||
}: FeaturedToolsProps) => {
|
||||
@ -47,14 +48,14 @@ const FeaturedTools = ({
|
||||
const language = useGetLanguage()
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return false
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
return stored === 'true'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (stored !== null)
|
||||
@ -62,7 +63,7 @@ const FeaturedTools = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
@ -125,28 +126,28 @@ const FeaturedTools = ({
|
||||
const showEmptyState = !isLoading && totalVisible === 0
|
||||
|
||||
return (
|
||||
<div className='px-3 pb-3 pt-2'>
|
||||
<div className="px-3 pb-3 pt-2">
|
||||
<button
|
||||
type='button'
|
||||
className='flex w-full items-center rounded-md px-0 py-1 text-left text-text-primary'
|
||||
type="button"
|
||||
className="flex w-full items-center rounded-md px-0 py-1 text-left text-text-primary"
|
||||
onClick={() => setIsCollapsed(prev => !prev)}
|
||||
>
|
||||
<span className='system-xs-medium text-text-primary'>{t('workflow.tabs.featuredTools')}</span>
|
||||
<span className="system-xs-medium text-text-primary">{t('tabs.featuredTools', { ns: 'workflow' })}</span>
|
||||
<ArrowDownRoundFill className={`ml-0.5 h-4 w-4 text-text-tertiary transition-transform ${isCollapsed ? '-rotate-90' : 'rotate-0'}`} />
|
||||
</button>
|
||||
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
{isLoading && (
|
||||
<div className='py-3'>
|
||||
<Loading type='app' />
|
||||
<div className="py-3">
|
||||
<Loading type="app" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEmptyState && (
|
||||
<p className='system-xs-regular py-2 text-text-tertiary'>
|
||||
<Link className='text-text-accent' href={getMarketplaceUrl('', { category: 'tool' })} target='_blank' rel='noopener noreferrer'>
|
||||
{t('workflow.tabs.noFeaturedPlugins')}
|
||||
<p className="system-xs-regular py-2 text-text-tertiary">
|
||||
<Link className="text-text-accent" href={getMarketplaceUrl('', { category: 'tool' })} target="_blank" rel="noopener noreferrer">
|
||||
{t('tabs.noFeaturedPlugins', { ns: 'workflow' })}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
@ -155,7 +156,7 @@ const FeaturedTools = ({
|
||||
<>
|
||||
{visibleInstalledProviders.length > 0 && (
|
||||
<Tools
|
||||
className='p-0'
|
||||
className="p-0"
|
||||
tools={visibleInstalledProviders}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple
|
||||
@ -163,12 +164,11 @@ const FeaturedTools = ({
|
||||
viewType={ViewType.flat}
|
||||
hasSearchText={false}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)}
|
||||
|
||||
{visibleUninstalledPlugins.length > 0 && (
|
||||
<div className='mt-1 flex flex-col gap-1'>
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
{visibleUninstalledPlugins.map(plugin => (
|
||||
<FeaturedToolUninstalledItem
|
||||
key={plugin.plugin_id}
|
||||
@ -177,7 +177,7 @@ const FeaturedTools = ({
|
||||
onInstallSuccess={async () => {
|
||||
await onInstallSuccess?.()
|
||||
}}
|
||||
t={t}
|
||||
t={t as any}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -187,7 +187,7 @@ const FeaturedTools = ({
|
||||
|
||||
{!isLoading && totalVisible > 0 && canToggleVisibility && (
|
||||
<div
|
||||
className='group mt-1 flex cursor-pointer items-center gap-x-2 rounded-lg py-1 pl-3 pr-2 text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary'
|
||||
className="group mt-1 flex cursor-pointer items-center gap-x-2 rounded-lg py-1 pl-3 pr-2 text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary"
|
||||
onClick={() => {
|
||||
setVisibleCount((count) => {
|
||||
if (count >= maxAvailable)
|
||||
@ -197,16 +197,18 @@ const FeaturedTools = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center px-1 text-text-tertiary transition-colors group-hover:text-text-secondary'>
|
||||
<RiMoreLine className='size-4 group-hover:hidden' />
|
||||
{isExpanded ? (
|
||||
<ArrowUpDoubleLine className='hidden size-4 group-hover:block' />
|
||||
) : (
|
||||
<ArrowDownDoubleLine className='hidden size-4 group-hover:block' />
|
||||
)}
|
||||
<div className="flex items-center px-1 text-text-tertiary transition-colors group-hover:text-text-secondary">
|
||||
<RiMoreLine className="size-4 group-hover:hidden" />
|
||||
{isExpanded
|
||||
? (
|
||||
<ArrowUpDoubleLine className="hidden size-4 group-hover:block" />
|
||||
)
|
||||
: (
|
||||
<ArrowDownDoubleLine className="hidden size-4 group-hover:block" />
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular'>
|
||||
{t(isExpanded ? 'workflow.tabs.showLessFeatured' : 'workflow.tabs.showMoreFeatured')}
|
||||
<div className="system-xs-regular">
|
||||
{t(isExpanded ? 'tabs.showLessFeatured' : 'tabs.showMoreFeatured', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -218,7 +220,7 @@ const FeaturedTools = ({
|
||||
|
||||
type FeaturedToolUninstalledItemProps = {
|
||||
plugin: Plugin
|
||||
language: string
|
||||
language: Locale
|
||||
onInstallSuccess?: () => Promise<void> | void
|
||||
t: (key: string, options?: Record<string, any>) => string
|
||||
}
|
||||
@ -231,7 +233,7 @@ function FeaturedToolUninstalledItem({
|
||||
}: FeaturedToolUninstalledItemProps) {
|
||||
const label = plugin.label?.[language] || plugin.name
|
||||
const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief
|
||||
const installCountLabel = t('plugin.install', { num: formatNumber(plugin.install_count || 0) })
|
||||
const installCountLabel = t('install', { ns: 'plugin', num: formatNumber(plugin.install_count || 0) })
|
||||
const [actionOpen, setActionOpen] = useState(false)
|
||||
const [isActionHovered, setIsActionHovered] = useState(false)
|
||||
const [isInstallModalOpen, setIsInstallModalOpen] = useState(false)
|
||||
@ -255,28 +257,28 @@ function FeaturedToolUninstalledItem({
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
position='right'
|
||||
position="right"
|
||||
needsDelay={false}
|
||||
popupClassName='!p-0 !px-3 !py-2.5 !w-[224px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
|
||||
popupClassName="!p-0 !px-3 !py-2.5 !w-[224px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg"
|
||||
popupContent={(
|
||||
<div>
|
||||
<BlockIcon size='md' className='mb-2' type={BlockEnum.Tool} toolIcon={plugin.icon} />
|
||||
<div className='mb-1 text-sm leading-5 text-text-primary'>{label}</div>
|
||||
<div className='text-xs leading-[18px] text-text-secondary'>{description}</div>
|
||||
<BlockIcon size="md" className="mb-2" type={BlockEnum.Tool} toolIcon={plugin.icon} />
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{label}</div>
|
||||
<div className="text-xs leading-[18px] text-text-secondary">{description}</div>
|
||||
</div>
|
||||
)}
|
||||
disabled={!description || isActionHovered || actionOpen || isInstallModalOpen}
|
||||
>
|
||||
<div
|
||||
className='group flex h-8 w-full items-center rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
|
||||
className="group flex h-8 w-full items-center rounded-lg pl-3 pr-1 hover:bg-state-base-hover"
|
||||
>
|
||||
<div className='flex h-full min-w-0 items-center'>
|
||||
<div className="flex h-full min-w-0 items-center">
|
||||
<BlockIcon type={BlockEnum.Tool} toolIcon={plugin.icon} />
|
||||
<div className='ml-2 min-w-0'>
|
||||
<div className='system-sm-medium truncate text-text-secondary'>{label}</div>
|
||||
<div className="ml-2 min-w-0">
|
||||
<div className="system-sm-medium truncate text-text-secondary">{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-auto flex h-full items-center gap-1 pl-1'>
|
||||
<div className="ml-auto flex h-full items-center gap-1 pl-1">
|
||||
<span className={`system-xs-regular text-text-tertiary ${actionOpen ? 'hidden' : 'group-hover:hidden'}`}>{installCountLabel}</span>
|
||||
<div
|
||||
className={`system-xs-medium flex h-full items-center gap-1 text-components-button-secondary-accent-text [&_.action-btn]:h-6 [&_.action-btn]:min-h-0 [&_.action-btn]:w-6 [&_.action-btn]:rounded-lg [&_.action-btn]:p-0 ${actionOpen ? 'flex' : 'hidden group-hover:flex'}`}
|
||||
@ -287,15 +289,15 @@ function FeaturedToolUninstalledItem({
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='cursor-pointer rounded-md px-1.5 py-0.5 hover:bg-state-base-hover'
|
||||
type="button"
|
||||
className="cursor-pointer rounded-md px-1.5 py-0.5 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
setActionOpen(false)
|
||||
setIsInstallModalOpen(true)
|
||||
setIsActionHovered(true)
|
||||
}}
|
||||
>
|
||||
{t('plugin.installAction')}
|
||||
{t('installAction', { ns: 'plugin' })}
|
||||
</button>
|
||||
<Action
|
||||
open={actionOpen}
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
'use client'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum } from '../types'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from './types'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import BlockIcon from '../block-icon'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { Locale } from '@/i18n-config'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Link from 'next/link'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import TriggerPluginItem from './trigger-plugin/item'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ArrowDownDoubleLine, ArrowDownRoundFill, ArrowUpDoubleLine } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import Action from '@/app/components/workflow/block-selector/market-place-plugin/action'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { isServer } from '@/utils/client'
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { BlockEnum } from '../types'
|
||||
import TriggerPluginItem from './trigger-plugin/item'
|
||||
|
||||
const MAX_RECOMMENDED_COUNT = 15
|
||||
const INITIAL_VISIBLE_COUNT = 5
|
||||
@ -41,14 +43,14 @@ const FeaturedTriggers = ({
|
||||
const language = useGetLanguage()
|
||||
const [visibleCount, setVisibleCount] = useState(INITIAL_VISIBLE_COUNT)
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return false
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
return stored === 'true'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (stored !== null)
|
||||
@ -56,7 +58,7 @@ const FeaturedTriggers = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
@ -119,28 +121,28 @@ const FeaturedTriggers = ({
|
||||
const showEmptyState = !isLoading && totalVisible === 0
|
||||
|
||||
return (
|
||||
<div className='px-3 pb-3 pt-2'>
|
||||
<div className="px-3 pb-3 pt-2">
|
||||
<button
|
||||
type='button'
|
||||
className='flex w-full items-center rounded-md px-0 py-1 text-left text-text-primary'
|
||||
type="button"
|
||||
className="flex w-full items-center rounded-md px-0 py-1 text-left text-text-primary"
|
||||
onClick={() => setIsCollapsed(prev => !prev)}
|
||||
>
|
||||
<span className='system-xs-medium text-text-primary'>{t('workflow.tabs.featuredTools')}</span>
|
||||
<span className="system-xs-medium text-text-primary">{t('tabs.featuredTools', { ns: 'workflow' })}</span>
|
||||
<ArrowDownRoundFill className={`ml-0.5 h-4 w-4 text-text-tertiary transition-transform ${isCollapsed ? '-rotate-90' : 'rotate-0'}`} />
|
||||
</button>
|
||||
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
{isLoading && (
|
||||
<div className='py-3'>
|
||||
<Loading type='app' />
|
||||
<div className="py-3">
|
||||
<Loading type="app" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEmptyState && (
|
||||
<p className='system-xs-regular py-2 text-text-tertiary'>
|
||||
<Link className='text-text-accent' href={getMarketplaceUrl('', { category: 'trigger' })} target='_blank' rel='noopener noreferrer'>
|
||||
{t('workflow.tabs.noFeaturedTriggers')}
|
||||
<p className="system-xs-regular py-2 text-text-tertiary">
|
||||
<Link className="text-text-accent" href={getMarketplaceUrl('', { category: 'trigger' })} target="_blank" rel="noopener noreferrer">
|
||||
{t('tabs.noFeaturedTriggers', { ns: 'workflow' })}
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
@ -148,7 +150,7 @@ const FeaturedTriggers = ({
|
||||
{!showEmptyState && !isLoading && (
|
||||
<>
|
||||
{visibleInstalledProviders.length > 0 && (
|
||||
<div className='mt-1'>
|
||||
<div className="mt-1">
|
||||
{visibleInstalledProviders.map(provider => (
|
||||
<TriggerPluginItem
|
||||
key={provider.id}
|
||||
@ -161,7 +163,7 @@ const FeaturedTriggers = ({
|
||||
)}
|
||||
|
||||
{visibleUninstalledPlugins.length > 0 && (
|
||||
<div className='mt-1 flex flex-col gap-1'>
|
||||
<div className="mt-1 flex flex-col gap-1">
|
||||
{visibleUninstalledPlugins.map(plugin => (
|
||||
<FeaturedTriggerUninstalledItem
|
||||
key={plugin.plugin_id}
|
||||
@ -170,7 +172,7 @@ const FeaturedTriggers = ({
|
||||
onInstallSuccess={async () => {
|
||||
await onInstallSuccess?.()
|
||||
}}
|
||||
t={t}
|
||||
t={t as any}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -180,7 +182,7 @@ const FeaturedTriggers = ({
|
||||
|
||||
{!isLoading && totalVisible > 0 && canToggleVisibility && (
|
||||
<div
|
||||
className='group mt-1 flex cursor-pointer items-center gap-x-2 rounded-lg py-1 pl-3 pr-2 text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary'
|
||||
className="group mt-1 flex cursor-pointer items-center gap-x-2 rounded-lg py-1 pl-3 pr-2 text-text-tertiary transition-colors hover:bg-state-base-hover hover:text-text-secondary"
|
||||
onClick={() => {
|
||||
setVisibleCount((count) => {
|
||||
if (count >= maxAvailable)
|
||||
@ -190,16 +192,18 @@ const FeaturedTriggers = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center px-1 text-text-tertiary transition-colors group-hover:text-text-secondary'>
|
||||
<RiMoreLine className='size-4 group-hover:hidden' />
|
||||
{isExpanded ? (
|
||||
<ArrowUpDoubleLine className='hidden size-4 group-hover:block' />
|
||||
) : (
|
||||
<ArrowDownDoubleLine className='hidden size-4 group-hover:block' />
|
||||
)}
|
||||
<div className="flex items-center px-1 text-text-tertiary transition-colors group-hover:text-text-secondary">
|
||||
<RiMoreLine className="size-4 group-hover:hidden" />
|
||||
{isExpanded
|
||||
? (
|
||||
<ArrowUpDoubleLine className="hidden size-4 group-hover:block" />
|
||||
)
|
||||
: (
|
||||
<ArrowDownDoubleLine className="hidden size-4 group-hover:block" />
|
||||
)}
|
||||
</div>
|
||||
<div className='system-xs-regular'>
|
||||
{t(isExpanded ? 'workflow.tabs.showLessFeatured' : 'workflow.tabs.showMoreFeatured')}
|
||||
<div className="system-xs-regular">
|
||||
{t(isExpanded ? 'tabs.showLessFeatured' : 'tabs.showMoreFeatured', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -211,7 +215,7 @@ const FeaturedTriggers = ({
|
||||
|
||||
type FeaturedTriggerUninstalledItemProps = {
|
||||
plugin: Plugin
|
||||
language: string
|
||||
language: Locale
|
||||
onInstallSuccess?: () => Promise<void> | void
|
||||
t: (key: string, options?: Record<string, any>) => string
|
||||
}
|
||||
@ -224,7 +228,7 @@ function FeaturedTriggerUninstalledItem({
|
||||
}: FeaturedTriggerUninstalledItemProps) {
|
||||
const label = plugin.label?.[language] || plugin.name
|
||||
const description = typeof plugin.brief === 'object' ? plugin.brief[language] : plugin.brief
|
||||
const installCountLabel = t('plugin.install', { num: formatNumber(plugin.install_count || 0) })
|
||||
const installCountLabel = t('install', { ns: 'plugin', num: formatNumber(plugin.install_count || 0) })
|
||||
const [actionOpen, setActionOpen] = useState(false)
|
||||
const [isActionHovered, setIsActionHovered] = useState(false)
|
||||
const [isInstallModalOpen, setIsInstallModalOpen] = useState(false)
|
||||
@ -248,28 +252,28 @@ function FeaturedTriggerUninstalledItem({
|
||||
return (
|
||||
<>
|
||||
<Tooltip
|
||||
position='right'
|
||||
position="right"
|
||||
needsDelay={false}
|
||||
popupClassName='!p-0 !px-3 !py-2.5 !w-[224px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
|
||||
popupClassName="!p-0 !px-3 !py-2.5 !w-[224px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg"
|
||||
popupContent={(
|
||||
<div>
|
||||
<BlockIcon size='md' className='mb-2' type={BlockEnum.TriggerPlugin} toolIcon={plugin.icon} />
|
||||
<div className='mb-1 text-sm leading-5 text-text-primary'>{label}</div>
|
||||
<div className='text-xs leading-[18px] text-text-secondary'>{description}</div>
|
||||
<BlockIcon size="md" className="mb-2" type={BlockEnum.TriggerPlugin} toolIcon={plugin.icon} />
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{label}</div>
|
||||
<div className="text-xs leading-[18px] text-text-secondary">{description}</div>
|
||||
</div>
|
||||
)}
|
||||
disabled={!description || isActionHovered || actionOpen || isInstallModalOpen}
|
||||
>
|
||||
<div
|
||||
className='group flex h-8 w-full items-center rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
|
||||
className="group flex h-8 w-full items-center rounded-lg pl-3 pr-1 hover:bg-state-base-hover"
|
||||
>
|
||||
<div className='flex h-full min-w-0 items-center'>
|
||||
<div className="flex h-full min-w-0 items-center">
|
||||
<BlockIcon type={BlockEnum.TriggerPlugin} toolIcon={plugin.icon} />
|
||||
<div className='ml-2 min-w-0'>
|
||||
<div className='system-sm-medium truncate text-text-secondary'>{label}</div>
|
||||
<div className="ml-2 min-w-0">
|
||||
<div className="system-sm-medium truncate text-text-secondary">{label}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='ml-auto flex h-full items-center gap-1 pl-1'>
|
||||
<div className="ml-auto flex h-full items-center gap-1 pl-1">
|
||||
<span className={`system-xs-regular text-text-tertiary ${actionOpen ? 'hidden' : 'group-hover:hidden'}`}>{installCountLabel}</span>
|
||||
<div
|
||||
className={`system-xs-medium flex h-full items-center gap-1 text-components-button-secondary-accent-text [&_.action-btn]:h-6 [&_.action-btn]:min-h-0 [&_.action-btn]:w-6 [&_.action-btn]:rounded-lg [&_.action-btn]:p-0 ${actionOpen ? 'flex' : 'hidden group-hover:flex'}`}
|
||||
@ -280,15 +284,15 @@ function FeaturedTriggerUninstalledItem({
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
className='cursor-pointer rounded-md px-1.5 py-0.5 hover:bg-state-base-hover'
|
||||
type="button"
|
||||
className="cursor-pointer rounded-md px-1.5 py-0.5 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
setActionOpen(false)
|
||||
setIsInstallModalOpen(true)
|
||||
setIsActionHovered(true)
|
||||
}}
|
||||
>
|
||||
{t('plugin.installAction')}
|
||||
{t('installAction', { ns: 'plugin' })}
|
||||
</button>
|
||||
<Action
|
||||
open={actionOpen}
|
||||
|
||||
@ -17,7 +17,7 @@ export const useBlocks = () => {
|
||||
return BLOCKS.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
title: t(`workflow.blocks.${block.type}`),
|
||||
title: t(`blocks.${block.type}`, { ns: 'workflow' }),
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -28,7 +28,7 @@ export const useStartBlocks = () => {
|
||||
return START_BLOCKS.map((block) => {
|
||||
return {
|
||||
...block,
|
||||
title: t(`workflow.blocks.${block.type}`),
|
||||
title: t(`blocks.${block.type}`, { ns: 'workflow' }),
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -56,20 +56,19 @@ export const useTabs = ({
|
||||
const tabs = useMemo(() => {
|
||||
const tabConfigs = [{
|
||||
key: TabsEnum.Blocks,
|
||||
name: t('workflow.tabs.blocks'),
|
||||
name: t('tabs.blocks', { ns: 'workflow' }),
|
||||
show: !noBlocks,
|
||||
}, {
|
||||
key: TabsEnum.Sources,
|
||||
name: t('workflow.tabs.sources'),
|
||||
name: t('tabs.sources', { ns: 'workflow' }),
|
||||
show: !noSources,
|
||||
}, {
|
||||
key: TabsEnum.Tools,
|
||||
name: t('workflow.tabs.tools'),
|
||||
name: t('tabs.tools', { ns: 'workflow' }),
|
||||
show: !noTools,
|
||||
},
|
||||
{
|
||||
}, {
|
||||
key: TabsEnum.Start,
|
||||
name: t('workflow.tabs.start'),
|
||||
name: t('tabs.start', { ns: 'workflow' }),
|
||||
show: shouldShowStartTab,
|
||||
disabled: shouldDisableStartTab,
|
||||
}]
|
||||
@ -130,19 +129,19 @@ export const useToolTabs = (isHideMCPTools?: boolean) => {
|
||||
const tabs = [
|
||||
{
|
||||
key: ToolTypeEnum.All,
|
||||
name: t('workflow.tabs.allTool'),
|
||||
name: t('tabs.allTool', { ns: 'workflow' }),
|
||||
},
|
||||
{
|
||||
key: ToolTypeEnum.BuiltIn,
|
||||
name: t('workflow.tabs.plugin'),
|
||||
name: t('tabs.plugin', { ns: 'workflow' }),
|
||||
},
|
||||
{
|
||||
key: ToolTypeEnum.Custom,
|
||||
name: t('workflow.tabs.customTool'),
|
||||
name: t('tabs.customTool', { ns: 'workflow' }),
|
||||
},
|
||||
{
|
||||
key: ToolTypeEnum.Workflow,
|
||||
name: t('workflow.tabs.workflowTool'),
|
||||
name: t('tabs.workflowTool', { ns: 'workflow' }),
|
||||
},
|
||||
]
|
||||
if (!isHideMCPTools) {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { pinyin } from 'pinyin-pro'
|
||||
import type { FC, RefObject } from 'react'
|
||||
import type { ToolWithProvider } from '../types'
|
||||
import { pinyin } from 'pinyin-pro'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { CollectionType } from '../../tools/types'
|
||||
import classNames from '@/utils/classnames'
|
||||
|
||||
export const CUSTOM_GROUP_NAME = '@@@custom@@@'
|
||||
export const WORKFLOW_GROUP_NAME = '@@@workflow@@@'
|
||||
@ -86,8 +86,8 @@ const IndexBar: FC<IndexBarProps> = ({ letters, itemRefs, className }) => {
|
||||
element.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
return (
|
||||
<div className={classNames('index-bar sticky top-[20px] flex h-full w-6 flex-col items-center justify-center text-xs font-medium text-text-quaternary', className)}>
|
||||
<div className={classNames('absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]')}></div>
|
||||
<div className={cn('index-bar sticky top-[20px] flex h-full w-6 flex-col items-center justify-center text-xs font-medium text-text-quaternary', className)}>
|
||||
<div className={cn('absolute left-0 top-0 h-full w-px bg-[linear-gradient(270deg,rgba(255,255,255,0)_0%,rgba(16,24,40,0.08)_30%,rgba(16,24,40,0.08)_50%,rgba(16,24,40,0.08)_70.5%,rgba(255,255,255,0)_100%)]')}></div>
|
||||
{letters.map(letter => (
|
||||
<div className="cursor-pointer hover:text-text-secondary" key={letter} onClick={() => handleIndexClick(letter)}>
|
||||
{letter}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { NodeSelectorProps } from './main'
|
||||
import {
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import type { NodeSelectorProps } from './main'
|
||||
import NodeSelector from './main'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useStore } from '../store'
|
||||
import NodeSelector from './main'
|
||||
|
||||
const NodeSelectorWrapper = (props: NodeSelectorProps) => {
|
||||
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
|
||||
|
||||
@ -1,7 +1,17 @@
|
||||
import type {
|
||||
OffsetOptions,
|
||||
Placement,
|
||||
} from '@floating-ui/react'
|
||||
import type {
|
||||
FC,
|
||||
MouseEventHandler,
|
||||
} from 'react'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
NodeDefault,
|
||||
OnSelectBlock,
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
@ -9,31 +19,21 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNodes } from 'reactflow'
|
||||
import type {
|
||||
OffsetOptions,
|
||||
Placement,
|
||||
} from '@floating-ui/react'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
NodeDefault,
|
||||
OnSelectBlock,
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
import { BlockEnum, isTriggerNode } from '../types'
|
||||
import Tabs from './tabs'
|
||||
import { TabsEnum } from './types'
|
||||
import { useTabs } from './hooks'
|
||||
import {
|
||||
Plus02,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import Input from '@/app/components/base/input'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Input from '@/app/components/base/input'
|
||||
import {
|
||||
Plus02,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { BlockEnum, isTriggerNode } from '../types'
|
||||
import { useTabs } from './hooks'
|
||||
import Tabs from './tabs'
|
||||
import { TabsEnum } from './types'
|
||||
|
||||
export type NodeSelectorProps = {
|
||||
open?: boolean
|
||||
@ -161,16 +161,16 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
|
||||
const searchPlaceholder = useMemo(() => {
|
||||
if (activeTab === TabsEnum.Start)
|
||||
return t('workflow.tabs.searchTrigger')
|
||||
return t('tabs.searchTrigger', { ns: 'workflow' })
|
||||
|
||||
if (activeTab === TabsEnum.Blocks)
|
||||
return t('workflow.tabs.searchBlock')
|
||||
return t('tabs.searchBlock', { ns: 'workflow' })
|
||||
|
||||
if (activeTab === TabsEnum.Tools)
|
||||
return t('workflow.tabs.searchTool')
|
||||
return t('tabs.searchTool', { ns: 'workflow' })
|
||||
|
||||
if (activeTab === TabsEnum.Sources)
|
||||
return t('workflow.tabs.searchDataSource')
|
||||
return t('tabs.searchDataSource', { ns: 'workflow' })
|
||||
return ''
|
||||
}, [activeTab, t])
|
||||
|
||||
@ -190,20 +190,20 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
trigger
|
||||
? trigger(open)
|
||||
: (
|
||||
<div
|
||||
className={`
|
||||
<div
|
||||
className={`
|
||||
z-10 flex h-4
|
||||
w-4 cursor-pointer items-center justify-center rounded-full bg-components-button-primary-bg text-text-primary-on-surface hover:bg-components-button-primary-bg-hover
|
||||
${triggerClassName?.(open)}
|
||||
`}
|
||||
style={triggerStyle}
|
||||
>
|
||||
<Plus02 className='h-2.5 w-2.5' />
|
||||
</div>
|
||||
)
|
||||
style={triggerStyle}
|
||||
>
|
||||
<Plus02 className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<PortalToFollowElemContent className="z-[1000]">
|
||||
<div className={`rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg ${popupClassName}`}>
|
||||
<Tabs
|
||||
tabs={tabs}
|
||||
@ -211,8 +211,8 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
blocks={blocks}
|
||||
allowStartNodeSelection={canSelectUserInput}
|
||||
onActiveTabChange={handleActiveTabChange}
|
||||
filterElem={
|
||||
<div className='relative m-2' onClick={e => e.stopPropagation()}>
|
||||
filterElem={(
|
||||
<div className="relative m-2" onClick={e => e.stopPropagation()}>
|
||||
{activeTab === TabsEnum.Start && (
|
||||
<SearchBox
|
||||
autoFocus
|
||||
@ -221,7 +221,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
placeholder={searchPlaceholder}
|
||||
inputClassName='grow'
|
||||
inputClassName="grow"
|
||||
/>
|
||||
)}
|
||||
{activeTab === TabsEnum.Blocks && (
|
||||
@ -253,12 +253,12 @@ const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
inputClassName='grow'
|
||||
placeholder={t('searchTools', { ns: 'plugin' })!}
|
||||
inputClassName="grow"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
onSelect={handleSelect}
|
||||
searchText={searchText}
|
||||
tags={tags}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiMoreFill } from '@remixicon/react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ActionButton from '@/app/components/base/action-button'
|
||||
// import Button from '@/app/components/base/button'
|
||||
import {
|
||||
@ -11,11 +13,10 @@ import {
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useDownloadPlugin } from '@/service/use-plugins'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { downloadFile } from '@/utils/format'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
|
||||
type Props = {
|
||||
open: boolean
|
||||
@ -53,7 +54,8 @@ const OperationDropdown: FC<Props> = ({
|
||||
}), [author, name, version])
|
||||
const { data: blob, isLoading } = useDownloadPlugin(downloadInfo, needDownload)
|
||||
const handleDownload = useCallback(() => {
|
||||
if (isLoading) return
|
||||
if (isLoading)
|
||||
return
|
||||
queryClient.removeQueries({
|
||||
queryKey: ['plugins', 'downloadPlugin', downloadInfo],
|
||||
exact: true,
|
||||
@ -76,7 +78,7 @@ const OperationDropdown: FC<Props> = ({
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-end'
|
||||
placement="bottom-end"
|
||||
offset={{
|
||||
mainAxis: 0,
|
||||
crossAxis: 0,
|
||||
@ -84,13 +86,13 @@ const OperationDropdown: FC<Props> = ({
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={handleTrigger}>
|
||||
<ActionButton className={cn(open && 'bg-state-base-hover')}>
|
||||
<RiMoreFill className='h-4 w-4 text-components-button-secondary-accent-text' />
|
||||
<RiMoreFill className="h-4 w-4 text-components-button-secondary-accent-text" />
|
||||
</ActionButton>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[9999]'>
|
||||
<div className='min-w-[176px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg'>
|
||||
<div onClick={handleDownload} className='system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.download')}</div>
|
||||
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target='_blank' className='system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'>{t('common.operation.viewDetails')}</a>
|
||||
<PortalToFollowElemContent className="z-[9999]">
|
||||
<div className="min-w-[176px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg">
|
||||
<div onClick={handleDownload} className="system-md-regular cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover">{t('operation.download', { ns: 'common' })}</div>
|
||||
<a href={getMarketplaceUrl(`/plugins/${author}/${name}`, { theme })} target="_blank" className="system-md-regular block cursor-pointer rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover">{t('operation.viewDetails', { ns: 'common' })}</a>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Action from './action'
|
||||
import type { Plugin } from '@/app/components/plugins/types.ts'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import I18n from '@/context/i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
import { formatNumber } from '@/utils/format'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import Action from './action'
|
||||
|
||||
enum ActionType {
|
||||
install = 'install',
|
||||
@ -27,7 +26,7 @@ const Item: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
const getLocalizedText = (obj: Record<string, string> | undefined) =>
|
||||
obj?.[locale] || obj?.['en-US'] || obj?.en_US || ''
|
||||
const [isShowInstallModal, {
|
||||
@ -36,28 +35,28 @@ const Item: FC<Props> = ({
|
||||
}] = useBoolean(false)
|
||||
|
||||
return (
|
||||
<div className='group/plugin flex rounded-lg py-1 pl-3 pr-1 hover:bg-state-base-hover'>
|
||||
<div className="group/plugin flex rounded-lg py-1 pl-3 pr-1 hover:bg-state-base-hover">
|
||||
<div
|
||||
className='relative h-6 w-6 shrink-0 rounded-md border-[0.5px] border-components-panel-border-subtle bg-contain bg-center bg-no-repeat'
|
||||
className="relative h-6 w-6 shrink-0 rounded-md border-[0.5px] border-components-panel-border-subtle bg-contain bg-center bg-no-repeat"
|
||||
style={{ backgroundImage: `url(${payload.icon})` }}
|
||||
/>
|
||||
<div className='ml-2 flex w-0 grow'>
|
||||
<div className='w-0 grow'>
|
||||
<div className='system-sm-medium h-4 truncate leading-4 text-text-primary '>{getLocalizedText(payload.label)}</div>
|
||||
<div className='system-xs-regular h-5 truncate leading-5 text-text-tertiary'>{getLocalizedText(payload.brief)}</div>
|
||||
<div className='system-xs-regular flex space-x-1 text-text-tertiary'>
|
||||
<div className="ml-2 flex w-0 grow">
|
||||
<div className="w-0 grow">
|
||||
<div className="system-sm-medium h-4 truncate leading-4 text-text-primary ">{getLocalizedText(payload.label)}</div>
|
||||
<div className="system-xs-regular h-5 truncate leading-5 text-text-tertiary">{getLocalizedText(payload.brief)}</div>
|
||||
<div className="system-xs-regular flex space-x-1 text-text-tertiary">
|
||||
<div>{payload.org}</div>
|
||||
<div>·</div>
|
||||
<div>{t('plugin.install', { num: formatNumber(payload.install_count || 0) })}</div>
|
||||
<div>{t('install', { ns: 'plugin', num: formatNumber(payload.install_count || 0) })}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Action */}
|
||||
<div className={cn(!open ? 'hidden' : 'flex', 'system-xs-medium h-4 items-center space-x-1 text-components-button-secondary-accent-text group-hover/plugin:flex')}>
|
||||
<div
|
||||
className='cursor-pointer rounded-md px-1.5 py-0.5 hover:bg-state-base-hover'
|
||||
className="cursor-pointer rounded-md px-1.5 py-0.5 hover:bg-state-base-hover"
|
||||
onClick={showInstallModal}
|
||||
>
|
||||
{t('plugin.installAction')}
|
||||
{t('installAction', { ns: 'plugin' })}
|
||||
</div>
|
||||
<Action
|
||||
open={open}
|
||||
|
||||
@ -1,21 +1,22 @@
|
||||
'use client'
|
||||
import { useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
import type { RefObject } from 'react'
|
||||
import type { Plugin, PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { RiArrowRightUpLine, RiSearchLine } from '@remixicon/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useImperativeHandle, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import useStickyScroll, { ScrollPosition } from '../use-sticky-scroll'
|
||||
import Item from './item'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import cn from '@/utils/classnames'
|
||||
import Link from 'next/link'
|
||||
import { RiArrowRightUpLine, RiSearchLine } from '@remixicon/react'
|
||||
import { noop } from 'lodash-es'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
export type ListProps = {
|
||||
wrapElemRef: React.RefObject<HTMLElement | null>
|
||||
list: Plugin[]
|
||||
searchText: string
|
||||
tags: string[]
|
||||
category?: PluginCategoryEnum
|
||||
toolContentClassName?: string
|
||||
disableMaxWidth?: boolean
|
||||
hideFindMoreFooter?: boolean
|
||||
@ -29,6 +30,7 @@ const List = ({
|
||||
searchText,
|
||||
tags,
|
||||
list,
|
||||
category,
|
||||
toolContentClassName,
|
||||
disableMaxWidth = false,
|
||||
hideFindMoreFooter = false,
|
||||
@ -77,12 +79,12 @@ const List = ({
|
||||
|
||||
return (
|
||||
<Link
|
||||
className='system-sm-medium sticky bottom-0 z-10 flex h-8 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'
|
||||
href={getMarketplaceUrl('')}
|
||||
target='_blank'
|
||||
className="system-sm-medium sticky bottom-0 z-10 flex h-8 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"
|
||||
href={getMarketplaceUrl('', { category })}
|
||||
target="_blank"
|
||||
>
|
||||
<span>{t('plugin.findMoreInMarketplace')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
<span>{t('findMoreInMarketplace', { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="ml-0.5 h-3 w-3" />
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@ -96,15 +98,15 @@ const List = ({
|
||||
className={cn('system-sm-medium sticky z-10 flex h-8 cursor-pointer justify-between px-4 py-1 text-text-primary', stickyClassName, !disableMaxWidth && maxWidthClassName)}
|
||||
onClick={handleHeadClick}
|
||||
>
|
||||
<span>{t('plugin.fromMarketplace')}</span>
|
||||
<span>{t('fromMarketplace', { ns: 'plugin' })}</span>
|
||||
<Link
|
||||
href={urlWithSearchText}
|
||||
target='_blank'
|
||||
className='flex items-center text-text-accent-light-mode-only'
|
||||
target="_blank"
|
||||
className="flex items-center text-text-accent-light-mode-only"
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<span>{t('plugin.searchInMarketplace')}</span>
|
||||
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
|
||||
<span>{t('searchInMarketplace', { ns: 'plugin' })}</span>
|
||||
<RiArrowRightUpLine className="ml-0.5 h-3 w-3" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@ -117,15 +119,15 @@ const List = ({
|
||||
/>
|
||||
))}
|
||||
{hasRes && (
|
||||
<div className='mb-3 mt-2 flex items-center justify-center space-x-2'>
|
||||
<div className="mb-3 mt-2 flex items-center justify-center space-x-2">
|
||||
<div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(16,24,40,0.08)] to-[rgba(255,255,255,0.01)]"></div>
|
||||
<Link
|
||||
href={urlWithSearchText}
|
||||
target='_blank'
|
||||
className='system-sm-medium flex h-4 shrink-0 items-center text-text-accent-light-mode-only'
|
||||
target="_blank"
|
||||
className="system-sm-medium flex h-4 shrink-0 items-center text-text-accent-light-mode-only"
|
||||
>
|
||||
<RiSearchLine className='mr-0.5 h-3 w-3' />
|
||||
<span>{t('plugin.searchInMarketplace')}</span>
|
||||
<RiSearchLine className="mr-0.5 h-3 w-3" />
|
||||
<span>{t('searchInMarketplace', { ns: 'plugin' })}</span>
|
||||
</Link>
|
||||
<div className="h-[2px] w-[90px] bg-gradient-to-l from-[rgba(255,255,255,0.01)] to-[rgba(16,24,40,0.08)]"></div>
|
||||
</div>
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
'use client'
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import type { OnSelectBlock } from '@/app/components/workflow/types'
|
||||
import type { ViewType } from '@/app/components/workflow/block-selector/view-type-select'
|
||||
import type { OnSelectBlock } from '@/app/components/workflow/types'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import Link from 'next/link'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import { useRAGRecommendedPlugins } from '@/service/use-tools'
|
||||
import List from './list'
|
||||
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/arrows'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
|
||||
import { useRAGRecommendedPlugins } from '@/service/use-tools'
|
||||
import { isServer } from '@/utils/client'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import List from './list'
|
||||
|
||||
type RAGToolRecommendationsProps = {
|
||||
viewType: ViewType
|
||||
@ -28,14 +30,14 @@ const RAGToolRecommendations = ({
|
||||
}: RAGToolRecommendationsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [isCollapsed, setIsCollapsed] = useState<boolean>(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return false
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
return stored === 'true'
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (stored !== null)
|
||||
@ -43,7 +45,7 @@ const RAGToolRecommendations = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined')
|
||||
if (isServer)
|
||||
return
|
||||
window.localStorage.setItem(STORAGE_KEY, String(isCollapsed))
|
||||
}, [isCollapsed])
|
||||
@ -52,7 +54,7 @@ const RAGToolRecommendations = ({
|
||||
data: ragRecommendedPlugins,
|
||||
isLoading: isLoadingRAGRecommendedPlugins,
|
||||
isFetching: isFetchingRAGRecommendedPlugins,
|
||||
} = useRAGRecommendedPlugins()
|
||||
} = useRAGRecommendedPlugins('tool')
|
||||
|
||||
const recommendedPlugins = useMemo(() => {
|
||||
if (ragRecommendedPlugins)
|
||||
@ -75,33 +77,34 @@ const RAGToolRecommendations = ({
|
||||
}, [onTagsChange])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col p-1'>
|
||||
<div className="flex flex-col p-1">
|
||||
<button
|
||||
type='button'
|
||||
className='flex w-full items-center rounded-md px-3 pb-0.5 pt-1 text-left text-text-tertiary'
|
||||
type="button"
|
||||
className="flex w-full items-center rounded-md px-3 pb-0.5 pt-1 text-left text-text-tertiary"
|
||||
onClick={() => setIsCollapsed(prev => !prev)}
|
||||
>
|
||||
<span className='system-xs-medium text-text-tertiary'>{t('pipeline.ragToolSuggestions.title')}</span>
|
||||
<span className="system-xs-medium text-text-tertiary">{t('ragToolSuggestions.title', { ns: 'pipeline' })}</span>
|
||||
<ArrowDownRoundFill className={`ml-1 h-4 w-4 text-text-tertiary transition-transform ${isCollapsed ? '-rotate-90' : 'rotate-0'}`} />
|
||||
</button>
|
||||
{!isCollapsed && (
|
||||
<>
|
||||
{/* For first time loading, show loading */}
|
||||
{isLoadingRAGRecommendedPlugins && (
|
||||
<div className='py-2'>
|
||||
<Loading type='app' />
|
||||
<div className="py-2">
|
||||
<Loading type="app" />
|
||||
</div>
|
||||
)}
|
||||
{!isFetchingRAGRecommendedPlugins && recommendedPlugins.length === 0 && unInstalledPlugins.length === 0 && (
|
||||
<p className='system-xs-regular px-3 py-1 text-text-tertiary'>
|
||||
<p className="system-xs-regular px-3 py-1 text-text-tertiary">
|
||||
<Trans
|
||||
i18nKey='pipeline.ragToolSuggestions.noRecommendationPlugins'
|
||||
i18nKey="ragToolSuggestions.noRecommendationPlugins"
|
||||
ns="pipeline"
|
||||
components={{
|
||||
CustomLink: (
|
||||
<Link
|
||||
className='text-text-accent'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className="text-text-accent"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={getMarketplaceUrl('', { tags: 'rag' })}
|
||||
/>
|
||||
),
|
||||
@ -118,14 +121,14 @@ const RAGToolRecommendations = ({
|
||||
viewType={viewType}
|
||||
/>
|
||||
<div
|
||||
className='flex cursor-pointer items-center gap-x-2 py-1 pl-3 pr-2'
|
||||
className="flex cursor-pointer items-center gap-x-2 py-1 pl-3 pr-2"
|
||||
onClick={loadMore}
|
||||
>
|
||||
<div className='px-1'>
|
||||
<RiMoreLine className='size-4 text-text-tertiary' />
|
||||
<div className="px-1">
|
||||
<RiMoreLine className="size-4 text-text-tertiary" />
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-tertiary'>
|
||||
{t('common.operation.more')}
|
||||
<div className="system-xs-regular text-text-tertiary">
|
||||
{t('operation.more', { ns: 'common' })}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,15 +1,15 @@
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import type { BlockEnum, ToolWithProvider } from '../../types'
|
||||
import type { ToolDefaultValue } from '../types'
|
||||
import { ViewType } from '../view-type-select'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { groupItems } from '../index-bar'
|
||||
import cn from '@/utils/classnames'
|
||||
import ToolListTreeView from '../tool/tool-list-tree-view/list'
|
||||
import ToolListFlatView from '../tool/tool-list-flat-view/list'
|
||||
import UninstalledItem from './uninstalled-item'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import type { OnSelectBlock } from '@/app/components/workflow/types'
|
||||
import { useCallback, useMemo, useRef } from 'react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { groupItems } from '../index-bar'
|
||||
import ToolListFlatView from '../tool/tool-list-flat-view/list'
|
||||
import ToolListTreeView from '../tool/tool-list-tree-view/list'
|
||||
import { ViewType } from '../view-type-select'
|
||||
import UninstalledItem from './uninstalled-item'
|
||||
|
||||
type ListProps = {
|
||||
onSelect: OnSelectBlock
|
||||
@ -67,25 +67,27 @@ const List = ({
|
||||
return (
|
||||
<div className={cn('max-w-[100%] p-1', className)}>
|
||||
{!!tools.length && (
|
||||
isFlatView ? (
|
||||
<ToolListFlatView
|
||||
toolRefs={toolRefs}
|
||||
letters={letters}
|
||||
payload={listViewToolData}
|
||||
isShowLetterIndex={false}
|
||||
hasSearchText={false}
|
||||
onSelect={handleSelect}
|
||||
canNotSelectMultiple
|
||||
indexBar={null}
|
||||
/>
|
||||
) : (
|
||||
<ToolListTreeView
|
||||
payload={treeViewToolsData}
|
||||
hasSearchText={false}
|
||||
onSelect={handleSelect}
|
||||
canNotSelectMultiple
|
||||
/>
|
||||
)
|
||||
isFlatView
|
||||
? (
|
||||
<ToolListFlatView
|
||||
toolRefs={toolRefs}
|
||||
letters={letters}
|
||||
payload={listViewToolData}
|
||||
isShowLetterIndex={false}
|
||||
hasSearchText={false}
|
||||
onSelect={handleSelect}
|
||||
canNotSelectMultiple
|
||||
indexBar={null}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ToolListTreeView
|
||||
payload={treeViewToolsData}
|
||||
hasSearchText={false}
|
||||
onSelect={handleSelect}
|
||||
canNotSelectMultiple
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{
|
||||
unInstalledPlugins.map((item) => {
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
import React from 'react'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { Plugin } from '@/app/components/plugins/types'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import I18n from '@/context/i18n'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import { BlockEnum } from '../../types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import InstallFromMarketplace from '@/app/components/plugins/install-plugin/install-from-marketplace'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import { BlockEnum } from '../../types'
|
||||
|
||||
type UninstalledItemProps = {
|
||||
payload: Plugin
|
||||
@ -17,7 +16,7 @@ const UninstalledItem = ({
|
||||
payload,
|
||||
}: UninstalledItemProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { locale } = useContext(I18n)
|
||||
const locale = useLocale()
|
||||
|
||||
const getLocalizedText = (obj: Record<string, string> | undefined) =>
|
||||
obj?.[locale] || obj?.['en-US'] || obj?.en_US || ''
|
||||
@ -27,26 +26,26 @@ const UninstalledItem = ({
|
||||
}] = useBoolean(false)
|
||||
|
||||
return (
|
||||
<div className='flex h-8 items-center rounded-lg pl-3 pr-2 hover:bg-state-base-hover'>
|
||||
<div className="flex h-8 items-center rounded-lg pl-3 pr-2 hover:bg-state-base-hover">
|
||||
<BlockIcon
|
||||
className='shrink-0'
|
||||
className="shrink-0"
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={payload.icon}
|
||||
/>
|
||||
<div className='ml-2 flex w-0 grow items-center'>
|
||||
<div className='flex w-0 grow items-center gap-x-2'>
|
||||
<span className='system-sm-regular truncate text-text-primary'>
|
||||
<div className="ml-2 flex w-0 grow items-center">
|
||||
<div className="flex w-0 grow items-center gap-x-2">
|
||||
<span className="system-sm-regular truncate text-text-primary">
|
||||
{getLocalizedText(payload.label)}
|
||||
</span>
|
||||
<span className='system-xs-regular text-text-quaternary'>
|
||||
<span className="system-xs-regular text-text-quaternary">
|
||||
{payload.org}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className='system-xs-medium cursor-pointer pl-1.5 text-components-button-secondary-accent-text'
|
||||
className="system-xs-medium cursor-pointer pl-1.5 text-components-button-secondary-accent-text"
|
||||
onClick={showInstallModal}
|
||||
>
|
||||
{t('plugin.installAction')}
|
||||
{t('installAction', { ns: 'plugin' })}
|
||||
</div>
|
||||
{isShowInstallModal && (
|
||||
<InstallFromMarketplace
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import type { BlockEnum, CommonNodeType } from '../types'
|
||||
import type { TriggerDefaultValue } from './types'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { useAvailableNodesMetaData } from '../../workflow-app/hooks'
|
||||
import BlockIcon from '../block-icon'
|
||||
import type { BlockEnum, CommonNodeType } from '../types'
|
||||
import { BlockEnum as BlockEnumValues } from '../types'
|
||||
// import { useNodeMetaData } from '../hooks'
|
||||
import { START_BLOCKS } from './constants'
|
||||
import type { TriggerDefaultValue } from './types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useAvailableNodesMetaData } from '../../workflow-app/hooks'
|
||||
|
||||
type StartBlocksProps = {
|
||||
searchText: string
|
||||
@ -41,9 +41,9 @@ const StartBlocks = ({
|
||||
const normalizedSearch = searchText.toLowerCase()
|
||||
const getDisplayName = (blockType: BlockEnum) => {
|
||||
if (blockType === BlockEnumValues.TriggerWebhook)
|
||||
return t('workflow.customWebhook')
|
||||
return t('customWebhook', { ns: 'workflow' })
|
||||
|
||||
return t(`workflow.blocks.${blockType}`)
|
||||
return t(`blocks.${blockType}`, { ns: 'workflow' })
|
||||
}
|
||||
|
||||
return START_BLOCKS.filter((block) => {
|
||||
@ -67,48 +67,49 @@ const StartBlocks = ({
|
||||
onContentStateChange?.(!isEmpty)
|
||||
}, [isEmpty, onContentStateChange])
|
||||
|
||||
const renderBlock = useCallback((block: { type: BlockEnum; title: string; description?: string }) => (
|
||||
const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => (
|
||||
<Tooltip
|
||||
key={block.type}
|
||||
position='right'
|
||||
popupClassName='w-[224px] rounded-xl'
|
||||
position="right"
|
||||
popupClassName="w-[224px] rounded-xl"
|
||||
needsDelay={false}
|
||||
popupContent={(
|
||||
<div>
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mb-2'
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={block.type}
|
||||
/>
|
||||
<div className='system-md-medium mb-1 text-text-primary'>
|
||||
<div className="system-md-medium mb-1 text-text-primary">
|
||||
{block.type === BlockEnumValues.TriggerWebhook
|
||||
? t('workflow.customWebhook')
|
||||
: t(`workflow.blocks.${block.type}`)
|
||||
}
|
||||
? t('customWebhook', { ns: 'workflow' })
|
||||
: t(`blocks.${block.type}`, { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className='system-xs-regular text-text-secondary'>
|
||||
{t(`workflow.blocksAbout.${block.type}`)}
|
||||
<div className="system-xs-regular text-text-secondary">
|
||||
{t(`blocksAbout.${block.type}`, { ns: 'workflow' })}
|
||||
</div>
|
||||
{(block.type === BlockEnumValues.TriggerWebhook || block.type === BlockEnumValues.TriggerSchedule) && (
|
||||
<div className='system-xs-regular mb-1 mt-1 text-text-tertiary'>
|
||||
{t('tools.author')} {t('workflow.difyTeam')}
|
||||
<div className="system-xs-regular mb-1 mt-1 text-text-tertiary">
|
||||
{t('author', { ns: 'tools' })}
|
||||
{' '}
|
||||
{t('difyTeam', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className='flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover'
|
||||
className="flex h-8 w-full cursor-pointer items-center rounded-lg px-3 hover:bg-state-base-hover"
|
||||
onClick={() => onSelect(block.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
className='mr-2 shrink-0'
|
||||
className="mr-2 shrink-0"
|
||||
type={block.type}
|
||||
/>
|
||||
<div className='flex w-0 grow items-center justify-between text-sm text-text-secondary'>
|
||||
<span className='truncate'>{t(`workflow.blocks.${block.type}`)}</span>
|
||||
<div className="flex w-0 grow items-center justify-between text-sm text-text-secondary">
|
||||
<span className="truncate">{t(`blocks.${block.type}`, { ns: 'workflow' })}</span>
|
||||
{block.type === BlockEnumValues.Start && (
|
||||
<span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{t('workflow.blocks.originalStartNode')}</span>
|
||||
<span className="system-xs-regular ml-2 shrink-0 text-text-quaternary">{t('blocks.originalStartNode', { ns: 'workflow' })}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -119,14 +120,14 @@ const StartBlocks = ({
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className='p-1'>
|
||||
<div className='mb-1'>
|
||||
<div className="p-1">
|
||||
<div className="mb-1">
|
||||
{filteredBlocks.map((block, index) => (
|
||||
<div key={block.type}>
|
||||
{renderBlock(block)}
|
||||
{block.type === BlockEnumValues.Start && index < filteredBlocks.length - 1 && (
|
||||
<div className='my-1 px-3'>
|
||||
<div className='border-t border-divider-subtle' />
|
||||
<div className="my-1 px-3">
|
||||
<div className="border-t border-divider-subtle" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,24 +1,24 @@
|
||||
import type { Dispatch, FC, SetStateAction } from 'react'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
OnSelectBlock,
|
||||
ToolWithProvider,
|
||||
} from '../types'
|
||||
import { TabsEnum } from './types'
|
||||
import Blocks from './blocks'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools, useInvalidateAllBuiltInTools } from '@/service/use-tools'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import AllStartBlocks from './all-start-blocks'
|
||||
import AllTools from './all-tools'
|
||||
import Blocks from './blocks'
|
||||
import DataSources from './data-sources'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { basePath } from '@/utils/var'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { TabsEnum } from './types'
|
||||
|
||||
export type TabsProps = {
|
||||
activeTab: TabsEnum
|
||||
@ -129,7 +129,7 @@ const Tabs: FC<TabsProps> = ({
|
||||
<div onClick={e => e.stopPropagation()}>
|
||||
{
|
||||
!noBlocks && (
|
||||
<div className='relative flex bg-background-section-burn pl-1 pt-1'>
|
||||
<div className="relative flex bg-background-section-burn pl-1 pt-1">
|
||||
{
|
||||
tabs.map((tab) => {
|
||||
const commonProps = {
|
||||
@ -152,9 +152,9 @@ const Tabs: FC<TabsProps> = ({
|
||||
return (
|
||||
<Tooltip
|
||||
key={tab.key}
|
||||
position='top'
|
||||
popupClassName='max-w-[200px]'
|
||||
popupContent={t('workflow.tabs.startDisabledTip')}
|
||||
position="top"
|
||||
popupClassName="max-w-[200px]"
|
||||
popupContent={t('tabs.startDisabledTip', { ns: 'workflow' })}
|
||||
>
|
||||
<div {...commonProps}>
|
||||
{tab.name}
|
||||
@ -178,7 +178,7 @@ const Tabs: FC<TabsProps> = ({
|
||||
{filterElem}
|
||||
{
|
||||
activeTab === TabsEnum.Start && (!noBlocks || forceShowStartContent) && (
|
||||
<div className='border-t border-divider-subtle'>
|
||||
<div className="border-t border-divider-subtle">
|
||||
<AllStartBlocks
|
||||
allowUserInputSelection={allowStartNodeSelection}
|
||||
searchText={searchText}
|
||||
@ -191,7 +191,7 @@ const Tabs: FC<TabsProps> = ({
|
||||
}
|
||||
{
|
||||
activeTab === TabsEnum.Blocks && !noBlocks && (
|
||||
<div className='border-t border-divider-subtle'>
|
||||
<div className="border-t border-divider-subtle">
|
||||
<Blocks
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
@ -203,7 +203,7 @@ const Tabs: FC<TabsProps> = ({
|
||||
}
|
||||
{
|
||||
activeTab === TabsEnum.Sources && !!dataSources.length && (
|
||||
<div className='border-t border-divider-subtle'>
|
||||
<div className="border-t border-divider-subtle">
|
||||
<DataSources
|
||||
searchText={searchText}
|
||||
onSelect={onSelect}
|
||||
@ -223,7 +223,6 @@ const Tabs: FC<TabsProps> = ({
|
||||
customTools={customTools || []}
|
||||
workflowTools={workflowTools || []}
|
||||
mcpTools={mcpTools || []}
|
||||
canChooseMCPTool
|
||||
onTagsChange={onTagsChange}
|
||||
isInRAGPipeline={inRAGPipeline}
|
||||
featuredPlugins={featuredPlugins}
|
||||
|
||||
@ -1,28 +1,30 @@
|
||||
'use client'
|
||||
import type {
|
||||
OffsetOptions,
|
||||
Placement,
|
||||
} from '@floating-ui/react'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import type { CustomCollectionBackend } from '@/app/components/tools/types'
|
||||
import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type {
|
||||
OffsetOptions,
|
||||
Placement,
|
||||
} from '@floating-ui/react'
|
||||
import AllTools from '@/app/components/workflow/block-selector/all-tools'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import type { BlockEnum, OnSelectBlock } from '@/app/components/workflow/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import SearchBox from '@/app/components/plugins/marketplace/search-box'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useBoolean } from 'ahooks'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal/modal'
|
||||
import EditCustomToolModal from '@/app/components/tools/edit-custom-collection-modal'
|
||||
import AllTools from '@/app/components/workflow/block-selector/all-tools'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import {
|
||||
createCustomCollection,
|
||||
} from '@/service/tools'
|
||||
import type { CustomCollectionBackend } from '@/app/components/tools/types'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
@ -33,9 +35,7 @@ import {
|
||||
useInvalidateAllMCPTools,
|
||||
useInvalidateAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
||||
import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||
import cn from '@/utils/classnames'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type Props = {
|
||||
panelClassName?: string
|
||||
@ -50,7 +50,6 @@ type Props = {
|
||||
supportAddCustomTool?: boolean
|
||||
scope?: string
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolPicker: FC<Props> = ({
|
||||
@ -66,7 +65,6 @@ const ToolPicker: FC<Props> = ({
|
||||
scope = 'all',
|
||||
selectedTools,
|
||||
panelClassName,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
@ -119,7 +117,8 @@ const ToolPicker: FC<Props> = ({
|
||||
const handleAddedCustomTool = invalidateCustomTools
|
||||
|
||||
const handleTriggerClick = () => {
|
||||
if (disabled) return
|
||||
if (disabled)
|
||||
return
|
||||
onShowChange(true)
|
||||
}
|
||||
|
||||
@ -140,7 +139,7 @@ const ToolPicker: FC<Props> = ({
|
||||
await createCustomCollection(data)
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('common.api.actionSuccess'),
|
||||
message: t('api.actionSuccess', { ns: 'common' }),
|
||||
})
|
||||
hideEditCustomCollectionModal()
|
||||
handleAddedCustomTool()
|
||||
@ -149,7 +148,7 @@ const ToolPicker: FC<Props> = ({
|
||||
if (isShowEditCollectionToolModal) {
|
||||
return (
|
||||
<EditCustomToolModal
|
||||
positionLeft
|
||||
dialogClassName="bg-background-overlay"
|
||||
payload={null}
|
||||
onHide={hideEditCustomCollectionModal}
|
||||
onAdd={doCreateCustomToolCollection}
|
||||
@ -170,24 +169,24 @@ const ToolPicker: FC<Props> = ({
|
||||
{trigger}
|
||||
</PortalToFollowElemTrigger>
|
||||
|
||||
<PortalToFollowElemContent className='z-[1000]'>
|
||||
<PortalToFollowElemContent className="z-[1000]">
|
||||
<div className={cn('relative min-h-20 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-sm', panelClassName)}>
|
||||
<div className='p-2 pb-1'>
|
||||
<div className="p-2 pb-1">
|
||||
<SearchBox
|
||||
search={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
tags={tags}
|
||||
onTagsChange={setTags}
|
||||
placeholder={t('plugin.searchTools')!}
|
||||
placeholder={t('searchTools', { ns: 'plugin' })!}
|
||||
supportAddCustomTool={supportAddCustomTool}
|
||||
onAddedCustomTool={handleAddedCustomTool}
|
||||
onShowAddCustomCollectionModal={showEditCustomCollectionModal}
|
||||
inputClassName='grow'
|
||||
inputClassName="grow"
|
||||
/>
|
||||
</div>
|
||||
<AllTools
|
||||
className='mt-1'
|
||||
toolContentClassName='max-w-[100%]'
|
||||
className="mt-1"
|
||||
toolContentClassName="max-w-[100%]"
|
||||
tags={tags}
|
||||
searchText={searchText}
|
||||
onSelect={handleSelect as OnSelectBlock}
|
||||
@ -197,7 +196,6 @@ const ToolPicker: FC<Props> = ({
|
||||
workflowTools={workflowToolList || []}
|
||||
mcpTools={mcpTools || []}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
onTagsChange={setTags}
|
||||
featuredPlugins={featuredPlugins}
|
||||
featuredLoading={isFeaturedLoading}
|
||||
|
||||
@ -1,18 +1,24 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { ToolWithProvider } from '../../types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { ToolDefaultValue } from '../types'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import { BlockEnum } from '../../types'
|
||||
|
||||
const normalizeProviderIcon = (icon: ToolWithProvider['icon']) => {
|
||||
const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
|
||||
if (!icon)
|
||||
return icon
|
||||
if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`))
|
||||
return `${basePath}${icon}`
|
||||
return icon
|
||||
@ -36,31 +42,46 @@ const ToolItem: FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
|
||||
const language = useGetLanguage()
|
||||
const { theme } = useTheme()
|
||||
const normalizedIcon = useMemo<ToolWithProvider['icon']>(() => {
|
||||
return normalizeProviderIcon(provider.icon) ?? provider.icon
|
||||
}, [provider.icon])
|
||||
const normalizedIconDark = useMemo(() => {
|
||||
if (!provider.icon_dark)
|
||||
return undefined
|
||||
return normalizeProviderIcon(provider.icon_dark) ?? provider.icon_dark
|
||||
}, [provider.icon_dark])
|
||||
const providerIcon = useMemo(() => {
|
||||
if (theme === Theme.dark && normalizedIconDark)
|
||||
return normalizedIconDark
|
||||
return normalizedIcon
|
||||
}, [theme, normalizedIcon, normalizedIconDark])
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
key={payload.name}
|
||||
position='right'
|
||||
position="right"
|
||||
needsDelay={false}
|
||||
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
|
||||
popupClassName="!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg"
|
||||
popupContent={(
|
||||
<div>
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mb-2'
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={provider.icon}
|
||||
toolIcon={providerIcon}
|
||||
/>
|
||||
<div className='mb-1 text-sm leading-5 text-text-primary'>{payload.label[language]}</div>
|
||||
<div className='text-xs leading-[18px] text-text-secondary'>{payload.description[language]}</div>
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label[language]}</div>
|
||||
<div className="text-xs leading-[18px] text-text-secondary">{payload.description[language]}</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
key={payload.name}
|
||||
className='flex cursor-pointer items-center justify-between rounded-lg pl-[21px] pr-1 hover:bg-state-base-hover'
|
||||
className="flex cursor-pointer items-center justify-between rounded-lg pl-[21px] pr-1 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
if (disabled) return
|
||||
if (disabled)
|
||||
return
|
||||
const params: Record<string, string> = {}
|
||||
if (payload.parameters) {
|
||||
payload.parameters.forEach((item) => {
|
||||
@ -73,7 +94,8 @@ const ToolItem: FC<Props> = ({
|
||||
provider_name: provider.name,
|
||||
plugin_id: provider.plugin_id,
|
||||
plugin_unique_identifier: provider.plugin_unique_identifier,
|
||||
provider_icon: normalizeProviderIcon(provider.icon),
|
||||
provider_icon: normalizedIcon,
|
||||
provider_icon_dark: normalizedIconDark,
|
||||
tool_name: payload.name,
|
||||
tool_label: payload.label[language],
|
||||
tool_description: payload.description[language],
|
||||
@ -83,16 +105,20 @@ const ToolItem: FC<Props> = ({
|
||||
params,
|
||||
meta: provider.meta,
|
||||
})
|
||||
trackEvent('tool_selected', {
|
||||
tool_name: payload.name,
|
||||
plugin_id: provider.plugin_id,
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}>
|
||||
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
|
||||
</div>
|
||||
{isAdded && (
|
||||
<div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div>
|
||||
<div className="system-xs-regular mr-4 text-text-tertiary">{t('addToolModal.added', { ns: 'tools' })}</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip >
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
export default React.memo(ToolItem)
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { ToolWithProvider } from '../../../types'
|
||||
import type { BlockEnum } from '../../../types'
|
||||
import type { BlockEnum, ToolWithProvider } from '../../../types'
|
||||
import type { ToolDefaultValue, ToolValue } from '../../types'
|
||||
import Tool from '../tool'
|
||||
import { ViewType } from '../../view-type-select'
|
||||
import * as React from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { ViewType } from '../../view-type-select'
|
||||
import Tool from '../tool'
|
||||
|
||||
type Props = {
|
||||
payload: ToolWithProvider[]
|
||||
@ -19,7 +18,6 @@ type Props = {
|
||||
letters: string[]
|
||||
toolRefs: any
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolViewFlatView: FC<Props> = ({
|
||||
@ -33,7 +31,6 @@ const ToolViewFlatView: FC<Props> = ({
|
||||
onSelectMultiple,
|
||||
toolRefs,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const firstLetterToolIds = useMemo(() => {
|
||||
const res: Record<string, string> = {}
|
||||
@ -45,8 +42,8 @@ const ToolViewFlatView: FC<Props> = ({
|
||||
return res
|
||||
}, [payload, letters])
|
||||
return (
|
||||
<div className='flex w-full'>
|
||||
<div className='mr-1 grow'>
|
||||
<div className="flex w-full">
|
||||
<div className="mr-1 grow">
|
||||
{payload.map(tool => (
|
||||
<div
|
||||
key={tool.id}
|
||||
@ -64,7 +61,6 @@ const ToolViewFlatView: FC<Props> = ({
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { ToolWithProvider } from '../../../types'
|
||||
import Tool from '../tool'
|
||||
import type { BlockEnum } from '../../../types'
|
||||
import { ViewType } from '../../view-type-select'
|
||||
import type { BlockEnum, ToolWithProvider } from '../../../types'
|
||||
import type { ToolDefaultValue, ToolValue } from '../../types'
|
||||
import * as React from 'react'
|
||||
import { ViewType } from '../../view-type-select'
|
||||
import Tool from '../tool'
|
||||
|
||||
type Props = {
|
||||
groupName: string
|
||||
@ -15,7 +14,6 @@ type Props = {
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const Item: FC<Props> = ({
|
||||
@ -26,11 +24,10 @@ const Item: FC<Props> = ({
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>
|
||||
<div className="flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary">
|
||||
{groupName}
|
||||
</div>
|
||||
<div>
|
||||
@ -44,7 +41,6 @@ const Item: FC<Props> = ({
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import type { ToolWithProvider } from '../../../types'
|
||||
import type { BlockEnum } from '../../../types'
|
||||
import type { BlockEnum, ToolWithProvider } from '../../../types'
|
||||
import type { ToolDefaultValue, ToolValue } from '../../types'
|
||||
import Item from './item'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AGENT_GROUP_NAME, CUSTOM_GROUP_NAME, WORKFLOW_GROUP_NAME } from '../../index-bar'
|
||||
import Item from './item'
|
||||
|
||||
type Props = {
|
||||
payload: Record<string, ToolWithProvider[]>
|
||||
@ -15,7 +15,6 @@ type Props = {
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
|
||||
const ToolListTreeView: FC<Props> = ({
|
||||
@ -25,23 +24,23 @@ const ToolListTreeView: FC<Props> = ({
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const getI18nGroupName = useCallback((name: string) => {
|
||||
if (name === CUSTOM_GROUP_NAME)
|
||||
return t('workflow.tabs.customTool')
|
||||
return t('tabs.customTool', { ns: 'workflow' })
|
||||
|
||||
if (name === WORKFLOW_GROUP_NAME)
|
||||
return t('workflow.tabs.workflowTool')
|
||||
return t('tabs.workflowTool', { ns: 'workflow' })
|
||||
|
||||
if (name === AGENT_GROUP_NAME)
|
||||
return t('workflow.tabs.agent')
|
||||
return t('tabs.agent', { ns: 'workflow' })
|
||||
|
||||
return name
|
||||
}, [t])
|
||||
|
||||
if (!payload) return null
|
||||
if (!payload)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div>
|
||||
@ -55,7 +54,6 @@ const ToolListTreeView: FC<Props> = ({
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -1,24 +1,30 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import type { Tool as ToolType } from '../../../tools/types'
|
||||
import { CollectionType } from '../../../tools/types'
|
||||
import type { ToolWithProvider } from '../../types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { ToolDefaultValue, ToolValue } from '../types'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useHover } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Mcp } from '@/app/components/base/icons/src/vender/other'
|
||||
import { useMCPToolAvailability } from '@/app/components/workflow/nodes/_base/components/mcp-tool-availability'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import { CollectionType } from '../../../tools/types'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { ViewType } from '../view-type-select'
|
||||
import ActionItem from './action-item'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useHover } from 'ahooks'
|
||||
import McpToolNotSupportTooltip from '../../nodes/_base/components/mcp-tool-not-support-tooltip'
|
||||
import { Mcp } from '@/app/components/base/icons/src/vender/other'
|
||||
import { basePath } from '@/utils/var'
|
||||
|
||||
const normalizeProviderIcon = (icon: ToolWithProvider['icon']) => {
|
||||
const normalizeProviderIcon = (icon?: ToolWithProvider['icon']) => {
|
||||
if (!icon)
|
||||
return icon
|
||||
if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`))
|
||||
return `${basePath}${icon}`
|
||||
return icon
|
||||
@ -33,7 +39,6 @@ type Props = {
|
||||
canNotSelectMultiple?: boolean
|
||||
onSelectMultiple?: (type: BlockEnum, tools: ToolDefaultValue[]) => void
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
isShowLetterIndex?: boolean
|
||||
}
|
||||
|
||||
@ -46,9 +51,9 @@ const Tool: FC<Props> = ({
|
||||
canNotSelectMultiple,
|
||||
onSelectMultiple,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { allowed: isMCPToolAllowed } = useMCPToolAvailability()
|
||||
const language = useGetLanguage()
|
||||
const isFlatView = viewType === ViewType.flat
|
||||
const notShowProvider = payload.type === CollectionType.workflow
|
||||
@ -58,9 +63,24 @@ const Tool: FC<Props> = ({
|
||||
const ref = useRef(null)
|
||||
const isHovering = useHover(ref)
|
||||
const isMCPTool = payload.type === CollectionType.mcp
|
||||
const isShowCanNotChooseMCPTip = !canChooseMCPTool && isMCPTool
|
||||
const isShowCanNotChooseMCPTip = !isMCPToolAllowed && isMCPTool
|
||||
const { theme } = useTheme()
|
||||
const normalizedIcon = useMemo<ToolWithProvider['icon']>(() => {
|
||||
return normalizeProviderIcon(payload.icon) ?? payload.icon
|
||||
}, [payload.icon])
|
||||
const normalizedIconDark = useMemo(() => {
|
||||
if (!payload.icon_dark)
|
||||
return undefined
|
||||
return normalizeProviderIcon(payload.icon_dark) ?? payload.icon_dark
|
||||
}, [payload.icon_dark])
|
||||
const providerIcon = useMemo<ToolWithProvider['icon']>(() => {
|
||||
if (theme === Theme.dark && normalizedIconDark)
|
||||
return normalizedIconDark
|
||||
return normalizedIcon
|
||||
}, [theme, normalizedIcon, normalizedIconDark])
|
||||
const getIsDisabled = useCallback((tool: ToolType) => {
|
||||
if (!selectedTools || !selectedTools.length) return false
|
||||
if (!selectedTools || !selectedTools.length)
|
||||
return false
|
||||
return selectedTools.some(selectedTool => (selectedTool.provider_name === payload.name || selectedTool.provider_name === payload.id) && selectedTool.tool_name === tool.name)
|
||||
}, [payload.id, payload.name, selectedTools])
|
||||
|
||||
@ -71,8 +91,8 @@ const Tool: FC<Props> = ({
|
||||
const notShowProviderSelectInfo = useMemo(() => {
|
||||
if (isAllSelected) {
|
||||
return (
|
||||
<span className='system-xs-regular text-text-tertiary'>
|
||||
{t('tools.addToolModal.added')}
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{t('addToolModal.added', { ns: 'tools' })}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@ -80,7 +100,8 @@ const Tool: FC<Props> = ({
|
||||
const selectedInfo = useMemo(() => {
|
||||
if (isHovering && !isAllSelected) {
|
||||
return (
|
||||
<span className='system-xs-regular text-components-button-secondary-accent-text'
|
||||
<span
|
||||
className="system-xs-regular text-components-button-secondary-accent-text"
|
||||
onClick={() => {
|
||||
onSelectMultiple?.(BlockEnum.Tool, actions.filter(action => !getIsDisabled(action)).map((tool) => {
|
||||
const params: Record<string, string> = {}
|
||||
@ -95,7 +116,8 @@ const Tool: FC<Props> = ({
|
||||
provider_name: payload.name,
|
||||
plugin_id: payload.plugin_id,
|
||||
plugin_unique_identifier: payload.plugin_unique_identifier,
|
||||
provider_icon: normalizeProviderIcon(payload.icon),
|
||||
provider_icon: normalizedIcon,
|
||||
provider_icon_dark: normalizedIconDark,
|
||||
tool_name: tool.name,
|
||||
tool_label: tool.label[language],
|
||||
tool_description: tool.description[language],
|
||||
@ -107,7 +129,7 @@ const Tool: FC<Props> = ({
|
||||
}))
|
||||
}}
|
||||
>
|
||||
{t('workflow.tabs.addAll')}
|
||||
{t('tabs.addAll', { ns: 'workflow' })}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@ -116,11 +138,10 @@ const Tool: FC<Props> = ({
|
||||
return <></>
|
||||
|
||||
return (
|
||||
<span className='system-xs-regular text-text-tertiary'>
|
||||
<span className="system-xs-regular text-text-tertiary">
|
||||
{isAllSelected
|
||||
? t('workflow.tabs.allAdded')
|
||||
: `${selectedToolsNum} / ${totalToolsNum}`
|
||||
}
|
||||
? t('tabs.allAdded', { ns: 'workflow' })
|
||||
: `${selectedToolsNum} / ${totalToolsNum}`}
|
||||
</span>
|
||||
)
|
||||
}, [actions, getIsDisabled, isAllSelected, isHovering, language, onSelectMultiple, payload.id, payload.is_team_authorization, payload.name, payload.type, selectedToolsNum, t, totalToolsNum])
|
||||
@ -141,10 +162,10 @@ const Tool: FC<Props> = ({
|
||||
return payload.author
|
||||
|
||||
if (payload.type === CollectionType.custom)
|
||||
return t('workflow.tabs.customTool')
|
||||
return t('tabs.customTool', { ns: 'workflow' })
|
||||
|
||||
if (payload.type === CollectionType.workflow)
|
||||
return t('workflow.tabs.workflowTool')
|
||||
return t('tabs.workflowTool', { ns: 'workflow' })
|
||||
|
||||
return ''
|
||||
}, [payload.author, payload.type, t])
|
||||
@ -157,7 +178,7 @@ const Tool: FC<Props> = ({
|
||||
>
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
className='group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
|
||||
className="group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
if (hasAction) {
|
||||
setFold(!isFold)
|
||||
@ -177,7 +198,8 @@ const Tool: FC<Props> = ({
|
||||
provider_name: payload.name,
|
||||
plugin_id: payload.plugin_id,
|
||||
plugin_unique_identifier: payload.plugin_unique_identifier,
|
||||
provider_icon: normalizeProviderIcon(payload.icon),
|
||||
provider_icon: normalizedIcon,
|
||||
provider_icon_dark: normalizedIconDark,
|
||||
tool_name: tool.name,
|
||||
tool_label: tool.label[language],
|
||||
tool_description: tool.description[language],
|
||||
@ -190,20 +212,20 @@ const Tool: FC<Props> = ({
|
||||
>
|
||||
<div className={cn('flex h-8 grow items-center', isShowCanNotChooseMCPTip && 'opacity-30')}>
|
||||
<BlockIcon
|
||||
className='shrink-0'
|
||||
className="shrink-0"
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={payload.icon}
|
||||
toolIcon={providerIcon}
|
||||
/>
|
||||
<div className='ml-2 flex w-0 grow items-center text-sm text-text-primary'>
|
||||
<span className='max-w-[250px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
|
||||
<div className="ml-2 flex w-0 grow items-center text-sm text-text-primary">
|
||||
<span className="max-w-[250px] truncate">{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
|
||||
{isFlatView && groupName && (
|
||||
<span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{groupName}</span>
|
||||
<span className="system-xs-regular ml-2 shrink-0 text-text-quaternary">{groupName}</span>
|
||||
)}
|
||||
{isMCPTool && <Mcp className='ml-2 size-3.5 shrink-0 text-text-quaternary' />}
|
||||
{isMCPTool && <Mcp className="ml-2 size-3.5 shrink-0 text-text-quaternary" />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='ml-2 flex items-center'>
|
||||
<div className="ml-2 flex items-center">
|
||||
{!isShowCanNotChooseMCPTip && !canNotSelectMultiple && (notShowProvider ? notShowProviderSelectInfo : selectedInfo)}
|
||||
{isShowCanNotChooseMCPTip && <McpToolNotSupportTooltip />}
|
||||
{hasAction && (
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { memo, useMemo, useRef } from 'react'
|
||||
import type { BlockEnum, ToolWithProvider } from '../types'
|
||||
import IndexBar, { groupItems } from './index-bar'
|
||||
import type { ToolDefaultValue, ToolValue } from './types'
|
||||
import type { ToolTypeEnum } from './types'
|
||||
import { ViewType } from './view-type-select'
|
||||
import Empty from '@/app/components/tools/add-tool-modal/empty'
|
||||
import type { ToolDefaultValue, ToolTypeEnum, ToolValue } from './types'
|
||||
import { memo, useMemo, useRef } from 'react'
|
||||
import Empty from '@/app/components/tools/provider/empty'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import ToolListTreeView from './tool/tool-list-tree-view/list'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import IndexBar, { groupItems } from './index-bar'
|
||||
import ToolListFlatView from './tool/tool-list-flat-view/list'
|
||||
import classNames from '@/utils/classnames'
|
||||
import ToolListTreeView from './tool/tool-list-tree-view/list'
|
||||
import { ViewType } from './view-type-select'
|
||||
|
||||
type ToolsProps = {
|
||||
onSelect: (type: BlockEnum, tool: ToolDefaultValue) => void
|
||||
@ -22,7 +21,6 @@ type ToolsProps = {
|
||||
className?: string
|
||||
indexBarClassName?: string
|
||||
selectedTools?: ToolValue[]
|
||||
canChooseMCPTool?: boolean
|
||||
}
|
||||
const Tools = ({
|
||||
onSelect,
|
||||
@ -36,7 +34,6 @@ const Tools = ({
|
||||
className,
|
||||
indexBarClassName,
|
||||
selectedTools,
|
||||
canChooseMCPTool,
|
||||
}: ToolsProps) => {
|
||||
// const tools: any = []
|
||||
const language = useGetLanguage()
|
||||
@ -91,38 +88,38 @@ const Tools = ({
|
||||
const toolRefs = useRef({})
|
||||
|
||||
return (
|
||||
<div className={classNames('max-w-[100%] p-1', className)}>
|
||||
<div className={cn('max-w-[100%] p-1', className)}>
|
||||
{!tools.length && !hasSearchText && (
|
||||
<div className='py-10'>
|
||||
<div className="py-10">
|
||||
<Empty type={toolType!} isAgent={isAgent} />
|
||||
</div>
|
||||
)}
|
||||
{!!tools.length && (
|
||||
isFlatView ? (
|
||||
<ToolListFlatView
|
||||
toolRefs={toolRefs}
|
||||
letters={letters}
|
||||
payload={listViewToolData}
|
||||
isShowLetterIndex={isShowLetterIndex}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
|
||||
/>
|
||||
) : (
|
||||
<ToolListTreeView
|
||||
payload={treeViewToolsData}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
canChooseMCPTool={canChooseMCPTool}
|
||||
/>
|
||||
)
|
||||
isFlatView
|
||||
? (
|
||||
<ToolListFlatView
|
||||
toolRefs={toolRefs}
|
||||
letters={letters}
|
||||
payload={listViewToolData}
|
||||
isShowLetterIndex={isShowLetterIndex}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
indexBar={<IndexBar letters={letters} itemRefs={toolRefs} className={indexBarClassName} />}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<ToolListTreeView
|
||||
payload={treeViewToolsData}
|
||||
hasSearchText={hasSearchText}
|
||||
onSelect={onSelect}
|
||||
canNotSelectMultiple={canNotSelectMultiple}
|
||||
onSelectMultiple={onSelectMultiple}
|
||||
selectedTools={selectedTools}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,15 +1,14 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React from 'react'
|
||||
import type { TriggerWithProvider } from '../types'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
|
||||
import type { Event } from '@/app/components/tools/types'
|
||||
import { BlockEnum } from '../../types'
|
||||
import type { TriggerDefaultValue } from '../types'
|
||||
import * as React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import BlockIcon from '../../block-icon'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum } from '../../types'
|
||||
|
||||
type Props = {
|
||||
provider: TriggerWithProvider
|
||||
@ -32,27 +31,28 @@ const TriggerPluginActionItem: FC<Props> = ({
|
||||
return (
|
||||
<Tooltip
|
||||
key={payload.name}
|
||||
position='right'
|
||||
position="right"
|
||||
needsDelay={false}
|
||||
popupClassName='!p-0 !px-3 !py-2.5 !w-[224px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
|
||||
popupClassName="!p-0 !px-3 !py-2.5 !w-[224px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg"
|
||||
popupContent={(
|
||||
<div>
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mb-2'
|
||||
size="md"
|
||||
className="mb-2"
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
toolIcon={provider.icon}
|
||||
/>
|
||||
<div className='mb-1 text-sm leading-5 text-text-primary'>{payload.label[language]}</div>
|
||||
<div className='text-xs leading-[18px] text-text-secondary'>{payload.description[language]}</div>
|
||||
<div className="mb-1 text-sm leading-5 text-text-primary">{payload.label[language]}</div>
|
||||
<div className="text-xs leading-[18px] text-text-secondary">{payload.description[language]}</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
<div
|
||||
key={payload.name}
|
||||
className='flex cursor-pointer items-center justify-between rounded-lg pl-[21px] pr-1 hover:bg-state-base-hover'
|
||||
className="flex cursor-pointer items-center justify-between rounded-lg pl-[21px] pr-1 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
if (disabled) return
|
||||
if (disabled)
|
||||
return
|
||||
const params: Record<string, string> = {}
|
||||
if (payload.parameters) {
|
||||
payload.parameters.forEach((item: any) => {
|
||||
@ -81,10 +81,10 @@ const TriggerPluginActionItem: FC<Props> = ({
|
||||
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
|
||||
</div>
|
||||
{isAdded && (
|
||||
<div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div>
|
||||
<div className="system-xs-regular mr-4 text-text-tertiary">{t('addToolModal.added', { ns: 'tools' })}</div>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip >
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
export default React.memo(TriggerPluginActionItem)
|
||||
|
||||
@ -1,16 +1,28 @@
|
||||
'use client'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect, useMemo, useRef } from 'react'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import BlockIcon from '@/app/components/workflow/block-icon'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { basePath } from '@/utils/var'
|
||||
import TriggerPluginActionItem from './action-item'
|
||||
|
||||
const normalizeProviderIcon = (icon?: TriggerWithProvider['icon']) => {
|
||||
if (!icon)
|
||||
return icon
|
||||
if (typeof icon === 'string' && basePath && icon.startsWith('/') && !icon.startsWith(`${basePath}/`))
|
||||
return `${basePath}${icon}`
|
||||
return icon
|
||||
}
|
||||
|
||||
type Props = {
|
||||
className?: string
|
||||
payload: TriggerWithProvider
|
||||
@ -26,6 +38,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const { theme } = useTheme()
|
||||
const notShowProvider = payload.type === CollectionType.workflow
|
||||
const actions = payload.events
|
||||
const hasAction = !notShowProvider
|
||||
@ -48,13 +61,30 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
return payload.author
|
||||
|
||||
if (payload.type === CollectionType.custom)
|
||||
return t('workflow.tabs.customTool')
|
||||
return t('tabs.customTool', { ns: 'workflow' })
|
||||
|
||||
if (payload.type === CollectionType.workflow)
|
||||
return t('workflow.tabs.workflowTool')
|
||||
return t('tabs.workflowTool', { ns: 'workflow' })
|
||||
|
||||
return payload.author || ''
|
||||
}, [payload.author, payload.type, t])
|
||||
const normalizedIcon = useMemo<TriggerWithProvider['icon']>(() => {
|
||||
return normalizeProviderIcon(payload.icon) ?? payload.icon
|
||||
}, [payload.icon])
|
||||
const normalizedIconDark = useMemo(() => {
|
||||
if (!payload.icon_dark)
|
||||
return undefined
|
||||
return normalizeProviderIcon(payload.icon_dark) ?? payload.icon_dark
|
||||
}, [payload.icon_dark])
|
||||
const providerIcon = useMemo<TriggerWithProvider['icon']>(() => {
|
||||
if (theme === Theme.dark && normalizedIconDark)
|
||||
return normalizedIconDark
|
||||
return normalizedIcon
|
||||
}, [normalizedIcon, normalizedIconDark, theme])
|
||||
const providerWithResolvedIcon = useMemo(() => ({
|
||||
...payload,
|
||||
icon: providerIcon,
|
||||
}), [payload, providerIcon])
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -64,7 +94,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
>
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
className='group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
|
||||
className="group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover"
|
||||
onClick={() => {
|
||||
if (hasAction) {
|
||||
setFold(!isFold)
|
||||
@ -95,19 +125,19 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
})
|
||||
}}
|
||||
>
|
||||
<div className='flex h-8 grow items-center'>
|
||||
<div className="flex h-8 grow items-center">
|
||||
<BlockIcon
|
||||
className='shrink-0'
|
||||
className="shrink-0"
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
toolIcon={payload.icon}
|
||||
toolIcon={providerIcon}
|
||||
/>
|
||||
<div className='ml-2 flex min-w-0 flex-1 items-center text-sm text-text-primary'>
|
||||
<span className='max-w-[200px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
|
||||
<span className='system-xs-regular ml-2 truncate text-text-quaternary'>{groupName}</span>
|
||||
<div className="ml-2 flex min-w-0 flex-1 items-center text-sm text-text-primary">
|
||||
<span className="max-w-[200px] truncate">{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
|
||||
<span className="system-xs-regular ml-2 truncate text-text-quaternary">{groupName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='ml-2 flex items-center'>
|
||||
<div className="ml-2 flex items-center">
|
||||
{hasAction && (
|
||||
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-tertiary group-hover/item:text-text-tertiary', isFold && 'text-text-quaternary')} />
|
||||
)}
|
||||
@ -118,7 +148,7 @@ const TriggerPluginItem: FC<Props> = ({
|
||||
actions.map(action => (
|
||||
<TriggerPluginActionItem
|
||||
key={action.name}
|
||||
provider={payload}
|
||||
provider={providerWithResolvedIcon}
|
||||
payload={action}
|
||||
onSelect={onSelect}
|
||||
disabled={false}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import TriggerPluginItem from './item'
|
||||
import type { BlockEnum } from '../../types'
|
||||
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
|
||||
import { memo, useEffect, useMemo } from 'react'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import TriggerPluginItem from './item'
|
||||
|
||||
type TriggerPluginListProps = {
|
||||
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { ParametersSchema, PluginMeta, PluginTriggerSubscriptionConstructor, SupportedCreationMethods, TriggerEvent } from '../../plugins/types'
|
||||
import type { Collection, Event } from '../../tools/types'
|
||||
import type { TypeWithI18N } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
|
||||
export enum TabsEnum {
|
||||
Start = 'start',
|
||||
@ -39,9 +39,9 @@ export type TriggerDefaultValue = PluginCommonDefaultValue & {
|
||||
title: string
|
||||
plugin_unique_identifier: string
|
||||
is_team_authorization: boolean
|
||||
params: Record<string, any>
|
||||
paramSchemas: Record<string, any>[]
|
||||
output_schema: Record<string, any>
|
||||
params: Record<string, unknown>
|
||||
paramSchemas: Record<string, unknown>[]
|
||||
output_schema: Record<string, unknown>
|
||||
subscription_id?: string
|
||||
meta?: PluginMeta
|
||||
}
|
||||
@ -52,13 +52,14 @@ export type ToolDefaultValue = PluginCommonDefaultValue & {
|
||||
tool_description: string
|
||||
title: string
|
||||
is_team_authorization: boolean
|
||||
params: Record<string, any>
|
||||
paramSchemas: Record<string, any>[]
|
||||
output_schema?: Record<string, any>
|
||||
params: Record<string, unknown>
|
||||
paramSchemas: Record<string, unknown>[]
|
||||
output_schema?: Record<string, unknown>
|
||||
credential_id?: string
|
||||
meta?: PluginMeta
|
||||
plugin_id?: string
|
||||
provider_icon?: Collection['icon']
|
||||
provider_icon_dark?: Collection['icon']
|
||||
plugin_unique_identifier?: string
|
||||
}
|
||||
|
||||
@ -81,10 +82,10 @@ export type ToolValue = {
|
||||
tool_name: string
|
||||
tool_label: string
|
||||
tool_description?: string
|
||||
settings?: Record<string, any>
|
||||
parameters?: Record<string, any>
|
||||
settings?: Record<string, unknown>
|
||||
parameters?: Record<string, unknown>
|
||||
enabled?: boolean
|
||||
extra?: Record<string, any>
|
||||
extra?: { description?: string } & Record<string, unknown>
|
||||
credential_id?: string
|
||||
}
|
||||
|
||||
@ -93,12 +94,12 @@ export type DataSourceItem = {
|
||||
plugin_unique_identifier: string
|
||||
provider: string
|
||||
declaration: {
|
||||
credentials_schema: any[]
|
||||
credentials_schema: unknown[]
|
||||
provider_type: string
|
||||
identity: {
|
||||
author: string
|
||||
description: TypeWithI18N
|
||||
icon: string | { background: string; content: string }
|
||||
icon: string | { background: string, content: string }
|
||||
label: TypeWithI18N
|
||||
name: string
|
||||
tags: string[]
|
||||
@ -107,15 +108,15 @@ export type DataSourceItem = {
|
||||
description: TypeWithI18N
|
||||
identity: {
|
||||
author: string
|
||||
icon?: string | { background: string; content: string }
|
||||
icon?: string | { background: string, content: string }
|
||||
label: TypeWithI18N
|
||||
name: string
|
||||
provider: string
|
||||
}
|
||||
parameters: any[]
|
||||
parameters: unknown[]
|
||||
output_schema?: {
|
||||
type: string
|
||||
properties: Record<string, any>
|
||||
properties: Record<string, unknown>
|
||||
}
|
||||
}[]
|
||||
}
|
||||
@ -129,18 +130,18 @@ export type TriggerParameter = {
|
||||
label: TypeWithI18N
|
||||
description?: TypeWithI18N
|
||||
type: 'string' | 'number' | 'boolean' | 'select' | 'file' | 'files'
|
||||
| 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select'
|
||||
| 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select'
|
||||
auto_generate?: {
|
||||
type: string
|
||||
value?: any
|
||||
value?: unknown
|
||||
} | null
|
||||
template?: {
|
||||
type: string
|
||||
value?: any
|
||||
value?: unknown
|
||||
} | null
|
||||
scope?: string | null
|
||||
required?: boolean
|
||||
default?: any
|
||||
default?: unknown
|
||||
min?: number | null
|
||||
max?: number | null
|
||||
precision?: number | null
|
||||
@ -153,11 +154,11 @@ export type TriggerParameter = {
|
||||
|
||||
export type TriggerCredentialField = {
|
||||
type: 'secret-input' | 'text-input' | 'select' | 'boolean'
|
||||
| 'app-selector' | 'model-selector' | 'tools-selector'
|
||||
| 'app-selector' | 'model-selector' | 'tools-selector'
|
||||
name: string
|
||||
scope?: string | null
|
||||
required: boolean
|
||||
default?: string | number | boolean | Array<any> | null
|
||||
default?: string | number | boolean | Array<unknown> | null
|
||||
options?: Array<{
|
||||
value: string
|
||||
label: TypeWithI18N
|
||||
@ -190,7 +191,7 @@ export type TriggerApiEntity = {
|
||||
identity: TriggerIdentity
|
||||
description: TypeWithI18N
|
||||
parameters: TriggerParameter[]
|
||||
output_schema?: Record<string, any>
|
||||
output_schema?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type TriggerProviderApiEntity = {
|
||||
@ -236,32 +237,15 @@ type TriggerSubscriptionStructure = {
|
||||
name: string
|
||||
provider: string
|
||||
credential_type: TriggerCredentialTypeEnum
|
||||
credentials: TriggerSubCredentials
|
||||
credentials: Record<string, unknown>
|
||||
endpoint: string
|
||||
parameters: TriggerSubParameters
|
||||
properties: TriggerSubProperties
|
||||
parameters: Record<string, unknown>
|
||||
properties: Record<string, unknown>
|
||||
workflows_in_use: number
|
||||
}
|
||||
|
||||
export type TriggerSubscription = TriggerSubscriptionStructure
|
||||
|
||||
export type TriggerSubCredentials = {
|
||||
access_tokens: string
|
||||
}
|
||||
|
||||
export type TriggerSubParameters = {
|
||||
repository: string
|
||||
webhook_secret?: string
|
||||
}
|
||||
|
||||
export type TriggerSubProperties = {
|
||||
active: boolean
|
||||
events: string[]
|
||||
external_id: string
|
||||
repository: string
|
||||
webhook_secret?: string
|
||||
}
|
||||
|
||||
export type TriggerSubscriptionBuilder = TriggerSubscriptionStructure
|
||||
|
||||
// OAuth configuration types
|
||||
@ -274,7 +258,7 @@ export type TriggerOAuthConfig = {
|
||||
params: {
|
||||
client_id: string
|
||||
client_secret: string
|
||||
[key: string]: any
|
||||
[key: string]: string
|
||||
}
|
||||
system_configured: boolean
|
||||
}
|
||||
|
||||
@ -5,7 +5,8 @@ const useCheckVerticalScrollbar = (ref: React.RefObject<HTMLElement>) => {
|
||||
|
||||
useEffect(() => {
|
||||
const elem = ref.current
|
||||
if (!elem) return
|
||||
if (!elem)
|
||||
return
|
||||
|
||||
const checkScrollbar = () => {
|
||||
setHasVerticalScrollbar(elem.scrollHeight > elem.clientHeight)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { useThrottleFn } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
|
||||
export enum ScrollPosition {
|
||||
belowTheWrap = 'belowTheWrap',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
import type { DataSourceItem } from './types'
|
||||
import type { Tool } from '@/app/components/tools/types'
|
||||
|
||||
export const transformDataSourceToTool = (dataSourceItem: DataSourceItem) => {
|
||||
return {
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useCallback } from 'react'
|
||||
import { RiNodeTree, RiSortAlphabetAsc } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export enum ViewType {
|
||||
flat = 'flat',
|
||||
@ -27,30 +28,26 @@ const ViewTypeSelect: FC<Props> = ({
|
||||
}, [viewType, onChange])
|
||||
|
||||
return (
|
||||
<div className='flex items-center rounded-lg bg-components-segmented-control-bg-normal p-px'>
|
||||
<div className="flex items-center rounded-lg bg-components-segmented-control-bg-normal p-px">
|
||||
<div
|
||||
className={
|
||||
cn('rounded-lg p-[3px]',
|
||||
viewType === ViewType.flat
|
||||
? 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-xs'
|
||||
: 'cursor-pointer text-text-tertiary',
|
||||
)
|
||||
cn('rounded-lg p-[3px]', viewType === ViewType.flat
|
||||
? 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-xs'
|
||||
: 'cursor-pointer text-text-tertiary')
|
||||
}
|
||||
onClick={handleChange(ViewType.flat)}
|
||||
>
|
||||
<RiSortAlphabetAsc className='h-4 w-4' />
|
||||
<RiSortAlphabetAsc className="h-4 w-4" />
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
cn('rounded-lg p-[3px]',
|
||||
viewType === ViewType.tree
|
||||
? 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-xs'
|
||||
: 'cursor-pointer text-text-tertiary',
|
||||
)
|
||||
cn('rounded-lg p-[3px]', viewType === ViewType.tree
|
||||
? 'bg-components-segmented-control-item-active-bg text-text-accent-light-mode-only shadow-xs'
|
||||
: 'cursor-pointer text-text-tertiary')
|
||||
}
|
||||
onClick={handleChange(ViewType.tree)}
|
||||
>
|
||||
<RiNodeTree className='h-4 w-4 ' />
|
||||
<RiNodeTree className="h-4 w-4 " />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
115
web/app/components/workflow/candidate-node-main.tsx
Normal file
115
web/app/components/workflow/candidate-node-main.tsx
Normal file
@ -0,0 +1,115 @@
|
||||
import type {
|
||||
FC,
|
||||
} from 'react'
|
||||
import type {
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { useEventListener } from 'ahooks'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
import { CUSTOM_NODE } from './constants'
|
||||
import { useAutoGenerateWebhookUrl, useNodesInteractions, useNodesSyncDraft, useWorkflowHistory, WorkflowHistoryEvent } from './hooks'
|
||||
import CustomNode from './nodes'
|
||||
import CustomNoteNode from './note-node'
|
||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from './store'
|
||||
import { BlockEnum } from './types'
|
||||
import { getIterationStartNode, getLoopStartNode } from './utils'
|
||||
|
||||
type Props = {
|
||||
candidateNode: Node
|
||||
}
|
||||
const CandidateNodeMain: FC<Props> = ({
|
||||
candidateNode,
|
||||
}) => {
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const mousePosition = useStore(s => s.mousePosition)
|
||||
const { zoom } = useViewport()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
useEventListener('click', (e) => {
|
||||
e.preventDefault()
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.push({
|
||||
...candidateNode,
|
||||
data: {
|
||||
...candidateNode.data,
|
||||
_isCandidate: false,
|
||||
},
|
||||
position: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
})
|
||||
if (candidateNode.data.type === BlockEnum.Iteration)
|
||||
draft.push(getIterationStartNode(candidateNode.id))
|
||||
|
||||
if (candidateNode.data.type === BlockEnum.Loop)
|
||||
draft.push(getLoopStartNode(candidateNode.id))
|
||||
})
|
||||
setNodes(newNodes)
|
||||
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NoteAdd, { nodeId: candidateNode.id })
|
||||
else
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: candidateNode.id })
|
||||
|
||||
workflowStore.setState({ candidateNode: undefined })
|
||||
|
||||
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
||||
handleNodeSelect(candidateNode.id)
|
||||
|
||||
if (candidateNode.data.type === BlockEnum.TriggerWebhook) {
|
||||
handleSyncWorkflowDraft(true, true, {
|
||||
onSuccess: () => autoGenerateWebhookUrl(candidateNode.id),
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
useEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
workflowStore.setState({ candidateNode: undefined })
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute z-10"
|
||||
style={{
|
||||
left: mousePosition.elementX,
|
||||
top: mousePosition.elementY,
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: '0 0',
|
||||
}}
|
||||
>
|
||||
{
|
||||
candidateNode.type === CUSTOM_NODE && (
|
||||
<CustomNode {...candidateNode as any} />
|
||||
)
|
||||
}
|
||||
{
|
||||
candidateNode.type === CUSTOM_NOTE_NODE && (
|
||||
<CustomNoteNode {...candidateNode as any} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CandidateNodeMain)
|
||||
@ -1,114 +1,19 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import { produce } from 'immer'
|
||||
import {
|
||||
useReactFlow,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import { useEventListener } from 'ahooks'
|
||||
|
||||
import CandidateNodeMain from './candidate-node-main'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from './store'
|
||||
import { WorkflowHistoryEvent, useAutoGenerateWebhookUrl, useNodesInteractions, useNodesSyncDraft, useWorkflowHistory } from './hooks'
|
||||
import { CUSTOM_NODE } from './constants'
|
||||
import { getIterationStartNode, getLoopStartNode } from './utils'
|
||||
import CustomNode from './nodes'
|
||||
import CustomNoteNode from './note-node'
|
||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||
import { BlockEnum } from './types'
|
||||
import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow'
|
||||
|
||||
const CandidateNode = () => {
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const candidateNode = useStore(s => s.candidateNode)
|
||||
const mousePosition = useStore(s => s.mousePosition)
|
||||
const { zoom } = useViewport()
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
|
||||
useEventListener('click', (e) => {
|
||||
const { candidateNode, mousePosition } = workflowStore.getState()
|
||||
|
||||
if (candidateNode) {
|
||||
e.preventDefault()
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.push({
|
||||
...candidateNode,
|
||||
data: {
|
||||
...candidateNode.data,
|
||||
_isCandidate: false,
|
||||
},
|
||||
position: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
})
|
||||
if (candidateNode.data.type === BlockEnum.Iteration)
|
||||
draft.push(getIterationStartNode(candidateNode.id))
|
||||
|
||||
if (candidateNode.data.type === BlockEnum.Loop)
|
||||
draft.push(getLoopStartNode(candidateNode.id))
|
||||
})
|
||||
setNodes(newNodes)
|
||||
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
||||
saveStateToHistory(WorkflowHistoryEvent.NoteAdd, { nodeId: candidateNode.id })
|
||||
else
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: candidateNode.id })
|
||||
|
||||
workflowStore.setState({ candidateNode: undefined })
|
||||
|
||||
if (candidateNode.type === CUSTOM_NOTE_NODE)
|
||||
handleNodeSelect(candidateNode.id)
|
||||
|
||||
if (candidateNode.data.type === BlockEnum.TriggerWebhook) {
|
||||
handleSyncWorkflowDraft(true, true, {
|
||||
onSuccess: () => autoGenerateWebhookUrl(candidateNode.id),
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
useEventListener('contextmenu', (e) => {
|
||||
const { candidateNode } = workflowStore.getState()
|
||||
if (candidateNode) {
|
||||
e.preventDefault()
|
||||
workflowStore.setState({ candidateNode: undefined })
|
||||
}
|
||||
})
|
||||
|
||||
if (!candidateNode)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-10'
|
||||
style={{
|
||||
left: mousePosition.elementX,
|
||||
top: mousePosition.elementY,
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: '0 0',
|
||||
}}
|
||||
>
|
||||
{
|
||||
candidateNode.type === CUSTOM_NODE && (
|
||||
<CustomNode {...candidateNode as any} />
|
||||
)
|
||||
}
|
||||
{
|
||||
candidateNode.type === CUSTOM_NOTE_NODE && (
|
||||
<CustomNoteNode {...candidateNode as any} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<CandidateNodeMain candidateNode={candidateNode} />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { LoroDoc } from 'loro-crdt'
|
||||
import { CollaborationManager } from '../collaboration-manager'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { LoroDoc } from 'loro-crdt'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { CollaborationManager } from '../collaboration-manager'
|
||||
|
||||
const NODE_ID = 'node-1'
|
||||
const LLM_NODE_ID = 'llm-node'
|
||||
const PARAM_NODE_ID = 'parameter-node'
|
||||
|
||||
const createNode = (variables: string[]): Node => ({
|
||||
const createNode = (variables: string[]): Node<Record<string, any>> => ({
|
||||
id: NODE_ID,
|
||||
type: 'custom',
|
||||
position: { x: 0, y: 0 },
|
||||
@ -29,13 +29,14 @@ const createNode = (variables: string[]): Node => ({
|
||||
},
|
||||
})
|
||||
|
||||
const createLLMNode = (templates: Array<{ id: string; role: string; text: string }>): Node => ({
|
||||
const createLLMNode = (templates: Array<{ id: string, role: string, text: string }>): Node<Record<string, any>> => ({
|
||||
id: LLM_NODE_ID,
|
||||
type: 'custom',
|
||||
position: { x: 200, y: 200 },
|
||||
data: {
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
desc: '',
|
||||
selected: false,
|
||||
model: {
|
||||
mode: 'chat',
|
||||
@ -56,13 +57,14 @@ const createLLMNode = (templates: Array<{ id: string; role: string; text: string
|
||||
},
|
||||
})
|
||||
|
||||
const createParameterExtractorNode = (parameters: Array<{ description: string; name: string; required: boolean; type: string }>): Node => ({
|
||||
const createParameterExtractorNode = (parameters: Array<{ description: string, name: string, required: boolean, type: string }>): Node<Record<string, any>> => ({
|
||||
id: PARAM_NODE_ID,
|
||||
type: 'custom',
|
||||
position: { x: 400, y: 120 },
|
||||
data: {
|
||||
type: BlockEnum.ParameterExtractor,
|
||||
title: 'ParameterExtractor',
|
||||
desc: '',
|
||||
selected: true,
|
||||
model: {
|
||||
mode: 'chat',
|
||||
@ -91,21 +93,25 @@ const getManager = (doc: LoroDoc) => {
|
||||
|
||||
const deepClone = <T>(value: T): T => JSON.parse(JSON.stringify(value))
|
||||
|
||||
const syncNodes = (manager: CollaborationManager, previous: Node[], next: Node[]) => {
|
||||
;(manager as any).syncNodes(previous, next)
|
||||
}
|
||||
|
||||
const exportNodes = (manager: CollaborationManager) => manager.getNodes()
|
||||
|
||||
describe('Loro merge behavior smoke test', () => {
|
||||
it('inspects concurrent edits after merge', () => {
|
||||
const docA = new LoroDoc()
|
||||
const managerA = getManager(docA)
|
||||
managerA.syncNodes([], [createNode(['a'])])
|
||||
syncNodes(managerA, [], [createNode(['a'])])
|
||||
|
||||
const snapshot = docA.export({ mode: 'snapshot' })
|
||||
|
||||
const docB = LoroDoc.fromSnapshot(snapshot)
|
||||
const managerB = getManager(docB)
|
||||
|
||||
managerA.syncNodes([createNode(['a'])], [createNode(['a', 'b'])])
|
||||
managerB.syncNodes([createNode(['a'])], [createNode(['a', 'c'])])
|
||||
syncNodes(managerA, [createNode(['a'])], [createNode(['a', 'b'])])
|
||||
syncNodes(managerB, [createNode(['a'])], [createNode(['a', 'c'])])
|
||||
|
||||
const updateForA = docB.export({ mode: 'update', from: docA.version() })
|
||||
docA.import(updateForA)
|
||||
@ -134,7 +140,7 @@ describe('Loro merge behavior smoke test', () => {
|
||||
|
||||
const docA = new LoroDoc()
|
||||
const managerA = getManager(docA)
|
||||
managerA.syncNodes([], [createLLMNode(deepClone(baseTemplate))])
|
||||
syncNodes(managerA, [], [createLLMNode(deepClone(baseTemplate))])
|
||||
|
||||
const snapshot = docA.export({ mode: 'snapshot' })
|
||||
const docB = LoroDoc.fromSnapshot(snapshot)
|
||||
@ -148,7 +154,7 @@ describe('Loro merge behavior smoke test', () => {
|
||||
text: 'hello from docA',
|
||||
},
|
||||
]
|
||||
managerA.syncNodes([createLLMNode(deepClone(baseTemplate))], [createLLMNode(deepClone(additionTemplate))])
|
||||
syncNodes(managerA, [createLLMNode(deepClone(baseTemplate))], [createLLMNode(deepClone(additionTemplate))])
|
||||
|
||||
const editedTemplate = [
|
||||
{
|
||||
@ -157,7 +163,7 @@ describe('Loro merge behavior smoke test', () => {
|
||||
text: 'updated by docB',
|
||||
},
|
||||
]
|
||||
managerB.syncNodes([createLLMNode(deepClone(baseTemplate))], [createLLMNode(deepClone(editedTemplate))])
|
||||
syncNodes(managerB, [createLLMNode(deepClone(baseTemplate))], [createLLMNode(deepClone(editedTemplate))])
|
||||
|
||||
const updateForA = docB.export({ mode: 'update', from: docA.version() })
|
||||
docA.import(updateForA)
|
||||
@ -196,7 +202,7 @@ describe('Loro merge behavior smoke test', () => {
|
||||
|
||||
const docA = new LoroDoc()
|
||||
const managerA = getManager(docA)
|
||||
managerA.syncNodes([], [createParameterExtractorNode(deepClone(baseParameters))])
|
||||
syncNodes(managerA, [], [createParameterExtractorNode(deepClone(baseParameters))])
|
||||
|
||||
const snapshot = docA.export({ mode: 'snapshot' })
|
||||
const docB = LoroDoc.fromSnapshot(snapshot)
|
||||
@ -207,13 +213,13 @@ describe('Loro merge behavior smoke test', () => {
|
||||
{ description: 'dd', name: 'cc', required: false, type: 'string' },
|
||||
{ description: 'new from A', name: 'ee', required: false, type: 'number' },
|
||||
]
|
||||
managerA.syncNodes([createParameterExtractorNode(deepClone(baseParameters))], [createParameterExtractorNode(deepClone(docAUpdate))])
|
||||
syncNodes(managerA, [createParameterExtractorNode(deepClone(baseParameters))], [createParameterExtractorNode(deepClone(docAUpdate))])
|
||||
|
||||
const docBUpdate = [
|
||||
{ description: 'bb', name: 'aa', required: false, type: 'string' },
|
||||
{ description: 'dd updated by B', name: 'cc', required: true, type: 'string' },
|
||||
]
|
||||
managerB.syncNodes([createParameterExtractorNode(deepClone(baseParameters))], [createParameterExtractorNode(deepClone(docBUpdate))])
|
||||
syncNodes(managerB, [createParameterExtractorNode(deepClone(baseParameters))], [createParameterExtractorNode(deepClone(docBUpdate))])
|
||||
|
||||
const updateForA = docB.export({ mode: 'update', from: docA.version() })
|
||||
docA.import(updateForA)
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import type { NodePanelPresenceMap, NodePanelPresenceUser } from '@/app/components/workflow/collaboration/types/collaboration'
|
||||
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import { LoroDoc } from 'loro-crdt'
|
||||
import { Position } from 'reactflow'
|
||||
import { CollaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { Edge, Node } from '@/app/components/workflow/types'
|
||||
import type { NodePanelPresenceMap, NodePanelPresenceUser } from '@/app/components/workflow/collaboration/types/collaboration'
|
||||
|
||||
const NODE_ID = '1760342909316'
|
||||
|
||||
@ -56,21 +57,21 @@ const createNodeSnapshot = (variableNames: string[]): Node<{ variables: Workflow
|
||||
selected: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left',
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
data: {
|
||||
selected: true,
|
||||
title: '开始',
|
||||
desc: '',
|
||||
type: BlockEnum.Start,
|
||||
variables: variableNames.map(createVariable),
|
||||
variables: variableNames.map(name => createVariable(name)),
|
||||
},
|
||||
})
|
||||
|
||||
const LLM_NODE_ID = 'llm-node'
|
||||
const PARAM_NODE_ID = 'param-extractor-node'
|
||||
|
||||
const createLLMNodeSnapshot = (promptTemplates: PromptTemplateItem[]): Node<any> => ({
|
||||
const createLLMNodeSnapshot = (promptTemplates: PromptTemplateItem[]): Node<Record<string, any>> => ({
|
||||
id: LLM_NODE_ID,
|
||||
type: 'custom',
|
||||
position: { x: 200, y: 120 },
|
||||
@ -80,11 +81,12 @@ const createLLMNodeSnapshot = (promptTemplates: PromptTemplateItem[]): Node<any>
|
||||
selected: false,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left',
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
data: {
|
||||
type: 'llm',
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
desc: '',
|
||||
selected: false,
|
||||
context: {
|
||||
enabled: false,
|
||||
@ -105,7 +107,7 @@ const createLLMNodeSnapshot = (promptTemplates: PromptTemplateItem[]): Node<any>
|
||||
},
|
||||
})
|
||||
|
||||
const createParameterExtractorNodeSnapshot = (parameters: ParameterItem[]): Node<any> => ({
|
||||
const createParameterExtractorNodeSnapshot = (parameters: ParameterItem[]): Node<Record<string, any>> => ({
|
||||
id: PARAM_NODE_ID,
|
||||
type: 'custom',
|
||||
position: { x: 420, y: 220 },
|
||||
@ -115,11 +117,12 @@ const createParameterExtractorNodeSnapshot = (parameters: ParameterItem[]): Node
|
||||
selected: true,
|
||||
selectable: true,
|
||||
draggable: true,
|
||||
sourcePosition: 'right',
|
||||
targetPosition: 'left',
|
||||
sourcePosition: Position.Right,
|
||||
targetPosition: Position.Left,
|
||||
data: {
|
||||
type: 'parameter-extractor',
|
||||
type: BlockEnum.ParameterExtractor,
|
||||
title: '参数提取器',
|
||||
desc: '',
|
||||
selected: true,
|
||||
model: {
|
||||
mode: 'chat',
|
||||
@ -391,12 +394,12 @@ describe('CollaborationManager syncNodes', () => {
|
||||
])
|
||||
;(promptManager as any).syncNodes([], [deepClone(base)])
|
||||
|
||||
const storedBefore = promptManager.getNodes().find(node => node.id === LLM_NODE_ID)
|
||||
const storedBefore = promptManager.getNodes().find(node => node.id === LLM_NODE_ID) as Node<Record<string, any>> | undefined
|
||||
const firstTemplate = (storedBefore?.data as any).prompt_template?.[0]
|
||||
expect(firstTemplate?.text).toBe('base')
|
||||
|
||||
// simulate consumer mutating the plain JSON array and syncing back
|
||||
const mutatedNode = deepClone(storedBefore!)
|
||||
const mutatedNode = deepClone(storedBefore!) as Node<Record<string, any>>
|
||||
mutatedNode.data.prompt_template.push({
|
||||
id: 'user',
|
||||
role: 'user',
|
||||
@ -424,8 +427,8 @@ describe('CollaborationManager syncNodes', () => {
|
||||
const node = createParameterExtractorNodeSnapshot(initialParameters)
|
||||
;(parameterManager as any).syncNodes([], [deepClone(node)])
|
||||
|
||||
const stored = parameterManager.getNodes().find(n => n.id === PARAM_NODE_ID)!
|
||||
const mutatedNode = deepClone(stored)
|
||||
const stored = parameterManager.getNodes().find(n => n.id === PARAM_NODE_ID) as Node<Record<string, any>>
|
||||
const mutatedNode = deepClone(stored) as Node<Record<string, any>>
|
||||
mutatedNode.data.parameters[0].description = 'updated'
|
||||
|
||||
;(parameterManager as any).syncNodes([stored], [mutatedNode])
|
||||
@ -437,14 +440,16 @@ describe('CollaborationManager syncNodes', () => {
|
||||
})
|
||||
|
||||
it('filters out transient/private data keys while keeping allowlisted ones', () => {
|
||||
const nodeWithPrivate: Node = {
|
||||
const nodeWithPrivate: Node<Record<string, any>> = {
|
||||
id: 'private-node',
|
||||
type: 'custom',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: 'private',
|
||||
desc: '',
|
||||
_foo: 'should disappear',
|
||||
_children: ['child-a'],
|
||||
_children: [{ nodeId: 'child-a', nodeType: BlockEnum.Start }],
|
||||
selected: true,
|
||||
variables: [],
|
||||
},
|
||||
@ -454,7 +459,7 @@ describe('CollaborationManager syncNodes', () => {
|
||||
|
||||
const stored = (manager.getNodes() as Node[]).find(node => node.id === 'private-node')!
|
||||
expect((stored.data as any)._foo).toBeUndefined()
|
||||
expect((stored.data as any)._children).toEqual(['child-a'])
|
||||
expect((stored.data as any)._children).toEqual([{ nodeId: 'child-a', nodeType: BlockEnum.Start }])
|
||||
expect((stored.data as any).selected).toBeUndefined()
|
||||
})
|
||||
|
||||
@ -508,20 +513,32 @@ describe('CollaborationManager syncNodes', () => {
|
||||
source: 'node-a',
|
||||
target: 'node-b',
|
||||
type: 'default',
|
||||
data: { label: 'initial' },
|
||||
} as Edge
|
||||
data: {
|
||||
sourceType: BlockEnum.Start,
|
||||
targetType: BlockEnum.LLM,
|
||||
_waitingRun: false,
|
||||
},
|
||||
}
|
||||
|
||||
;(edgeManager as any).setEdges([], [edge])
|
||||
expect(edgeManager.getEdges()).toHaveLength(1)
|
||||
expect((edgeManager.getEdges()[0].data as any).label).toBe('initial')
|
||||
const storedEdge = edgeManager.getEdges()[0]!
|
||||
expect(storedEdge.data).toBeDefined()
|
||||
expect(storedEdge.data!._waitingRun).toBe(false)
|
||||
|
||||
const updatedEdge: Edge = {
|
||||
...edge,
|
||||
data: { label: 'updated' },
|
||||
data: {
|
||||
sourceType: BlockEnum.Start,
|
||||
targetType: BlockEnum.LLM,
|
||||
_waitingRun: true,
|
||||
},
|
||||
}
|
||||
;(edgeManager as any).setEdges([edge], [updatedEdge])
|
||||
expect(edgeManager.getEdges()).toHaveLength(1)
|
||||
expect((edgeManager.getEdges()[0].data as any).label).toBe('updated')
|
||||
const updatedStoredEdge = edgeManager.getEdges()[0]!
|
||||
expect(updatedStoredEdge.data).toBeDefined()
|
||||
expect(updatedStoredEdge.data!._waitingRun).toBe(true)
|
||||
|
||||
;(edgeManager as any).setEdges([updatedEdge], [])
|
||||
expect(edgeManager.getEdges()).toHaveLength(0)
|
||||
@ -532,11 +549,29 @@ describe('CollaborationManager public API wrappers', () => {
|
||||
let manager: CollaborationManager
|
||||
const baseNodes: Node[] = []
|
||||
const updatedNodes: Node[] = [
|
||||
{ id: 'new-node', type: 'custom', position: { x: 0, y: 0 }, data: {} } as Node,
|
||||
{
|
||||
id: 'new-node',
|
||||
type: 'custom',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: BlockEnum.Start,
|
||||
title: 'New node',
|
||||
desc: '',
|
||||
},
|
||||
},
|
||||
]
|
||||
const baseEdges: Edge[] = []
|
||||
const updatedEdges: Edge[] = [
|
||||
{ id: 'edge-1', source: 'source', target: 'target', type: 'default', data: {} } as Edge,
|
||||
{
|
||||
id: 'edge-1',
|
||||
source: 'source',
|
||||
target: 'target',
|
||||
type: 'default',
|
||||
data: {
|
||||
sourceType: BlockEnum.Start,
|
||||
targetType: BlockEnum.End,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
beforeEach(() => {
|
||||
@ -544,9 +579,9 @@ describe('CollaborationManager public API wrappers', () => {
|
||||
})
|
||||
|
||||
it('setNodes delegates to syncNodes and commits the CRDT document', () => {
|
||||
const commit = jest.fn()
|
||||
const commit = vi.fn()
|
||||
;(manager as any).doc = { commit }
|
||||
const syncSpy = jest.spyOn(manager as any, 'syncNodes').mockImplementation(() => undefined)
|
||||
const syncSpy = vi.spyOn(manager as any, 'syncNodes').mockImplementation(() => undefined)
|
||||
|
||||
manager.setNodes(baseNodes, updatedNodes)
|
||||
|
||||
@ -556,10 +591,10 @@ describe('CollaborationManager public API wrappers', () => {
|
||||
})
|
||||
|
||||
it('setNodes skips syncing when undo/redo replay is running', () => {
|
||||
const commit = jest.fn()
|
||||
const commit = vi.fn()
|
||||
;(manager as any).doc = { commit }
|
||||
;(manager as any).isUndoRedoInProgress = true
|
||||
const syncSpy = jest.spyOn(manager as any, 'syncNodes').mockImplementation(() => undefined)
|
||||
const syncSpy = vi.spyOn(manager as any, 'syncNodes').mockImplementation(() => undefined)
|
||||
|
||||
manager.setNodes(baseNodes, updatedNodes)
|
||||
|
||||
@ -569,9 +604,9 @@ describe('CollaborationManager public API wrappers', () => {
|
||||
})
|
||||
|
||||
it('setEdges delegates to syncEdges and commits the CRDT document', () => {
|
||||
const commit = jest.fn()
|
||||
const commit = vi.fn()
|
||||
;(manager as any).doc = { commit }
|
||||
const syncSpy = jest.spyOn(manager as any, 'syncEdges').mockImplementation(() => undefined)
|
||||
const syncSpy = vi.spyOn(manager as any, 'syncEdges').mockImplementation(() => undefined)
|
||||
|
||||
manager.setEdges(baseEdges, updatedEdges)
|
||||
|
||||
@ -581,7 +616,7 @@ describe('CollaborationManager public API wrappers', () => {
|
||||
})
|
||||
|
||||
it('disconnect tears down the collaboration state only when last connection closes', () => {
|
||||
const forceSpy = jest.spyOn(manager as any, 'forceDisconnect').mockImplementation(() => undefined)
|
||||
const forceSpy = vi.spyOn(manager as any, 'forceDisconnect').mockImplementation(() => undefined)
|
||||
;(manager as any).activeConnections.add('conn-a')
|
||||
;(manager as any).activeConnections.add('conn-b')
|
||||
|
||||
|
||||
@ -2,9 +2,9 @@ import type { Socket } from 'socket.io-client'
|
||||
import { CRDTProvider } from '../crdt-provider'
|
||||
|
||||
type FakeDoc = {
|
||||
export: jest.Mock<Uint8Array, [options?: { mode?: string }]>
|
||||
import: jest.Mock<void, [Uint8Array]>
|
||||
subscribe: jest.Mock<void, [(payload: any) => void]>
|
||||
export: ReturnType<typeof vi.fn>
|
||||
import: ReturnType<typeof vi.fn>
|
||||
subscribe: ReturnType<typeof vi.fn>
|
||||
trigger: (event: any) => void
|
||||
}
|
||||
|
||||
@ -12,9 +12,9 @@ const createFakeDoc = (): FakeDoc => {
|
||||
let handler: ((payload: any) => void) | null = null
|
||||
|
||||
return {
|
||||
export: jest.fn(() => new Uint8Array([1, 2, 3])),
|
||||
import: jest.fn(),
|
||||
subscribe: jest.fn((cb: (payload: any) => void) => {
|
||||
export: vi.fn(() => new Uint8Array([1, 2, 3])),
|
||||
import: vi.fn(),
|
||||
subscribe: vi.fn((cb: (payload: any) => void) => {
|
||||
handler = cb
|
||||
}),
|
||||
trigger: (event: any) => {
|
||||
@ -27,11 +27,11 @@ const createMockSocket = () => {
|
||||
const handlers = new Map<string, (...args: any[]) => void>()
|
||||
|
||||
const socket: any = {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn((event: string, handler: (...args: any[]) => void) => {
|
||||
emit: vi.fn(),
|
||||
on: vi.fn((event: string, handler: (...args: any[]) => void) => {
|
||||
handlers.set(event, handler)
|
||||
}),
|
||||
off: jest.fn((event: string) => {
|
||||
off: vi.fn((event: string) => {
|
||||
handlers.delete(event)
|
||||
}),
|
||||
trigger: (event: string, ...args: any[]) => {
|
||||
@ -106,7 +106,7 @@ describe('CRDTProvider', () => {
|
||||
|
||||
const provider = new CRDTProvider(socket, doc as unknown as any)
|
||||
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined)
|
||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined)
|
||||
|
||||
socket.trigger('graph_update', new Uint8Array([1]))
|
||||
expect(errorSpy).toHaveBeenCalledWith('Error importing graph update:', expect.any(Error))
|
||||
|
||||
@ -3,7 +3,7 @@ import { EventEmitter } from '../event-emitter'
|
||||
describe('EventEmitter', () => {
|
||||
it('registers and invokes handlers via on/emit', () => {
|
||||
const emitter = new EventEmitter()
|
||||
const handler = jest.fn()
|
||||
const handler = vi.fn()
|
||||
|
||||
emitter.on('test', handler)
|
||||
emitter.emit('test', { value: 42 })
|
||||
@ -13,8 +13,8 @@ describe('EventEmitter', () => {
|
||||
|
||||
it('removes specific handler with off', () => {
|
||||
const emitter = new EventEmitter()
|
||||
const handlerA = jest.fn()
|
||||
const handlerB = jest.fn()
|
||||
const handlerA = vi.fn()
|
||||
const handlerB = vi.fn()
|
||||
|
||||
emitter.on('test', handlerA)
|
||||
emitter.on('test', handlerB)
|
||||
@ -28,8 +28,8 @@ describe('EventEmitter', () => {
|
||||
|
||||
it('clears all listeners when off is called without handler', () => {
|
||||
const emitter = new EventEmitter()
|
||||
const handlerA = jest.fn()
|
||||
const handlerB = jest.fn()
|
||||
const handlerA = vi.fn()
|
||||
const handlerB = vi.fn()
|
||||
|
||||
emitter.on('trigger', handlerA)
|
||||
emitter.on('trigger', handlerB)
|
||||
@ -44,8 +44,8 @@ describe('EventEmitter', () => {
|
||||
|
||||
it('removeAllListeners clears every registered event', () => {
|
||||
const emitter = new EventEmitter()
|
||||
emitter.on('one', jest.fn())
|
||||
emitter.on('two', jest.fn())
|
||||
emitter.on('one', vi.fn())
|
||||
emitter.on('two', vi.fn())
|
||||
|
||||
emitter.removeAllListeners()
|
||||
|
||||
@ -55,7 +55,7 @@ describe('EventEmitter', () => {
|
||||
|
||||
it('returns an unsubscribe function from on', () => {
|
||||
const emitter = new EventEmitter()
|
||||
const handler = jest.fn()
|
||||
const handler = vi.fn()
|
||||
|
||||
const unsubscribe = emitter.on('detach', handler)
|
||||
unsubscribe()
|
||||
@ -67,14 +67,14 @@ describe('EventEmitter', () => {
|
||||
|
||||
it('continues emitting when a handler throws', () => {
|
||||
const emitter = new EventEmitter()
|
||||
const errorHandler = jest
|
||||
const errorHandler = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation()
|
||||
.mockImplementation(() => undefined)
|
||||
|
||||
const failingHandler = jest.fn(() => {
|
||||
const failingHandler = vi.fn(() => {
|
||||
throw new Error('boom')
|
||||
})
|
||||
const succeedingHandler = jest.fn()
|
||||
const succeedingHandler = vi.fn()
|
||||
|
||||
emitter.on('safe', failingHandler)
|
||||
emitter.on('safe', succeedingHandler)
|
||||
|
||||
@ -1,24 +1,29 @@
|
||||
import type { Socket } from 'socket.io-client'
|
||||
|
||||
const ioMock = jest.fn()
|
||||
const ioMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
jest.mock('socket.io-client', () => ({
|
||||
vi.mock('socket.io-client', () => ({
|
||||
io: (...args: any[]) => ioMock(...args),
|
||||
}))
|
||||
|
||||
const createMockSocket = (id: string): Socket & {
|
||||
type MockSocket = Socket & {
|
||||
trigger: (event: string, ...args: any[]) => void
|
||||
} => {
|
||||
emit: ReturnType<typeof vi.fn>
|
||||
on: ReturnType<typeof vi.fn>
|
||||
disconnect: ReturnType<typeof vi.fn>
|
||||
}
|
||||
|
||||
const createMockSocket = (id: string): MockSocket => {
|
||||
const handlers = new Map<string, (...args: any[]) => void>()
|
||||
|
||||
const socket: any = {
|
||||
id,
|
||||
connected: true,
|
||||
emit: jest.fn(),
|
||||
disconnect: jest.fn(() => {
|
||||
emit: vi.fn(),
|
||||
disconnect: vi.fn(() => {
|
||||
socket.connected = false
|
||||
}),
|
||||
on: jest.fn((event: string, handler: (...args: any[]) => void) => {
|
||||
on: vi.fn((event: string, handler: (...args: any[]) => void) => {
|
||||
handlers.set(event, handler)
|
||||
}),
|
||||
trigger: (event: string, ...args: any[]) => {
|
||||
@ -28,14 +33,14 @@ const createMockSocket = (id: string): Socket & {
|
||||
},
|
||||
}
|
||||
|
||||
return socket as Socket & { trigger: (event: string, ...args: any[]) => void }
|
||||
return socket as MockSocket
|
||||
}
|
||||
|
||||
describe('WebSocketClient', () => {
|
||||
let originalWindow: typeof window | undefined
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetModules()
|
||||
vi.resetModules()
|
||||
ioMock.mockReset()
|
||||
originalWindow = globalThis.window
|
||||
})
|
||||
@ -87,7 +92,7 @@ describe('WebSocketClient', () => {
|
||||
|
||||
it('attaches auth token from localStorage and emits user_connect on connect', async () => {
|
||||
const mockSocket = createMockSocket('socket-auth')
|
||||
ioMock.mockImplementation((url, options) => {
|
||||
ioMock.mockImplementation((url: string, options: { auth?: { token?: string } }) => {
|
||||
expect(options.auth).toEqual({ token: 'secret-token' })
|
||||
return mockSocket
|
||||
})
|
||||
@ -95,7 +100,7 @@ describe('WebSocketClient', () => {
|
||||
globalThis.window = {
|
||||
location: { protocol: 'https:', host: 'example.com' },
|
||||
localStorage: {
|
||||
getItem: jest.fn(() => 'secret-token'),
|
||||
getItem: vi.fn(() => 'secret-token'),
|
||||
},
|
||||
} as unknown as typeof window
|
||||
|
||||
|
||||
@ -1,9 +1,4 @@
|
||||
import { LoroDoc, LoroList, LoroMap, UndoManager } from 'loro-crdt'
|
||||
import { cloneDeep, isEqual } from 'lodash-es'
|
||||
import type { Socket } from 'socket.io-client'
|
||||
import { emitWithAuthGuard, webSocketClient } from './websocket-manager'
|
||||
import { CRDTProvider } from './crdt-provider'
|
||||
import { EventEmitter } from './event-emitter'
|
||||
import type {
|
||||
CommonNodeType,
|
||||
Edge,
|
||||
@ -17,6 +12,12 @@ import type {
|
||||
NodePanelPresenceUser,
|
||||
OnlineUser,
|
||||
} from '../types/collaboration'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import { LoroDoc, LoroList, LoroMap, UndoManager } from 'loro-crdt'
|
||||
import { CRDTProvider } from './crdt-provider'
|
||||
import { EventEmitter } from './event-emitter'
|
||||
import { emitWithAuthGuard, webSocketClient } from './websocket-manager'
|
||||
|
||||
type NodePanelPresenceEventData = {
|
||||
nodeId: string
|
||||
@ -151,13 +152,16 @@ export class CollaborationManager {
|
||||
container.set('sourcePosition', node.sourcePosition)
|
||||
container.set('targetPosition', node.targetPosition)
|
||||
|
||||
if (node.width === undefined) container.delete('width')
|
||||
if (node.width === undefined)
|
||||
container.delete('width')
|
||||
else container.set('width', node.width)
|
||||
|
||||
if (node.height === undefined) container.delete('height')
|
||||
if (node.height === undefined)
|
||||
container.delete('height')
|
||||
else container.set('height', node.height)
|
||||
|
||||
if (node.selected === undefined) container.delete('selected')
|
||||
if (node.selected === undefined)
|
||||
container.delete('selected')
|
||||
else container.set('selected', node.selected)
|
||||
|
||||
const optionalProps: Array<keyof Node> = [
|
||||
@ -192,7 +196,8 @@ export class CollaborationManager {
|
||||
const handledKeys = new Set<string>()
|
||||
|
||||
Object.entries(node.data || {}).forEach(([key, value]) => {
|
||||
if (!this.shouldSyncDataKey(key)) return
|
||||
if (!this.shouldSyncDataKey(key))
|
||||
return
|
||||
handledKeys.add(key)
|
||||
|
||||
if (listFields.has(key))
|
||||
@ -203,8 +208,10 @@ export class CollaborationManager {
|
||||
|
||||
const existingData = dataContainer.toJSON() || {}
|
||||
Object.keys(existingData).forEach((key) => {
|
||||
if (!this.shouldSyncDataKey(key)) return
|
||||
if (handledKeys.has(key)) return
|
||||
if (!this.shouldSyncDataKey(key))
|
||||
return
|
||||
if (handledKeys.has(key))
|
||||
return
|
||||
|
||||
dataContainer.delete(key)
|
||||
})
|
||||
@ -311,7 +318,8 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
setNodes = (oldNodes: Node[], newNodes: Node[]): void => {
|
||||
if (!this.doc) return
|
||||
if (!this.doc)
|
||||
return
|
||||
|
||||
// Don't track operations during undo/redo to prevent loops
|
||||
if (this.isUndoRedoInProgress)
|
||||
@ -322,7 +330,8 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
setEdges = (oldEdges: Edge[], newEdges: Edge[]): void => {
|
||||
if (!this.doc) return
|
||||
if (!this.doc)
|
||||
return
|
||||
|
||||
// Don't track operations during undo/redo to prevent loops
|
||||
if (this.isUndoRedoInProgress)
|
||||
@ -466,7 +475,8 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
getNodes(): Node[] {
|
||||
if (!this.nodesMap) return []
|
||||
if (!this.nodesMap)
|
||||
return []
|
||||
return Array.from(this.nodesMap.keys()).map(id => this.exportNode(id as string))
|
||||
}
|
||||
|
||||
@ -475,7 +485,8 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
emitCursorMove(position: CursorPosition): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
|
||||
const socket = this.getActiveSocket()
|
||||
if (!socket)
|
||||
@ -490,7 +501,8 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
emitSyncRequest(): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
|
||||
this.sendCollaborationEvent({
|
||||
type: 'sync_request',
|
||||
@ -500,7 +512,8 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
emitWorkflowUpdate(appId: string): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
|
||||
this.sendCollaborationEvent({
|
||||
type: 'workflow_update',
|
||||
@ -510,10 +523,12 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
emitNodePanelPresence(nodeId: string, isOpen: boolean, user: NodePanelPresenceUser): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
|
||||
const socket = this.getActiveSocket()
|
||||
if (!socket || !nodeId || !user?.userId) return
|
||||
if (!socket || !nodeId || !user?.userId)
|
||||
return
|
||||
|
||||
const payload: NodePanelPresenceEventData = {
|
||||
nodeId,
|
||||
@ -548,7 +563,7 @@ export class CollaborationManager {
|
||||
return this.eventEmitter.on('onlineUsers', callback)
|
||||
}
|
||||
|
||||
onWorkflowUpdate(callback: (update: { appId: string; timestamp: number }) => void): () => void {
|
||||
onWorkflowUpdate(callback: (update: { appId: string, timestamp: number }) => void): () => void {
|
||||
return this.eventEmitter.on('workflowUpdate', callback)
|
||||
}
|
||||
|
||||
@ -582,12 +597,13 @@ export class CollaborationManager {
|
||||
return this.eventEmitter.on('leaderChange', callback)
|
||||
}
|
||||
|
||||
onCommentsUpdate(callback: (update: { appId: string; timestamp: number }) => void): () => void {
|
||||
onCommentsUpdate(callback: (update: { appId: string, timestamp: number }) => void): () => void {
|
||||
return this.eventEmitter.on('commentsUpdate', callback)
|
||||
}
|
||||
|
||||
emitCommentsUpdate(appId: string): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
|
||||
this.sendCollaborationEvent({
|
||||
type: 'comments_update',
|
||||
@ -596,7 +612,7 @@ export class CollaborationManager {
|
||||
})
|
||||
}
|
||||
|
||||
onUndoRedoStateChange(callback: (state: { canUndo: boolean; canRedo: boolean }) => void): () => void {
|
||||
onUndoRedoStateChange(callback: (state: { canUndo: boolean, canRedo: boolean }) => void): () => void {
|
||||
return this.eventEmitter.on('undoRedoStateChange', callback)
|
||||
}
|
||||
|
||||
@ -688,22 +704,26 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
canUndo(): boolean {
|
||||
if (!this.undoManager) return false
|
||||
if (!this.undoManager)
|
||||
return false
|
||||
return this.undoManager.canUndo()
|
||||
}
|
||||
|
||||
canRedo(): boolean {
|
||||
if (!this.undoManager) return false
|
||||
if (!this.undoManager)
|
||||
return false
|
||||
return this.undoManager.canRedo()
|
||||
}
|
||||
|
||||
clearUndoStack(): void {
|
||||
if (!this.undoManager) return
|
||||
if (!this.undoManager)
|
||||
return
|
||||
this.undoManager.clear()
|
||||
}
|
||||
|
||||
private syncNodes(oldNodes: Node[], newNodes: Node[]): void {
|
||||
if (!this.nodesMap || !this.doc) return
|
||||
if (!this.nodesMap || !this.doc)
|
||||
return
|
||||
|
||||
const newIdSet = new Set(newNodes.map(node => node.id))
|
||||
|
||||
@ -719,7 +739,8 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
private syncEdges(oldEdges: Edge[], newEdges: Edge[]): void {
|
||||
if (!this.edgesMap) return
|
||||
if (!this.edgesMap)
|
||||
return
|
||||
|
||||
const oldEdgesMap = new Map(oldEdges.map(edge => [edge.id, edge]))
|
||||
const newEdgesMap = new Map(newEdges.map(edge => [edge.id, edge]))
|
||||
@ -816,7 +837,7 @@ export class CollaborationManager {
|
||||
socket.on('collaboration_update', (update: CollaborationUpdate) => {
|
||||
if (update.type === 'mouse_move') {
|
||||
// Update cursor state for this user
|
||||
const data = update.data as { x: number; y: number }
|
||||
const data = update.data as { x: number, y: number }
|
||||
this.cursors[update.userId] = {
|
||||
x: data.x,
|
||||
y: data.y,
|
||||
@ -861,7 +882,7 @@ export class CollaborationManager {
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('online_users', (data: { users: OnlineUser[]; leader?: string }) => {
|
||||
socket.on('online_users', (data: { users: OnlineUser[], leader?: string }) => {
|
||||
try {
|
||||
if (!data || !Array.isArray(data.users)) {
|
||||
console.warn('Invalid online_users data structure:', data)
|
||||
@ -951,7 +972,8 @@ export class CollaborationManager {
|
||||
// When a follower joins mid-session, it might miss earlier broadcasts and render stale data.
|
||||
// This lightweight checkpoint asks the leader to rebroadcast the latest graph snapshot once.
|
||||
private requestInitialSyncIfNeeded(): void {
|
||||
if (!this.pendingInitialSync) return
|
||||
if (!this.pendingInitialSync)
|
||||
return
|
||||
if (this.isLeader) {
|
||||
this.pendingInitialSync = false
|
||||
return
|
||||
@ -962,7 +984,8 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
private emitGraphResyncRequest(): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
|
||||
this.sendCollaborationEvent({
|
||||
type: 'graph_resync_request',
|
||||
@ -972,11 +995,14 @@ export class CollaborationManager {
|
||||
}
|
||||
|
||||
private broadcastCurrentGraph(): void {
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId)) return
|
||||
if (!this.doc) return
|
||||
if (!this.currentAppId || !webSocketClient.isConnected(this.currentAppId))
|
||||
return
|
||||
if (!this.doc)
|
||||
return
|
||||
|
||||
const socket = webSocketClient.getSocket(this.currentAppId)
|
||||
if (!socket) return
|
||||
if (!socket)
|
||||
return
|
||||
|
||||
try {
|
||||
const snapshot = this.doc.export({ mode: 'snapshot' })
|
||||
|
||||
@ -3,11 +3,11 @@ import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { MentionInput } from './mention-input'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type CommentInputProps = {
|
||||
position: { x: number; y: number }
|
||||
position: { x: number, y: number }
|
||||
onSubmit: (content: string, mentionedUserIds: string[]) => void
|
||||
onCancel: () => void
|
||||
onPositionChange?: (position: {
|
||||
@ -156,12 +156,12 @@ export const CommentInput: FC<CommentInputProps> = memo(({ position, onSubmit, o
|
||||
'relative z-10 flex-1 rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur pb-[4px] shadow-md',
|
||||
)}
|
||||
>
|
||||
<div className='relative pl-[9px] pt-[4px]'>
|
||||
<div className="relative pl-[9px] pt-[4px]">
|
||||
<MentionInput
|
||||
value={content}
|
||||
onChange={setContent}
|
||||
onSubmit={handleMentionSubmit}
|
||||
placeholder={t('workflow.comments.placeholder.add')}
|
||||
placeholder={t('comments.placeholder.add', { ns: 'workflow' })}
|
||||
autoFocus
|
||||
className="relative"
|
||||
/>
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import type { UserProfile } from '@/service/workflow-comment'
|
||||
import { RiArrowUpLine, RiAtLine, RiLoader2Line } from '@remixicon/react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
@ -13,16 +16,14 @@ import {
|
||||
useState,
|
||||
} from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { RiArrowUpLine, RiAtLine, RiLoader2Line } from '@remixicon/react'
|
||||
import Textarea from 'react-textarea-autosize'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import cn from '@/utils/classnames'
|
||||
import { type UserProfile, fetchMentionableUsers } from '@/service/workflow-comment'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { EnterKey } from '@/app/components/base/icons/src/public/common'
|
||||
import { fetchMentionableUsers } from '@/service/workflow-comment'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useStore, useWorkflowStore } from '../store'
|
||||
|
||||
type MentionInputProps = {
|
||||
value: string
|
||||
@ -72,7 +73,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
const [mentionPosition, setMentionPosition] = useState(0)
|
||||
const [selectedMentionIndex, setSelectedMentionIndex] = useState(0)
|
||||
const [mentionedUserIds, setMentionedUserIds] = useState<string[]>([])
|
||||
const resolvedPlaceholder = placeholder ?? t('workflow.comments.placeholder.add')
|
||||
const resolvedPlaceholder = placeholder ?? t('comments.placeholder.add', { ns: 'workflow' })
|
||||
const BASE_PADDING = 4
|
||||
const [shouldReserveButtonGap, setShouldReserveButtonGap] = useState(isEditing)
|
||||
const [shouldReserveHorizontalSpace, setShouldReserveHorizontalSpace] = useState(() => !isEditing)
|
||||
@ -131,7 +132,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
|
||||
const mentionEnd = nextMatchStart + matchedName.length + 1
|
||||
segments.push(
|
||||
<span key={`mention-${nextMatchStart}`} className='text-primary-600'>
|
||||
<span key={`mention-${nextMatchStart}`} className="text-primary-600">
|
||||
{value.slice(nextMatchStart, mentionEnd)}
|
||||
</span>,
|
||||
)
|
||||
@ -276,7 +277,8 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
}, [isEditing, evaluateContentLayout])
|
||||
|
||||
const filteredMentionUsers = useMemo(() => {
|
||||
if (!mentionQuery) return mentionUsers
|
||||
if (!mentionQuery)
|
||||
return mentionUsers
|
||||
return mentionUsers.filter(user =>
|
||||
user.name.toLowerCase().includes(mentionQuery.toLowerCase())
|
||||
|| user.email.toLowerCase().includes(mentionQuery.toLowerCase()),
|
||||
@ -384,7 +386,8 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
|
||||
const insertMention = useCallback((user: UserProfile) => {
|
||||
const textarea = textareaRef.current
|
||||
if (!textarea) return
|
||||
if (!textarea)
|
||||
return
|
||||
|
||||
const beforeMention = value.slice(0, mentionPosition)
|
||||
const afterMention = value.slice(textarea.selectionStart || 0)
|
||||
@ -510,7 +513,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
style={{ willChange: 'transform' }}
|
||||
>
|
||||
{highlightedValue}
|
||||
{''}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<Textarea
|
||||
@ -548,14 +551,14 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
<RiAtLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<Button
|
||||
className='z-20 ml-2 w-8 px-0'
|
||||
variant='primary'
|
||||
className="z-20 ml-2 w-8 px-0"
|
||||
variant="primary"
|
||||
disabled={!value.trim() || disabled || loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{loading
|
||||
? <RiLoader2Line className='h-4 w-4 animate-spin text-components-button-primary-text' />
|
||||
: <RiArrowUpLine className='h-4 w-4 text-components-button-primary-text' />}
|
||||
? <RiLoader2Line className="h-4 w-4 animate-spin text-components-button-primary-text" />
|
||||
: <RiArrowUpLine className="h-4 w-4 text-components-button-primary-text" />}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -578,22 +581,22 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
</div>
|
||||
<div
|
||||
ref={setActionRightRef}
|
||||
className='flex items-center gap-2'
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Button variant='secondary' size='small' onClick={onCancel} disabled={loading}>
|
||||
{t('common.operation.cancel')}
|
||||
<Button variant="secondary" size="small" onClick={onCancel} disabled={loading}>
|
||||
{t('operation.cancel', { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant='primary'
|
||||
size='small'
|
||||
variant="primary"
|
||||
size="small"
|
||||
disabled={loading || !value.trim()}
|
||||
onClick={() => handleSubmit()}
|
||||
className='gap-1'
|
||||
className="gap-1"
|
||||
>
|
||||
{loading && <RiLoader2Line className='mr-1 h-3.5 w-3.5 animate-spin' />}
|
||||
<span>{t('common.operation.save')}</span>
|
||||
{loading && <RiLoader2Line className="mr-1 h-3.5 w-3.5 animate-spin" />}
|
||||
<span>{t('operation.save', { ns: 'common' })}</span>
|
||||
{!loading && (
|
||||
<EnterKey className='h-4 w-4' />
|
||||
<EnterKey className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
'use client'
|
||||
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { useReactFlow, useViewport } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment'
|
||||
import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircleLine, RiCloseLine, RiDeleteBinLine, RiMoreFill } from '@remixicon/react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useReactFlow, useViewport } from 'reactflow'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import type { WorkflowCommentDetail, WorkflowCommentDetailReply } from '@/service/workflow-comment'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { MentionInput } from './mention-input'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useStore } from '../store'
|
||||
import { MentionInput } from './mention-input'
|
||||
|
||||
type CommentThreadProps = {
|
||||
comment: WorkflowCommentDetail
|
||||
@ -100,7 +100,7 @@ const ThreadMessage: FC<{
|
||||
|
||||
const mentionEnd = nextMatchStart + matchedName.length + 1
|
||||
segments.push(
|
||||
<span key={`mention-${nextMatchStart}`} className='text-primary-600'>
|
||||
<span key={`mention-${nextMatchStart}`} className="text-primary-600">
|
||||
{content.slice(nextMatchStart, mentionEnd)}
|
||||
</span>,
|
||||
)
|
||||
@ -119,7 +119,7 @@ const ThreadMessage: FC<{
|
||||
|
||||
return (
|
||||
<div className={cn('flex gap-3 pt-1', className)}>
|
||||
<div className='shrink-0'>
|
||||
<div className="shrink-0">
|
||||
<Avatar
|
||||
name={authorName}
|
||||
avatar={avatarUrl || null}
|
||||
@ -128,12 +128,12 @@ const ThreadMessage: FC<{
|
||||
backgroundColor={userColor}
|
||||
/>
|
||||
</div>
|
||||
<div className='min-w-0 flex-1 pb-4 text-text-primary last:pb-0'>
|
||||
<div className='flex flex-wrap items-center gap-x-2 gap-y-1'>
|
||||
<span className='system-sm-medium text-text-primary'>{authorName}</span>
|
||||
<span className='system-2xs-regular text-text-tertiary'>{formatTimeFromNow(createdAt * 1000)}</span>
|
||||
<div className="min-w-0 flex-1 pb-4 text-text-primary last:pb-0">
|
||||
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span className="system-sm-medium text-text-primary">{authorName}</span>
|
||||
<span className="system-2xs-regular text-text-tertiary">{formatTimeFromNow(createdAt * 1000)}</span>
|
||||
</div>
|
||||
<div className='system-sm-regular mt-1 whitespace-pre-wrap break-words text-text-secondary'>
|
||||
<div className="system-sm-regular mt-1 whitespace-pre-wrap break-words text-text-secondary">
|
||||
{highlightedContent}
|
||||
</div>
|
||||
</div>
|
||||
@ -166,7 +166,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
const { t } = useTranslation()
|
||||
const [replyContent, setReplyContent] = useState('')
|
||||
const [activeReplyMenuId, setActiveReplyMenuId] = useState<string | null>(null)
|
||||
const [editingReply, setEditingReply] = useState<{ id: string; content: string }>({ id: '', content: '' })
|
||||
const [editingReply, setEditingReply] = useState<{ id: string, content: string }>({ id: '', content: '' })
|
||||
const [deletingReplyId, setDeletingReplyId] = useState<string | null>(null)
|
||||
const [isSubmittingEdit, setIsSubmittingEdit] = useState(false)
|
||||
|
||||
@ -211,10 +211,12 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Don't intercept if actively editing a reply
|
||||
if (editingReply.id) return
|
||||
if (editingReply.id)
|
||||
return
|
||||
|
||||
// Don't intercept if mention dropdown is open (let MentionInput handle it)
|
||||
if (document.querySelector('[data-mention-dropdown]')) return
|
||||
if (document.querySelector('[data-mention-dropdown]'))
|
||||
return
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
@ -228,7 +230,8 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
}, [onClose, editingReply.id])
|
||||
|
||||
const handleReplySubmit = useCallback(async (content: string, mentionedUserIds: string[]) => {
|
||||
if (!onReply || replySubmitting) return
|
||||
if (!onReply || replySubmitting)
|
||||
return
|
||||
|
||||
setReplyContent('')
|
||||
|
||||
@ -277,9 +280,11 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
}, [])
|
||||
|
||||
const handleEditSubmit = useCallback(async (content: string, mentionedUserIds: string[]) => {
|
||||
if (!onReplyEdit || !editingReply) return
|
||||
if (!onReplyEdit || !editingReply)
|
||||
return
|
||||
const trimmed = content.trim()
|
||||
if (!trimmed) return
|
||||
if (!trimmed)
|
||||
return
|
||||
|
||||
setIsSubmittingEdit(true)
|
||||
try {
|
||||
@ -343,7 +348,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-50 w-[360px] max-w-[360px]'
|
||||
className="absolute z-50 w-[360px] max-w-[360px]"
|
||||
style={{
|
||||
left: canvasPosition.x + 40,
|
||||
top: canvasPosition.y,
|
||||
@ -354,98 +359,98 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
>
|
||||
<div
|
||||
ref={threadRef}
|
||||
className='relative flex h-[360px] flex-col overflow-hidden rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl'
|
||||
role='dialog'
|
||||
aria-modal='true'
|
||||
aria-labelledby='comment-thread-title'
|
||||
className="relative flex h-[360px] flex-col overflow-hidden rounded-2xl border border-components-panel-border bg-components-panel-bg shadow-xl"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="comment-thread-title"
|
||||
>
|
||||
<div className='flex items-center justify-between rounded-t-2xl border-b border-components-panel-border bg-components-panel-bg-blur px-4 py-3'>
|
||||
<div className="flex items-center justify-between rounded-t-2xl border-b border-components-panel-border bg-components-panel-bg-blur px-4 py-3">
|
||||
<div
|
||||
id='comment-thread-title'
|
||||
className='font-semibold uppercase text-text-primary'
|
||||
id="comment-thread-title"
|
||||
className="font-semibold uppercase text-text-primary"
|
||||
>
|
||||
{t('workflow.comments.panelTitle')}
|
||||
{t('comments.panelTitle', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className='flex items-center gap-1'>
|
||||
<div className="flex items-center gap-1">
|
||||
<Tooltip
|
||||
popupContent={t('workflow.comments.aria.deleteComment')}
|
||||
position='top'
|
||||
popupClassName='!px-2 !py-1.5'
|
||||
popupContent={t('comments.aria.deleteComment', { ns: 'workflow' })}
|
||||
position="top"
|
||||
popupClassName="!px-2 !py-1.5"
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
type="button"
|
||||
disabled={loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onDelete}
|
||||
aria-label={t('workflow.comments.aria.deleteComment')}
|
||||
aria-label={t('comments.aria.deleteComment', { ns: 'workflow' })}
|
||||
>
|
||||
<RiDeleteBinLine className='h-4 w-4' />
|
||||
<RiDeleteBinLine className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={t('workflow.comments.aria.resolveComment')}
|
||||
position='top'
|
||||
popupClassName='!px-2 !py-1.5'
|
||||
popupContent={t('comments.aria.resolveComment', { ns: 'workflow' })}
|
||||
position="top"
|
||||
popupClassName="!px-2 !py-1.5"
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
type="button"
|
||||
disabled={comment.resolved || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onResolve}
|
||||
aria-label={t('workflow.comments.aria.resolveComment')}
|
||||
aria-label={t('comments.aria.resolveComment', { ns: 'workflow' })}
|
||||
>
|
||||
{comment.resolved ? <RiCheckboxCircleFill className='h-4 w-4' /> : <RiCheckboxCircleLine className='h-4 w-4' />}
|
||||
{comment.resolved ? <RiCheckboxCircleFill className="h-4 w-4" /> : <RiCheckboxCircleLine className="h-4 w-4" />}
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Divider type='vertical' className='h-3.5' />
|
||||
<Divider type="vertical" className="h-3.5" />
|
||||
<Tooltip
|
||||
popupContent={t('workflow.comments.aria.previousComment')}
|
||||
position='top'
|
||||
popupClassName='!px-2 !py-1.5'
|
||||
popupContent={t('comments.aria.previousComment', { ns: 'workflow' })}
|
||||
position="top"
|
||||
popupClassName="!px-2 !py-1.5"
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
type="button"
|
||||
disabled={!canGoPrev || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onPrev}
|
||||
aria-label={t('workflow.comments.aria.previousComment')}
|
||||
aria-label={t('comments.aria.previousComment', { ns: 'workflow' })}
|
||||
>
|
||||
<RiArrowUpSLine className='h-4 w-4' />
|
||||
<RiArrowUpSLine className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip
|
||||
popupContent={t('workflow.comments.aria.nextComment')}
|
||||
position='top'
|
||||
popupClassName='!px-2 !py-1.5'
|
||||
popupContent={t('comments.aria.nextComment', { ns: 'workflow' })}
|
||||
position="top"
|
||||
popupClassName="!px-2 !py-1.5"
|
||||
>
|
||||
<button
|
||||
type='button'
|
||||
type="button"
|
||||
disabled={!canGoNext || loading}
|
||||
className={cn('flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary disabled:cursor-not-allowed disabled:text-text-disabled disabled:hover:bg-transparent disabled:hover:text-text-disabled')}
|
||||
onClick={onNext}
|
||||
aria-label={t('workflow.comments.aria.nextComment')}
|
||||
aria-label={t('comments.aria.nextComment', { ns: 'workflow' })}
|
||||
>
|
||||
<RiArrowDownSLine className='h-4 w-4' />
|
||||
<RiArrowDownSLine className="h-4 w-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
type="button"
|
||||
className="flex h-6 w-6 items-center justify-center rounded-lg text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
|
||||
onClick={onClose}
|
||||
aria-label={t('workflow.comments.aria.closeComment')}
|
||||
aria-label={t('comments.aria.closeComment', { ns: 'workflow' })}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4' />
|
||||
<RiCloseLine className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={messageListRef}
|
||||
className='relative mt-2 flex-1 overflow-y-auto px-4 pb-4'
|
||||
className="relative mt-2 flex-1 overflow-y-auto px-4 pb-4"
|
||||
>
|
||||
<div className='-mx-4 rounded-lg px-4 py-2 transition-colors hover:bg-components-panel-on-panel-item-bg-hover'>
|
||||
<div className="-mx-4 rounded-lg px-4 py-2 transition-colors hover:bg-components-panel-on-panel-item-bg-hover">
|
||||
<ThreadMessage
|
||||
authorId={comment.created_by_account?.id || ''}
|
||||
authorName={comment.created_by_account?.name || t('workflow.comments.fallback.user')}
|
||||
authorName={comment.created_by_account?.name || t('comments.fallback.user', { ns: 'workflow' })}
|
||||
avatarUrl={comment.created_by_account?.avatar_url || null}
|
||||
createdAt={comment.created_at}
|
||||
content={comment.content}
|
||||
@ -453,18 +458,18 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
/>
|
||||
</div>
|
||||
{replies.length > 0 && (
|
||||
<div className='mt-2 space-y-3 pt-3'>
|
||||
<div className="mt-2 space-y-3 pt-3">
|
||||
{replies.map((reply) => {
|
||||
const isReplyEditing = editingReply?.id === reply.id
|
||||
const isOwnReply = reply.created_by_account?.id === userProfile?.id
|
||||
return (
|
||||
<div
|
||||
key={reply.id}
|
||||
className='group relative -mx-4 rounded-lg px-4 py-2 transition-colors hover:bg-components-panel-on-panel-item-bg-hover'
|
||||
className="group relative -mx-4 rounded-lg px-4 py-2 transition-colors hover:bg-components-panel-on-panel-item-bg-hover"
|
||||
>
|
||||
{isOwnReply && !isReplyEditing && (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
placement="bottom-end"
|
||||
open={activeReplyMenuId === reply.id}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
@ -482,36 +487,36 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
className='flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary'
|
||||
type="button"
|
||||
className="flex h-6 w-6 items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setDeletingReplyId(null)
|
||||
setActiveReplyMenuId(prev => prev === reply.id ? null : reply.id)
|
||||
}}
|
||||
aria-label={t('workflow.comments.aria.replyActions')}
|
||||
aria-label={t('comments.aria.replyActions', { ns: 'workflow' })}
|
||||
>
|
||||
<RiMoreFill className='h-4 w-4' />
|
||||
<RiMoreFill className="h-4 w-4" />
|
||||
</button>
|
||||
</PortalToFollowElemTrigger>
|
||||
</div>
|
||||
<PortalToFollowElemContent
|
||||
className='z-[100] w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]'
|
||||
className="z-[100] w-36 rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg backdrop-blur-[10px]"
|
||||
data-reply-menu
|
||||
>
|
||||
{/* Menu buttons - hidden when showing delete confirm */}
|
||||
<div className={cn(deletingReplyId === reply.id ? 'hidden' : 'block')}>
|
||||
<button
|
||||
className='flex w-full items-center justify-start rounded-t-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
className="flex w-full items-center justify-start rounded-t-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleStartEdit(reply)
|
||||
}}
|
||||
>
|
||||
{t('workflow.comments.actions.editReply')}
|
||||
{t('comments.actions.editReply', { ns: 'workflow' })}
|
||||
</button>
|
||||
<button
|
||||
className='text-negative flex w-full items-center justify-start rounded-b-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover'
|
||||
className="text-negative flex w-full items-center justify-start rounded-b-xl px-3 py-2 text-left text-sm text-text-secondary hover:bg-state-base-hover"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
@ -524,14 +529,14 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('workflow.comments.actions.deleteReply')}
|
||||
{t('comments.actions.deleteReply', { ns: 'workflow' })}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Delete confirmation - shown when deletingReplyId matches */}
|
||||
<div className={cn(deletingReplyId === reply.id ? 'block' : 'hidden')}>
|
||||
<InlineDeleteConfirm
|
||||
title={t('workflow.comments.actions.deleteReply')}
|
||||
title={t('comments.actions.deleteReply', { ns: 'workflow' })}
|
||||
onConfirm={() => {
|
||||
setDeletingReplyId(null)
|
||||
setActiveReplyMenuId(null)
|
||||
@ -540,49 +545,51 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
onCancel={() => {
|
||||
setDeletingReplyId(null)
|
||||
}}
|
||||
className='m-0 w-full border-0 shadow-none'
|
||||
className="m-0 w-full border-0 shadow-none"
|
||||
/>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)}
|
||||
{isReplyEditing ? (
|
||||
<div className='flex gap-3 pt-1'>
|
||||
<div className='shrink-0'>
|
||||
<Avatar
|
||||
name={reply.created_by_account?.name || t('workflow.comments.fallback.user')}
|
||||
avatar={reply.created_by_account?.avatar_url || null}
|
||||
size={24}
|
||||
className='h-8 w-8 rounded-full'
|
||||
/>
|
||||
</div>
|
||||
<div className='min-w-0 flex-1'>
|
||||
<div className='rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1 shadow-md backdrop-blur-[10px]'>
|
||||
<MentionInput
|
||||
value={editingReply?.content ?? ''}
|
||||
onChange={newContent => setEditingReply(prev => prev ? { ...prev, content: newContent } : prev)}
|
||||
onSubmit={handleEditSubmit}
|
||||
onCancel={handleCancelEdit}
|
||||
placeholder={t('workflow.comments.placeholder.editReply')}
|
||||
disabled={loading}
|
||||
loading={replyUpdating || isSubmittingEdit}
|
||||
isEditing={true}
|
||||
className="system-sm-regular"
|
||||
autoFocus
|
||||
/>
|
||||
{isReplyEditing
|
||||
? (
|
||||
<div className="flex gap-3 pt-1">
|
||||
<div className="shrink-0">
|
||||
<Avatar
|
||||
name={reply.created_by_account?.name || t('comments.fallback.user', { ns: 'workflow' })}
|
||||
avatar={reply.created_by_account?.avatar_url || null}
|
||||
size={24}
|
||||
className="h-8 w-8 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-1 shadow-md backdrop-blur-[10px]">
|
||||
<MentionInput
|
||||
value={editingReply?.content ?? ''}
|
||||
onChange={newContent => setEditingReply(prev => prev ? { ...prev, content: newContent } : prev)}
|
||||
onSubmit={handleEditSubmit}
|
||||
onCancel={handleCancelEdit}
|
||||
placeholder={t('comments.placeholder.editReply', { ns: 'workflow' })}
|
||||
disabled={loading}
|
||||
loading={replyUpdating || isSubmittingEdit}
|
||||
isEditing={true}
|
||||
className="system-sm-regular"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<ThreadMessage
|
||||
authorId={reply.created_by_account?.id || ''}
|
||||
authorName={reply.created_by_account?.name || t('workflow.comments.fallback.user')}
|
||||
avatarUrl={reply.created_by_account?.avatar_url || null}
|
||||
createdAt={reply.created_at}
|
||||
content={reply.content}
|
||||
mentionableNames={mentionableNames}
|
||||
/>
|
||||
)}
|
||||
)
|
||||
: (
|
||||
<ThreadMessage
|
||||
authorId={reply.created_by_account?.id || ''}
|
||||
authorName={reply.created_by_account?.name || t('comments.fallback.user', { ns: 'workflow' })}
|
||||
avatarUrl={reply.created_by_account?.avatar_url || null}
|
||||
createdAt={reply.created_at}
|
||||
content={reply.content}
|
||||
mentionableNames={mentionableNames}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
@ -590,26 +597,26 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
)}
|
||||
</div>
|
||||
{loading && (
|
||||
<div className='bg-components-panel-bg/70 absolute inset-0 z-30 flex items-center justify-center text-sm text-text-tertiary'>
|
||||
{t('workflow.comments.loading')}
|
||||
<div className="bg-components-panel-bg/70 absolute inset-0 z-30 flex items-center justify-center text-sm text-text-tertiary">
|
||||
{t('comments.loading', { ns: 'workflow' })}
|
||||
</div>
|
||||
)}
|
||||
{onReply && (
|
||||
<div className='border-t border-components-panel-border px-4 py-3'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<div className="border-t border-components-panel-border px-4 py-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Avatar
|
||||
avatar={userProfile?.avatar_url || null}
|
||||
name={userProfile?.name || t('common.you')}
|
||||
name={userProfile?.name || t('you', { ns: 'common' })}
|
||||
size={24}
|
||||
className='h-8 w-8'
|
||||
className="h-8 w-8"
|
||||
/>
|
||||
<div className='flex-1 rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-[2px] shadow-sm'>
|
||||
<div className="flex-1 rounded-xl border border-components-chat-input-border bg-components-panel-bg-blur p-[2px] shadow-sm">
|
||||
<MentionInput
|
||||
ref={replyInputRef}
|
||||
value={replyContent}
|
||||
onChange={setReplyContent}
|
||||
onSubmit={handleReplySubmit}
|
||||
placeholder={t('workflow.comments.placeholder.reply')}
|
||||
placeholder={t('comments.placeholder.reply', { ns: 'workflow' })}
|
||||
disabled={loading}
|
||||
loading={replySubmitting}
|
||||
/>
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { Var } from './types'
|
||||
import { BlockEnum, VarType } from './types'
|
||||
|
||||
export const MAX_ITERATION_PARALLEL_NUM = 10
|
||||
export const MIN_ITERATION_PARALLEL_NUM = 1
|
||||
export const DEFAULT_ITER_TIMES = 1
|
||||
@ -42,16 +43,18 @@ export const isInWorkflowPage = () => {
|
||||
export const getGlobalVars = (isChatMode: boolean): Var[] => {
|
||||
const isInWorkflow = isInWorkflowPage()
|
||||
const vars: Var[] = [
|
||||
...(isChatMode ? [
|
||||
{
|
||||
variable: 'sys.dialogue_count',
|
||||
type: VarType.number,
|
||||
},
|
||||
{
|
||||
variable: 'sys.conversation_id',
|
||||
type: VarType.string,
|
||||
},
|
||||
] : []),
|
||||
...(isChatMode
|
||||
? [
|
||||
{
|
||||
variable: 'sys.dialogue_count',
|
||||
type: VarType.number,
|
||||
},
|
||||
{
|
||||
variable: 'sys.conversation_id',
|
||||
type: VarType.string,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
variable: 'sys.user_id',
|
||||
type: VarType.string,
|
||||
@ -68,12 +71,14 @@ export const getGlobalVars = (isChatMode: boolean): Var[] => {
|
||||
variable: 'sys.workflow_run_id',
|
||||
type: VarType.string,
|
||||
},
|
||||
...((isInWorkflow && !isChatMode) ? [
|
||||
{
|
||||
variable: 'sys.timestamp',
|
||||
type: VarType.number,
|
||||
},
|
||||
] : []),
|
||||
...((isInWorkflow && !isChatMode)
|
||||
? [
|
||||
{
|
||||
variable: 'sys.timestamp',
|
||||
type: VarType.number,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
]
|
||||
return vars
|
||||
}
|
||||
@ -104,11 +109,25 @@ export const RETRIEVAL_OUTPUT_STRUCT = `{
|
||||
}`
|
||||
|
||||
export const SUPPORT_OUTPUT_VARS_NODE = [
|
||||
BlockEnum.Start, BlockEnum.TriggerWebhook, BlockEnum.TriggerPlugin, BlockEnum.LLM, BlockEnum.KnowledgeRetrieval, BlockEnum.Code, BlockEnum.TemplateTransform,
|
||||
BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier,
|
||||
BlockEnum.ParameterExtractor, BlockEnum.Iteration, BlockEnum.Loop,
|
||||
BlockEnum.DocExtractor, BlockEnum.ListFilter,
|
||||
BlockEnum.Agent, BlockEnum.DataSource,
|
||||
BlockEnum.Start,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
BlockEnum.LLM,
|
||||
BlockEnum.KnowledgeRetrieval,
|
||||
BlockEnum.Code,
|
||||
BlockEnum.TemplateTransform,
|
||||
BlockEnum.HttpRequest,
|
||||
BlockEnum.Tool,
|
||||
BlockEnum.VariableAssigner,
|
||||
BlockEnum.VariableAggregator,
|
||||
BlockEnum.QuestionClassifier,
|
||||
BlockEnum.ParameterExtractor,
|
||||
BlockEnum.Iteration,
|
||||
BlockEnum.Loop,
|
||||
BlockEnum.DocExtractor,
|
||||
BlockEnum.ListFilter,
|
||||
BlockEnum.Agent,
|
||||
BlockEnum.DataSource,
|
||||
]
|
||||
|
||||
export const AGENT_OUTPUT_STRUCT: Var[] = [
|
||||
|
||||
@ -1,25 +1,25 @@
|
||||
import llmDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import knowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
|
||||
import agentDefault from '@/app/components/workflow/nodes/agent/default'
|
||||
|
||||
import questionClassifierDefault from '@/app/components/workflow/nodes/question-classifier/default'
|
||||
|
||||
import ifElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
||||
import iterationDefault from '@/app/components/workflow/nodes/iteration/default'
|
||||
import iterationStartDefault from '@/app/components/workflow/nodes/iteration-start/default'
|
||||
import loopDefault from '@/app/components/workflow/nodes/loop/default'
|
||||
import loopStartDefault from '@/app/components/workflow/nodes/loop-start/default'
|
||||
import loopEndDefault from '@/app/components/workflow/nodes/loop-end/default'
|
||||
|
||||
import codeDefault from '@/app/components/workflow/nodes/code/default'
|
||||
import templateTransformDefault from '@/app/components/workflow/nodes/template-transform/default'
|
||||
import variableAggregatorDefault from '@/app/components/workflow/nodes/variable-assigner/default'
|
||||
import documentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
||||
import assignerDefault from '@/app/components/workflow/nodes/assigner/default'
|
||||
import codeDefault from '@/app/components/workflow/nodes/code/default'
|
||||
|
||||
import documentExtractorDefault from '@/app/components/workflow/nodes/document-extractor/default'
|
||||
|
||||
import httpRequestDefault from '@/app/components/workflow/nodes/http/default'
|
||||
import parameterExtractorDefault from '@/app/components/workflow/nodes/parameter-extractor/default'
|
||||
import ifElseDefault from '@/app/components/workflow/nodes/if-else/default'
|
||||
import iterationStartDefault from '@/app/components/workflow/nodes/iteration-start/default'
|
||||
import iterationDefault from '@/app/components/workflow/nodes/iteration/default'
|
||||
import knowledgeRetrievalDefault from '@/app/components/workflow/nodes/knowledge-retrieval/default'
|
||||
import listOperatorDefault from '@/app/components/workflow/nodes/list-operator/default'
|
||||
|
||||
import llmDefault from '@/app/components/workflow/nodes/llm/default'
|
||||
import loopEndDefault from '@/app/components/workflow/nodes/loop-end/default'
|
||||
import loopStartDefault from '@/app/components/workflow/nodes/loop-start/default'
|
||||
import loopDefault from '@/app/components/workflow/nodes/loop/default'
|
||||
import parameterExtractorDefault from '@/app/components/workflow/nodes/parameter-extractor/default'
|
||||
import questionClassifierDefault from '@/app/components/workflow/nodes/question-classifier/default'
|
||||
import templateTransformDefault from '@/app/components/workflow/nodes/template-transform/default'
|
||||
import toolDefault from '@/app/components/workflow/nodes/tool/default'
|
||||
import variableAggregatorDefault from '@/app/components/workflow/nodes/variable-assigner/default'
|
||||
|
||||
export const WORKFLOW_COMMON_NODES = [
|
||||
llmDefault,
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import type { StateCreator } from 'zustand'
|
||||
import type { SliceFromInjection } from './store'
|
||||
import {
|
||||
createContext,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import type { SliceFromInjection } from './store'
|
||||
import {
|
||||
createWorkflowStore,
|
||||
} from './store'
|
||||
import type { StateCreator } from 'zustand'
|
||||
|
||||
type WorkflowStore = ReturnType<typeof createWorkflowStore>
|
||||
export const WorkflowContext = createContext<WorkflowStore | null>(null)
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { memo } from 'react'
|
||||
import type { ConnectionLineComponentProps } from 'reactflow'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
Position,
|
||||
getBezierPath,
|
||||
Position,
|
||||
} from 'reactflow'
|
||||
|
||||
const CustomConnectionLine = ({ fromX, fromY, toX, toY }: ConnectionLineComponentProps) => {
|
||||
@ -22,7 +22,7 @@ const CustomConnectionLine = ({ fromX, fromY, toX, toY }: ConnectionLineComponen
|
||||
<g>
|
||||
<path
|
||||
fill="none"
|
||||
stroke='#D0D5DD'
|
||||
stroke="#D0D5DD"
|
||||
strokeWidth={2}
|
||||
d={edgePath}
|
||||
/>
|
||||
@ -31,7 +31,7 @@ const CustomConnectionLine = ({ fromX, fromY, toX, toY }: ConnectionLineComponen
|
||||
y={toY - 4}
|
||||
width={2}
|
||||
height={8}
|
||||
fill='#2970FF'
|
||||
fill="#2970FF"
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
|
||||
@ -25,21 +25,21 @@ const CustomEdgeLinearGradientRender = ({
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={id}
|
||||
gradientUnits='userSpaceOnUse'
|
||||
gradientUnits="userSpaceOnUse"
|
||||
x1={x1}
|
||||
y1={y1}
|
||||
x2={x2}
|
||||
y2={y2}
|
||||
>
|
||||
<stop
|
||||
offset='0%'
|
||||
offset="0%"
|
||||
style={{
|
||||
stopColor: startColor,
|
||||
stopOpacity: 1,
|
||||
}}
|
||||
/>
|
||||
<stop
|
||||
offset='100%'
|
||||
offset="100%"
|
||||
style={{
|
||||
stopColor,
|
||||
stopOpacity: 1,
|
||||
|
||||
@ -1,32 +1,32 @@
|
||||
import type { EdgeProps } from 'reactflow'
|
||||
import type {
|
||||
Edge,
|
||||
OnSelectBlock,
|
||||
} from './types'
|
||||
import { intersection } from 'es-toolkit/array'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { intersection } from 'lodash-es'
|
||||
import type { EdgeProps } from 'reactflow'
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
Position,
|
||||
getBezierPath,
|
||||
Position,
|
||||
} from 'reactflow'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import BlockSelector from './block-selector'
|
||||
import { ITERATION_CHILDREN_Z_INDEX, LOOP_CHILDREN_Z_INDEX } from './constants'
|
||||
import CustomEdgeLinearGradientRender from './custom-edge-linear-gradient-render'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
useNodesInteractions,
|
||||
} from './hooks'
|
||||
import BlockSelector from './block-selector'
|
||||
import type {
|
||||
Edge,
|
||||
OnSelectBlock,
|
||||
} from './types'
|
||||
import { NodeRunningStatus } from './types'
|
||||
import { getEdgeColor } from './utils'
|
||||
import { ITERATION_CHILDREN_Z_INDEX, LOOP_CHILDREN_Z_INDEX } from './constants'
|
||||
import CustomEdgeLinearGradientRender from './custom-edge-linear-gradient-render'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
|
||||
const CustomEdge = ({
|
||||
id,
|
||||
@ -75,8 +75,9 @@ const CustomEdge = ({
|
||||
|| _targetRunningStatus === NodeRunningStatus.Exception
|
||||
|| _targetRunningStatus === NodeRunningStatus.Running
|
||||
)
|
||||
)
|
||||
) {
|
||||
return id
|
||||
}
|
||||
}, [_sourceRunningStatus, _targetRunningStatus, id])
|
||||
|
||||
const handleOpenChange = useCallback((v: boolean) => {
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { FC } from 'react'
|
||||
import { createContext, useCallback, useEffect, useRef } from 'react'
|
||||
import { createDatasetsDetailStore } from './store'
|
||||
import type { CommonNodeType, Node } from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
|
||||
import type { CommonNodeType, Node } from '../types'
|
||||
import { createContext, useCallback, useEffect, useRef } from 'react'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { BlockEnum } from '../types'
|
||||
import { createDatasetsDetailStore } from './store'
|
||||
|
||||
type DatasetsDetailStoreApi = ReturnType<typeof createDatasetsDetailStore>
|
||||
|
||||
@ -33,12 +33,14 @@ const DatasetsDetailProvider: FC<DatasetsDetailProviderProps> = ({
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!storeRef.current) return
|
||||
if (!storeRef.current)
|
||||
return
|
||||
const knowledgeRetrievalNodes = nodes.filter(node => node.data.type === BlockEnum.KnowledgeRetrieval)
|
||||
const allDatasetIds = knowledgeRetrievalNodes.reduce<string[]>((acc, node) => {
|
||||
return Array.from(new Set([...acc, ...(node.data as CommonNodeType<KnowledgeRetrievalNodeType>).dataset_ids]))
|
||||
}, [])
|
||||
if (allDatasetIds.length === 0) return
|
||||
if (allDatasetIds.length === 0)
|
||||
return
|
||||
updateDatasetsDetail(allDatasetIds)
|
||||
}, [])
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { produce } from 'immer'
|
||||
import { useContext } from 'react'
|
||||
import { createStore, useStore } from 'zustand'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { DatasetsDetailContext } from './provider'
|
||||
import { produce } from 'immer'
|
||||
|
||||
type DatasetsDetailStore = {
|
||||
datasetsDetail: Record<string, DataSet>
|
||||
|
||||
@ -1,14 +1,15 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { RiCloseLine, RiLock2Line } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import Modal from '@/app/components/base/modal'
|
||||
import Checkbox from '@/app/components/base/checkbox'
|
||||
import Button from '@/app/components/base/button'
|
||||
import type { EnvironmentVariable } from '@/app/components/workflow/types'
|
||||
import { noop } from 'lodash-es'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
export type DSLExportConfirmModalProps = {
|
||||
envList: EnvironmentVariable[]
|
||||
@ -36,48 +37,48 @@ const DSLExportConfirmModal = ({
|
||||
onClose={noop}
|
||||
className={cn('w-[480px] max-w-[480px]')}
|
||||
>
|
||||
<div className='title-2xl-semi-bold relative pb-6 text-text-primary'>{t('workflow.env.export.title')}</div>
|
||||
<div className='absolute right-4 top-4 cursor-pointer p-2' onClick={onClose}>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
<div className="title-2xl-semi-bold relative pb-6 text-text-primary">{t('env.export.title', { ns: 'workflow' })}</div>
|
||||
<div className="absolute right-4 top-4 cursor-pointer p-2" onClick={onClose}>
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
<div className='relative'>
|
||||
<table className='radius-md w-full border-separate border-spacing-0 border border-divider-regular shadow-xs'>
|
||||
<thead className='system-xs-medium-uppercase text-text-tertiary'>
|
||||
<div className="relative">
|
||||
<table className="radius-md w-full border-separate border-spacing-0 border border-divider-regular shadow-xs">
|
||||
<thead className="system-xs-medium-uppercase text-text-tertiary">
|
||||
<tr>
|
||||
<td width={220} className='h-7 border-b border-r border-divider-regular pl-3'>NAME</td>
|
||||
<td className='h-7 border-b border-divider-regular pl-3'>VALUE</td>
|
||||
<td width={220} className="h-7 border-b border-r border-divider-regular pl-3">NAME</td>
|
||||
<td className="h-7 border-b border-divider-regular pl-3">VALUE</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{envList.map((env, index) => (
|
||||
<tr key={env.name}>
|
||||
<td className={cn('system-xs-medium h-7 border-r pl-3', index + 1 !== envList.length && 'border-b')}>
|
||||
<div className='flex w-[200px] items-center gap-1'>
|
||||
<Env className='h-4 w-4 shrink-0 text-util-colors-violet-violet-600' />
|
||||
<div className='truncate text-text-primary'>{env.name}</div>
|
||||
<div className='shrink-0 text-text-tertiary'>Secret</div>
|
||||
<RiLock2Line className='h-3 w-3 shrink-0 text-text-tertiary' />
|
||||
<div className="flex w-[200px] items-center gap-1">
|
||||
<Env className="h-4 w-4 shrink-0 text-util-colors-violet-violet-600" />
|
||||
<div className="truncate text-text-primary">{env.name}</div>
|
||||
<div className="shrink-0 text-text-tertiary">Secret</div>
|
||||
<RiLock2Line className="h-3 w-3 shrink-0 text-text-tertiary" />
|
||||
</div>
|
||||
</td>
|
||||
<td className={cn('h-7 pl-3', index + 1 !== envList.length && 'border-b')}>
|
||||
<div className='system-xs-regular truncate text-text-secondary'>{env.value}</div>
|
||||
<div className="system-xs-regular truncate text-text-secondary">{env.value}</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className='mt-4 flex gap-2'>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Checkbox
|
||||
className='shrink-0'
|
||||
className="shrink-0"
|
||||
checked={exportSecrets}
|
||||
onCheck={() => setExportSecrets(!exportSecrets)}
|
||||
/>
|
||||
<div className='system-sm-medium cursor-pointer text-text-primary' onClick={() => setExportSecrets(!exportSecrets)}>{t('workflow.env.export.checkbox')}</div>
|
||||
<div className="system-sm-medium cursor-pointer text-text-primary" onClick={() => setExportSecrets(!exportSecrets)}>{t('env.export.checkbox', { ns: 'workflow' })}</div>
|
||||
</div>
|
||||
<div className='flex flex-row-reverse pt-6'>
|
||||
<Button className='ml-2' variant='primary' onClick={submit}>{exportSecrets ? t('workflow.env.export.export') : t('workflow.env.export.ignore')}</Button>
|
||||
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
|
||||
<div className="flex flex-row-reverse pt-6">
|
||||
<Button className="ml-2" variant="primary" onClick={submit}>{exportSecrets ? t('env.export.export', { ns: 'workflow' }) : t('env.export.ignore', { ns: 'workflow' })}</Button>
|
||||
<Button onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
import type { StartNodeType } from './nodes/start/types'
|
||||
import type { CommonNodeType, InputVar, Node } from './types'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import type { WorkflowDraftFeaturesPayload } from '@/service/workflow'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useStore } from './store'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
|
||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
import { updateFeatures } from '@/service/workflow'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesReadOnly,
|
||||
} from './hooks'
|
||||
import { type CommonNodeType, type InputVar, InputVarType, type Node } from './types'
|
||||
import useConfig from './nodes/start/use-config'
|
||||
import type { StartNodeType } from './nodes/start/types'
|
||||
import type { PromptVariable } from '@/models/debug'
|
||||
import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
|
||||
import { webSocketClient } from '@/app/components/workflow/collaboration/core/websocket-manager'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { type WorkflowDraftFeaturesPayload, updateFeatures } from '@/service/workflow'
|
||||
import { useStore } from './store'
|
||||
import { InputVarType } from './types'
|
||||
|
||||
const Features = () => {
|
||||
const setShowFeaturesPanel = useStore(s => s.setShowFeaturesPanel)
|
||||
@ -42,7 +44,8 @@ const Features = () => {
|
||||
}
|
||||
|
||||
const handleFeaturesChange = useCallback(async () => {
|
||||
if (!appId || !featuresStore) return
|
||||
if (!appId || !featuresStore)
|
||||
return
|
||||
|
||||
try {
|
||||
const currentFeatures = featuresStore.getState().features
|
||||
|
||||
@ -3,7 +3,7 @@ import Button from '@/app/components/base/button'
|
||||
import { BubbleX } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const ChatVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
@ -23,14 +23,14 @@ const ChatVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2',
|
||||
theme === 'dark' && showChatVariablePanel && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
'rounded-lg border border-transparent p-2',
|
||||
theme === 'dark' && showChatVariablePanel && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
variant='ghost'
|
||||
variant="ghost"
|
||||
>
|
||||
<BubbleX className='h-4 w-4 text-components-button-secondary-text' />
|
||||
<BubbleX className="h-4 w-4 text-components-button-secondary-text" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
import type { ChecklistItem } from '../hooks/use-checklist'
|
||||
import type {
|
||||
BlockEnum,
|
||||
CommonEdgeType,
|
||||
} from '../types'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiListCheck3,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
@ -5,58 +14,56 @@ import {
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useEdges,
|
||||
useNodes,
|
||||
} from 'reactflow'
|
||||
import { Warning } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import { IconR } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiListCheck3,
|
||||
} from '@remixicon/react'
|
||||
import BlockIcon from '../block-icon'
|
||||
import {
|
||||
useChecklist,
|
||||
useNodesInteractions,
|
||||
} from '../hooks'
|
||||
import type { ChecklistItem } from '../hooks/use-checklist'
|
||||
import type {
|
||||
CommonEdgeType,
|
||||
CommonNodeType,
|
||||
} from '../types'
|
||||
import cn from '@/utils/classnames'
|
||||
ChecklistSquare,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import BlockIcon from '../block-icon'
|
||||
import {
|
||||
ChecklistSquare,
|
||||
} from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { Warning } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import { IconR } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import type { BlockEnum } from '../types'
|
||||
useChecklist,
|
||||
useNodesInteractions,
|
||||
} from '../hooks'
|
||||
|
||||
type WorkflowChecklistProps = {
|
||||
disabled: boolean
|
||||
showGoTo?: boolean
|
||||
onItemClick?: (item: ChecklistItem) => void
|
||||
}
|
||||
const WorkflowChecklist = ({
|
||||
disabled,
|
||||
showGoTo = true,
|
||||
onItemClick,
|
||||
}: WorkflowChecklistProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const edges = useEdges<CommonEdgeType>()
|
||||
const nodes = useNodes()
|
||||
const needWarningNodes = useChecklist(nodes, edges)
|
||||
const { handleNodeSelect } = useNodesInteractions()
|
||||
|
||||
const handleChecklistItemClick = (item: ChecklistItem) => {
|
||||
if (!item.canNavigate)
|
||||
const goToEnabled = showGoTo && item.canNavigate && !item.disableGoTo
|
||||
if (!goToEnabled)
|
||||
return
|
||||
handleNodeSelect(item.id)
|
||||
if (onItemClick)
|
||||
onItemClick(item)
|
||||
else
|
||||
handleNodeSelect(item.id)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
placement="bottom-end"
|
||||
offset={{
|
||||
mainAxis: 12,
|
||||
crossAxis: 4,
|
||||
@ -80,61 +87,64 @@ const WorkflowChecklist = ({
|
||||
</div>
|
||||
{
|
||||
!!needWarningNodes.length && (
|
||||
<div className='absolute -right-1.5 -top-1.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full border border-gray-100 bg-[#F79009] text-[11px] font-semibold text-white'>
|
||||
<div className="absolute -right-1.5 -top-1.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full border border-gray-100 bg-[#F79009] text-[11px] font-semibold text-white">
|
||||
{needWarningNodes.length}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[12]'>
|
||||
<PortalToFollowElemContent className="z-[12]">
|
||||
<div
|
||||
className='w-[420px] overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg'
|
||||
className="w-[420px] overflow-y-auto rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-lg"
|
||||
style={{
|
||||
maxHeight: 'calc(2 / 3 * 100vh)',
|
||||
}}
|
||||
>
|
||||
<div className='text-md sticky top-0 z-[1] flex h-[44px] items-center bg-components-panel-bg pl-4 pr-3 pt-3 font-semibold text-text-primary'>
|
||||
<div className='grow'>{t('workflow.panel.checklist')}{needWarningNodes.length ? `(${needWarningNodes.length})` : ''}</div>
|
||||
<div className="text-md sticky top-0 z-[1] flex h-[44px] items-center bg-components-panel-bg pl-4 pr-3 pt-3 font-semibold text-text-primary">
|
||||
<div className="grow">
|
||||
{t('panel.checklist', { ns: 'workflow' })}
|
||||
{needWarningNodes.length ? `(${needWarningNodes.length})` : ''}
|
||||
</div>
|
||||
<div
|
||||
className='flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center'
|
||||
className="flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
<div className='pb-2'>
|
||||
<div className="pb-2">
|
||||
{
|
||||
!!needWarningNodes.length && (
|
||||
<>
|
||||
<div className='px-4 pt-1 text-xs text-text-tertiary'>{t('workflow.panel.checklistTip')}</div>
|
||||
<div className='px-4 py-2'>
|
||||
<div className="px-4 pt-1 text-xs text-text-tertiary">{t('panel.checklistTip', { ns: 'workflow' })}</div>
|
||||
<div className="px-4 py-2">
|
||||
{
|
||||
needWarningNodes.map(node => (
|
||||
<div
|
||||
key={node.id}
|
||||
className={cn(
|
||||
'group mb-2 rounded-lg border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xs last-of-type:mb-0',
|
||||
node.canNavigate ? 'cursor-pointer' : 'cursor-default opacity-80',
|
||||
showGoTo && node.canNavigate && !node.disableGoTo ? 'cursor-pointer' : 'cursor-default opacity-80',
|
||||
)}
|
||||
onClick={() => handleChecklistItemClick(node)}
|
||||
>
|
||||
<div className='flex h-9 items-center p-2 text-xs font-medium text-text-secondary'>
|
||||
<div className="flex h-9 items-center p-2 text-xs font-medium text-text-secondary">
|
||||
<BlockIcon
|
||||
type={node.type as BlockEnum}
|
||||
className='mr-1.5'
|
||||
className="mr-1.5"
|
||||
toolIcon={node.toolIcon}
|
||||
/>
|
||||
<span className='grow truncate'>
|
||||
<span className="grow truncate">
|
||||
{node.title}
|
||||
</span>
|
||||
{
|
||||
node.canNavigate && (
|
||||
<div className='flex h-4 w-[60px] shrink-0 items-center justify-center gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100'>
|
||||
<span className='whitespace-nowrap text-xs font-medium leading-4 text-primary-600'>
|
||||
{t('workflow.panel.goTo')}
|
||||
(showGoTo && node.canNavigate && !node.disableGoTo) && (
|
||||
<div className="flex h-4 w-[60px] shrink-0 items-center justify-center gap-1 opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||
<span className="whitespace-nowrap text-xs font-medium leading-4 text-primary-600">
|
||||
{t('panel.goTo', { ns: 'workflow' })}
|
||||
</span>
|
||||
<IconR className='h-3.5 w-3.5 text-primary-600' />
|
||||
<IconR className="h-3.5 w-3.5 text-primary-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -147,19 +157,19 @@ const WorkflowChecklist = ({
|
||||
>
|
||||
{
|
||||
node.unConnected && (
|
||||
<div className='px-3 py-1 first:pt-1.5 last:pb-1.5'>
|
||||
<div className='flex text-xs leading-4 text-text-tertiary'>
|
||||
<Warning className='mr-2 mt-[2px] h-3 w-3 text-[#F79009]' />
|
||||
{t('workflow.common.needConnectTip')}
|
||||
<div className="px-3 py-1 first:pt-1.5 last:pb-1.5">
|
||||
<div className="flex text-xs leading-4 text-text-tertiary">
|
||||
<Warning className="mr-2 mt-[2px] h-3 w-3 text-[#F79009]" />
|
||||
{t('common.needConnectTip', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
node.errorMessage && (
|
||||
<div className='px-3 py-1 first:pt-1.5 last:pb-1.5'>
|
||||
<div className='flex text-xs leading-4 text-text-tertiary'>
|
||||
<Warning className='mr-2 mt-[2px] h-3 w-3 text-[#F79009]' />
|
||||
<div className="px-3 py-1 first:pt-1.5 last:pb-1.5">
|
||||
<div className="flex text-xs leading-4 text-text-tertiary">
|
||||
<Warning className="mr-2 mt-[2px] h-3 w-3 text-[#F79009]" />
|
||||
{node.errorMessage}
|
||||
</div>
|
||||
</div>
|
||||
@ -175,9 +185,9 @@ const WorkflowChecklist = ({
|
||||
}
|
||||
{
|
||||
!needWarningNodes.length && (
|
||||
<div className='mx-4 mb-3 rounded-lg bg-components-panel-bg py-4 text-center text-xs text-text-tertiary'>
|
||||
<ChecklistSquare className='mx-auto mb-[5px] h-8 w-8 text-text-quaternary' />
|
||||
{t('workflow.panel.checklistResolved')}
|
||||
<div className="mx-4 mb-3 rounded-lg bg-components-panel-bg py-4 text-center text-xs text-text-tertiary">
|
||||
<ChecklistSquare className="mx-auto mb-[5px] h-8 w-8 text-text-quaternary" />
|
||||
{t('panel.checklistResolved', { ns: 'workflow' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
|
||||
const EditingTitle = () => {
|
||||
@ -18,21 +18,23 @@ const EditingTitle = () => {
|
||||
{
|
||||
!!draftUpdatedAt && (
|
||||
<>
|
||||
{t('workflow.common.autoSaved')} {formatTime(draftUpdatedAt / 1000, 'HH:mm:ss')}
|
||||
{t('common.autoSaved', { ns: 'workflow' })}
|
||||
{' '}
|
||||
{formatTime(draftUpdatedAt / 1000, 'HH:mm:ss')}
|
||||
</>
|
||||
)
|
||||
}
|
||||
<span className='mx-1 flex items-center'>·</span>
|
||||
<span className="mx-1 flex items-center">·</span>
|
||||
{
|
||||
publishedAt
|
||||
? `${t('workflow.common.published')} ${formatTimeFromNow(publishedAt)}`
|
||||
: t('workflow.common.unpublished')
|
||||
? `${t('common.published', { ns: 'workflow' })} ${formatTimeFromNow(publishedAt)}`
|
||||
: t('common.unpublished', { ns: 'workflow' })
|
||||
}
|
||||
{
|
||||
isSyncingWorkflowDraft && (
|
||||
<>
|
||||
<span className='mx-1 flex items-center'>·</span>
|
||||
{t('workflow.common.syncingData')}
|
||||
<span className="mx-1 flex items-center">·</span>
|
||||
{t('common.syncingData', { ns: 'workflow' })}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { memo } from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { Env } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const EnvButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
@ -26,14 +26,14 @@ const EnvButton = ({ disabled }: { disabled: boolean }) => {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2',
|
||||
theme === 'dark' && showEnvPanel && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
'rounded-lg border border-transparent p-2',
|
||||
theme === 'dark' && showEnvPanel && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
variant='ghost'
|
||||
variant="ghost"
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Env className='h-4 w-4 text-components-button-secondary-text' />
|
||||
<Env className="h-4 w-4 text-components-button-secondary-text" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { memo } from 'react'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { GlobalVariable } from '@/app/components/base/icons/src/vender/line/others'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
const GlobalVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||
const { theme } = useTheme()
|
||||
@ -26,14 +26,14 @@ const GlobalVariableButton = ({ disabled }: { disabled: boolean }) => {
|
||||
return (
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2',
|
||||
theme === 'dark' && showGlobalVariablePanel && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
'rounded-lg border border-transparent p-2',
|
||||
theme === 'dark' && showGlobalVariablePanel && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
variant='ghost'
|
||||
variant="ghost"
|
||||
>
|
||||
<GlobalVariable className='h-4 w-4 text-components-button-secondary-text' />
|
||||
<GlobalVariable className="h-4 w-4 text-components-button-secondary-text" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
import type { StartNodeType } from '../nodes/start/types'
|
||||
import type { RunAndHistoryProps } from './run-and-history'
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import type { StartNodeType } from '../nodes/start/types'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import Divider from '../../base/divider'
|
||||
import {
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import Divider from '../../base/divider'
|
||||
import type { RunAndHistoryProps } from './run-and-history'
|
||||
import RunAndHistory from './run-and-history'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import EditingTitle from './editing-title'
|
||||
import EnvButton from './env-button'
|
||||
import VersionHistoryButton from './version-history-button'
|
||||
import OnlineUsers from './online-users'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import ScrollToSelectedNodeButton from './scroll-to-selected-node-button'
|
||||
import GlobalVariableButton from './global-variable-button'
|
||||
import OnlineUsers from './online-users'
|
||||
import RunAndHistory from './run-and-history'
|
||||
import ScrollToSelectedNodeButton from './scroll-to-selected-node-button'
|
||||
import VersionHistoryButton from './version-history-button'
|
||||
|
||||
export type HeaderInNormalProps = {
|
||||
components?: {
|
||||
@ -65,19 +65,19 @@ const HeaderInNormal = ({
|
||||
}, [workflowStore, handleBackupDraft, selectedNode, handleNodeSelect, setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel, setShowVariableInspectPanel, setShowChatVariablePanel, setShowGlobalVariablePanel])
|
||||
|
||||
return (
|
||||
<div className='flex w-full items-center justify-between'>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<EditingTitle />
|
||||
</div>
|
||||
<div>
|
||||
<ScrollToSelectedNodeButton />
|
||||
</div>
|
||||
<div className='flex items-center gap-2'>
|
||||
<div className="flex items-center gap-2">
|
||||
<OnlineUsers />
|
||||
{components?.left}
|
||||
<Divider type='vertical' className='mx-auto h-3.5' />
|
||||
<Divider type="vertical" className="mx-auto h-3.5" />
|
||||
<RunAndHistory {...runAndHistoryProps} />
|
||||
<div className='shrink-0 cursor-pointer rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs backdrop-blur-[10px]'>
|
||||
<div className="shrink-0 cursor-pointer rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs backdrop-blur-[10px]">
|
||||
{components?.chatVariableTrigger}
|
||||
<EnvButton disabled={nodesReadOnly} />
|
||||
<GlobalVariableButton disabled={nodesReadOnly} />
|
||||
|
||||
@ -1,8 +1,20 @@
|
||||
import { RiHistoryLine } from '@remixicon/react'
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { RiHistoryLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Toast from '../../base/toast'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
import {
|
||||
useNodesSyncDraft,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import { useHooksStore } from '../hooks-store'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
@ -10,19 +22,7 @@ import {
|
||||
import {
|
||||
WorkflowVersion,
|
||||
} from '../types'
|
||||
import {
|
||||
useNodesSyncDraft,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import Toast from '../../base/toast'
|
||||
import RestoringTitle from './restoring-title'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { useInvalidAllLastRun } from '@/service/use-workflow'
|
||||
import { useHooksStore } from '../hooks-store'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
|
||||
export type HeaderInRestoringProps = {
|
||||
onRestoreSettled?: () => void
|
||||
@ -61,7 +61,7 @@ const HeaderInRestoring = ({
|
||||
onSuccess: () => {
|
||||
Toast.notify({
|
||||
type: 'success',
|
||||
message: t('workflow.versionHistory.action.restoreSuccess'),
|
||||
message: t('versionHistory.action.restoreSuccess', { ns: 'workflow' }),
|
||||
})
|
||||
// Notify other collaboration clients about the workflow restore
|
||||
if (appDetail)
|
||||
@ -70,7 +70,7 @@ const HeaderInRestoring = ({
|
||||
onError: () => {
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: t('workflow.versionHistory.action.restoreFailure'),
|
||||
message: t('versionHistory.action.restoreFailure', { ns: 'workflow' }),
|
||||
})
|
||||
},
|
||||
onSettled: () => {
|
||||
@ -86,27 +86,28 @@ const HeaderInRestoring = ({
|
||||
<div>
|
||||
<RestoringTitle />
|
||||
</div>
|
||||
<div className=' flex items-center justify-end gap-x-2'>
|
||||
<div className=" flex items-center justify-end gap-x-2">
|
||||
<Button
|
||||
onClick={handleRestore}
|
||||
disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft}
|
||||
variant='primary'
|
||||
variant="primary"
|
||||
className={cn(
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
'rounded-lg border border-transparent',
|
||||
theme === 'dark' && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
{t('workflow.common.restore')}
|
||||
{t('common.restore', { ns: 'workflow' })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancelRestore}
|
||||
className={cn(
|
||||
'text-components-button-secondary-accent-text',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
'rounded-lg border border-transparent text-components-button-secondary-accent-text',
|
||||
theme === 'dark' && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<RiHistoryLine className='h-4 w-4' />
|
||||
<span className='px-0.5'>{t('workflow.common.exitVersions')}</span>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
<RiHistoryLine className="h-4 w-4" />
|
||||
<span className="px-0.5">{t('common.exitVersions', { ns: 'workflow' })}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import type { ViewHistoryProps } from './view-history'
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import Divider from '../../base/divider'
|
||||
import {
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import Divider from '../../base/divider'
|
||||
import {
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import RunningTitle from './running-title'
|
||||
import type { ViewHistoryProps } from './view-history'
|
||||
import ViewHistory from './view-history'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
|
||||
export type HeaderInHistoryProps = {
|
||||
viewHistoryProps?: ViewHistoryProps
|
||||
@ -38,15 +38,15 @@ const HeaderInHistory = ({
|
||||
<div>
|
||||
<RunningTitle />
|
||||
</div>
|
||||
<div className='flex items-center space-x-2'>
|
||||
<div className="flex items-center space-x-2">
|
||||
<ViewHistory {...viewHistoryProps} withText />
|
||||
<Divider type='vertical' className='mx-auto h-3.5' />
|
||||
<Divider type="vertical" className="mx-auto h-3.5" />
|
||||
<Button
|
||||
variant='primary'
|
||||
variant="primary"
|
||||
onClick={handleGoBackToEdit}
|
||||
>
|
||||
<ArrowNarrowLeft className='mr-1 h-4 w-4' />
|
||||
{t('workflow.common.goBackToEdit')}
|
||||
<ArrowNarrowLeft className="mr-1 h-4 w-4" />
|
||||
{t('common.goBackToEdit', { ns: 'workflow' })}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import type { HeaderInNormalProps } from './header-in-normal'
|
||||
import type { HeaderInRestoringProps } from './header-in-restoring'
|
||||
import type { HeaderInHistoryProps } from './header-in-view-history'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import {
|
||||
useWorkflowMode,
|
||||
} from '../hooks'
|
||||
import type { HeaderInNormalProps } from './header-in-normal'
|
||||
import HeaderInNormal from './header-in-normal'
|
||||
import type { HeaderInHistoryProps } from './header-in-view-history'
|
||||
import type { HeaderInRestoringProps } from './header-in-restoring'
|
||||
import { useStore } from '../store'
|
||||
import dynamic from 'next/dynamic'
|
||||
import HeaderInNormal from './header-in-normal'
|
||||
|
||||
const HeaderInHistory = dynamic(() => import('./header-in-view-history'), {
|
||||
ssr: false,
|
||||
@ -38,9 +38,9 @@ const Header = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute left-0 top-7 z-10 flex h-0 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
|
||||
className="absolute left-0 top-7 z-10 flex h-0 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3"
|
||||
>
|
||||
{(inWorkflowCanvas || isPipelineCanvas) && maximizeCanvas && <div className='h-14 w-[52px]' />}
|
||||
{(inWorkflowCanvas || isPipelineCanvas) && maximizeCanvas && <div className="h-14 w-[52px]" />}
|
||||
{
|
||||
normal && (
|
||||
<HeaderInNormal
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
'use client'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useReactFlow } from 'reactflow'
|
||||
import Avatar from '@/app/components/base/avatar'
|
||||
import { useCollaboration } from '../collaboration/hooks/use-collaboration'
|
||||
import { useStore } from '../store'
|
||||
import cn from '@/utils/classnames'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import { getUserColor } from '../collaboration/utils/user-color'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useAppContext } from '@/context/app-context'
|
||||
import { getAvatar } from '@/service/common'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useCollaboration } from '../collaboration/hooks/use-collaboration'
|
||||
import { getUserColor } from '../collaboration/utils/user-color'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const useAvatarUrls = (users: any[]) => {
|
||||
const [avatarUrls, setAvatarUrls] = useState<Record<string, string>>({})
|
||||
@ -81,7 +81,8 @@ const OnlineUsers = () => {
|
||||
// Function to jump to user's cursor position
|
||||
const jumpToUserCursor = (userId: string) => {
|
||||
const cursor = cursors[userId]
|
||||
if (!cursor) return
|
||||
if (!cursor)
|
||||
return
|
||||
|
||||
// Convert world coordinates to center the view on the cursor
|
||||
reactFlow.setCenter(cursor.x, cursor.y, { zoom: 1, duration: 800 })
|
||||
@ -173,7 +174,8 @@ const OnlineUsers = () => {
|
||||
visibleUsers.length > 0 && '-ml-1',
|
||||
)}
|
||||
>
|
||||
+{remainingCount}
|
||||
+
|
||||
{remainingCount}
|
||||
</div>
|
||||
<ChevronDownIcon className="h-3 w-3 cursor-pointer text-gray-500" />
|
||||
</div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { memo, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
import { useStore } from '../store'
|
||||
import { WorkflowVersion } from '../types'
|
||||
import useTimestamp from '@/hooks/use-timestamp'
|
||||
|
||||
const RestoringTitle = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -11,25 +11,25 @@ const RestoringTitle = () => {
|
||||
const { formatTime } = useTimestamp()
|
||||
const currentVersion = useStore(state => state.currentVersion)
|
||||
const isDraft = currentVersion?.version === WorkflowVersion.Draft
|
||||
const publishStatus = isDraft ? t('workflow.common.unpublished') : t('workflow.common.published')
|
||||
const publishStatus = isDraft ? t('common.unpublished', { ns: 'workflow' }) : t('common.published', { ns: 'workflow' })
|
||||
|
||||
const versionName = useMemo(() => {
|
||||
if (isDraft)
|
||||
return t('workflow.versionHistory.currentDraft')
|
||||
return currentVersion?.marked_name || t('workflow.versionHistory.defaultName')
|
||||
return t('versionHistory.currentDraft', { ns: 'workflow' })
|
||||
return currentVersion?.marked_name || t('versionHistory.defaultName', { ns: 'workflow' })
|
||||
}, [currentVersion, t, isDraft])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-y-0.5'>
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<span className='system-sm-semibold text-text-primary'>
|
||||
<div className="flex flex-col gap-y-0.5">
|
||||
<div className="flex items-center gap-x-1">
|
||||
<span className="system-sm-semibold text-text-primary">
|
||||
{versionName}
|
||||
</span>
|
||||
<span className='system-2xs-medium-uppercase rounded-[5px] border border-text-accent-secondary bg-components-badge-bg-dimm px-1 py-0.5 text-text-accent-secondary'>
|
||||
{t('workflow.common.viewOnly')}
|
||||
<span className="system-2xs-medium-uppercase rounded-[5px] border border-text-accent-secondary bg-components-badge-bg-dimm px-1 py-0.5 text-text-accent-secondary">
|
||||
{t('common.viewOnly', { ns: 'workflow' })}
|
||||
</span>
|
||||
</div>
|
||||
<div className='system-xs-regular flex h-4 items-center gap-x-1 text-text-tertiary'>
|
||||
<div className="system-xs-regular flex h-4 items-center gap-x-1 text-text-tertiary">
|
||||
{
|
||||
currentVersion && (
|
||||
<>
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { ViewHistoryProps } from './view-history'
|
||||
import {
|
||||
RiPlayLargeLine,
|
||||
} from '@remixicon/react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflowStartRun,
|
||||
} from '../hooks'
|
||||
import type { ViewHistoryProps } from './view-history'
|
||||
import ViewHistory from './view-history'
|
||||
import Checklist from './checklist'
|
||||
import cn from '@/utils/classnames'
|
||||
import RunMode from './run-mode'
|
||||
import ViewHistory from './view-history'
|
||||
|
||||
const PreviewMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
@ -25,8 +25,8 @@ const PreviewMode = memo(() => {
|
||||
)}
|
||||
onClick={() => handleWorkflowStartRunInChatflow()}
|
||||
>
|
||||
<RiPlayLargeLine className='mr-1 h-4 w-4' />
|
||||
{t('workflow.common.debugAndPreview')}
|
||||
<RiPlayLargeLine className="mr-1 h-4 w-4" />
|
||||
{t('common.debugAndPreview', { ns: 'workflow' })}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@ -56,7 +56,7 @@ const RunAndHistory = ({
|
||||
const { RunMode: CustomRunMode } = components || {}
|
||||
|
||||
return (
|
||||
<div className='flex h-8 items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-0.5 shadow-xs'>
|
||||
<div className="flex h-8 items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-0.5 shadow-xs">
|
||||
{
|
||||
showRunButton && (
|
||||
CustomRunMode ? <CustomRunMode text={runButtonText} /> : <RunMode text={runButtonText} />
|
||||
@ -65,7 +65,7 @@ const RunAndHistory = ({
|
||||
{
|
||||
showPreviewButton && <PreviewMode />
|
||||
}
|
||||
<div className='mx-0.5 h-3.5 w-[1px] bg-divider-regular'></div>
|
||||
<div className="mx-0.5 h-3.5 w-[1px] bg-divider-regular"></div>
|
||||
<ViewHistory {...viewHistoryProps} />
|
||||
<Checklist disabled={nodesReadOnly} />
|
||||
</div>
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react'
|
||||
import type { TestRunMenuRef, TriggerOption } from './test-run-menu'
|
||||
import { RiLoader2Line, RiPlayLargeLine } from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { trackEvent } from '@/app/components/base/amplitude'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useWorkflowRun, useWorkflowRunValidation, useWorkflowStartRun } from '@/app/components/workflow/hooks'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { WorkflowRunningStatus } from '@/app/components/workflow/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import { getKeyboardKeyNameBySystem } from '@/app/components/workflow/utils'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiLoader2Line, RiPlayLargeLine } from '@remixicon/react'
|
||||
import { StopCircle } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { EVENT_WORKFLOW_STOP } from '@/app/components/workflow/variable-inspect/types'
|
||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { useDynamicTestRunOptions } from '../hooks/use-dynamic-test-run-options'
|
||||
import TestRunMenu, { type TestRunMenuRef, type TriggerOption, TriggerType } from './test-run-menu'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import TestRunMenu, { TriggerType } from './test-run-menu'
|
||||
|
||||
type RunModeProps = {
|
||||
text?: string
|
||||
@ -63,28 +66,33 @@ const RunMode = ({
|
||||
isValid = false
|
||||
})
|
||||
if (!isValid) {
|
||||
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
|
||||
notify({ type: 'error', message: t('panel.checklistTip', { ns: 'workflow' }) })
|
||||
return
|
||||
}
|
||||
|
||||
if (option.type === TriggerType.UserInput) {
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
trackEvent('app_start_action_time', { action_type: 'user_input' })
|
||||
}
|
||||
else if (option.type === TriggerType.Schedule) {
|
||||
handleWorkflowTriggerScheduleRunInWorkflow(option.nodeId)
|
||||
trackEvent('app_start_action_time', { action_type: 'schedule' })
|
||||
}
|
||||
else if (option.type === TriggerType.Webhook) {
|
||||
if (option.nodeId)
|
||||
handleWorkflowTriggerWebhookRunInWorkflow({ nodeId: option.nodeId })
|
||||
trackEvent('app_start_action_time', { action_type: 'webhook' })
|
||||
}
|
||||
else if (option.type === TriggerType.Plugin) {
|
||||
if (option.nodeId)
|
||||
handleWorkflowTriggerPluginRunInWorkflow(option.nodeId)
|
||||
trackEvent('app_start_action_time', { action_type: 'plugin' })
|
||||
}
|
||||
else if (option.type === TriggerType.All) {
|
||||
const targetNodeIds = option.relatedNodeIds?.filter(Boolean)
|
||||
if (targetNodeIds && targetNodeIds.length > 0)
|
||||
handleWorkflowRunAllTriggersInWorkflow(targetNodeIds)
|
||||
trackEvent('app_start_action_time', { action_type: 'all' })
|
||||
}
|
||||
else {
|
||||
// Placeholder for trigger-specific execution logic for schedule, webhook, plugin types
|
||||
@ -106,57 +114,57 @@ const RunMode = ({
|
||||
})
|
||||
|
||||
return (
|
||||
<div className='flex items-center gap-x-px'>
|
||||
<div className="flex items-center gap-x-px">
|
||||
{
|
||||
isRunning
|
||||
? (
|
||||
<button
|
||||
type='button'
|
||||
className={cn(
|
||||
'system-xs-medium flex h-7 cursor-not-allowed items-center gap-x-1 rounded-l-md bg-state-accent-hover px-1.5 text-text-accent',
|
||||
)}
|
||||
disabled={true}
|
||||
>
|
||||
<RiLoader2Line className='mr-1 size-4 animate-spin' />
|
||||
{isListening ? t('workflow.common.listening') : t('workflow.common.running')}
|
||||
</button>
|
||||
)
|
||||
: (
|
||||
<TestRunMenu
|
||||
ref={testRunMenuRef}
|
||||
options={dynamicOptions}
|
||||
onSelect={handleTriggerSelect}
|
||||
>
|
||||
<div
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'system-xs-medium flex h-7 cursor-pointer items-center gap-x-1 rounded-md px-1.5 text-text-accent hover:bg-state-accent-hover',
|
||||
'system-xs-medium flex h-7 cursor-not-allowed items-center gap-x-1 rounded-l-md bg-state-accent-hover px-1.5 text-text-accent',
|
||||
)}
|
||||
style={{ userSelect: 'none' }}
|
||||
disabled={true}
|
||||
>
|
||||
<RiPlayLargeLine className='mr-1 size-4' />
|
||||
{text ?? t('workflow.common.run')}
|
||||
<div className='system-kbd flex items-center gap-x-0.5 text-text-tertiary'>
|
||||
<div className='flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray'>
|
||||
{getKeyboardKeyNameBySystem('alt')}
|
||||
</div>
|
||||
<div className='flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray'>
|
||||
R
|
||||
<RiLoader2Line className="mr-1 size-4 animate-spin" />
|
||||
{isListening ? t('common.listening', { ns: 'workflow' }) : t('common.running', { ns: 'workflow' })}
|
||||
</button>
|
||||
)
|
||||
: (
|
||||
<TestRunMenu
|
||||
ref={testRunMenuRef}
|
||||
options={dynamicOptions}
|
||||
onSelect={handleTriggerSelect}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'system-xs-medium flex h-7 cursor-pointer items-center gap-x-1 rounded-md px-1.5 text-text-accent hover:bg-state-accent-hover',
|
||||
)}
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<RiPlayLargeLine className="mr-1 size-4" />
|
||||
{text ?? t('common.run', { ns: 'workflow' })}
|
||||
<div className="system-kbd flex items-center gap-x-0.5 text-text-tertiary">
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
{getKeyboardKeyNameBySystem('alt')}
|
||||
</div>
|
||||
<div className="flex size-4 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray">
|
||||
R
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TestRunMenu>
|
||||
)
|
||||
</TestRunMenu>
|
||||
)
|
||||
}
|
||||
{
|
||||
isRunning && (
|
||||
<button
|
||||
type='button'
|
||||
type="button"
|
||||
className={cn(
|
||||
'flex size-7 items-center justify-center rounded-r-md bg-state-accent-active',
|
||||
)}
|
||||
onClick={handleStop}
|
||||
>
|
||||
<StopCircle className='size-4 text-text-accent' />
|
||||
<StopCircle className="size-4 text-text-accent" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time'
|
||||
import { useIsChatMode } from '../hooks'
|
||||
import { useStore } from '../store'
|
||||
import { formatWorkflowRunIdentifier } from '../utils'
|
||||
import { ClockPlay } from '@/app/components/base/icons/src/vender/line/time'
|
||||
|
||||
const RunningTitle = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -11,12 +11,12 @@ const RunningTitle = () => {
|
||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
||||
|
||||
return (
|
||||
<div className='flex h-[18px] items-center text-xs text-gray-500'>
|
||||
<ClockPlay className='mr-1 h-3 w-3 text-gray-500' />
|
||||
<div className="flex h-[18px] items-center text-xs text-gray-500">
|
||||
<ClockPlay className="mr-1 h-3 w-3 text-gray-500" />
|
||||
<span>{isChatMode ? `Test Chat${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}` : `Test Run${formatWorkflowRunIdentifier(historyWorkflowData?.finished_at)}`}</span>
|
||||
<span className='mx-1'>·</span>
|
||||
<span className='ml-1 flex h-[18px] items-center rounded-[5px] border border-indigo-300 bg-white/[0.48] px-1 text-[10px] font-semibold uppercase text-indigo-600'>
|
||||
{t('workflow.common.viewOnly')}
|
||||
<span className="mx-1">·</span>
|
||||
<span className="ml-1 flex h-[18px] items-center rounded-[5px] border border-indigo-300 bg-white/[0.48] px-1 text-[10px] font-semibold uppercase text-indigo-600">
|
||||
{t('common.viewOnly', { ns: 'workflow' })}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import type { FC } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { CommonNodeType } from '../types'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { scrollToWorkflowNode } from '../utils/node-navigation'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
const ScrollToSelectedNodeButton: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -12,7 +12,8 @@ const ScrollToSelectedNodeButton: FC = () => {
|
||||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
|
||||
const handleScrollToSelectedNode = useCallback(() => {
|
||||
if (!selectedNode) return
|
||||
if (!selectedNode)
|
||||
return
|
||||
scrollToWorkflowNode(selectedNode.id)
|
||||
}, [selectedNode])
|
||||
|
||||
@ -26,7 +27,7 @@ const ScrollToSelectedNodeButton: FC = () => {
|
||||
)}
|
||||
onClick={handleScrollToSelectedNode}
|
||||
>
|
||||
{t('workflow.panel.scrollToSelectedNode')}
|
||||
{t('panel.scrollToSelectedNode', { ns: 'workflow' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import type { MouseEvent, MouseEventHandler, ReactElement } from 'react'
|
||||
import {
|
||||
type MouseEvent,
|
||||
type MouseEventHandler,
|
||||
type ReactElement,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
@ -158,14 +157,14 @@ const TestRunMenu = forwardRef<TestRunMenuRef, TestRunMenuProps>(({
|
||||
return (
|
||||
<div
|
||||
key={option.id}
|
||||
className='system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover'
|
||||
className="system-md-regular flex cursor-pointer items-center rounded-lg px-3 py-1.5 text-text-secondary hover:bg-state-base-hover"
|
||||
onClick={() => handleSelect(option)}
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center'>
|
||||
<div className='flex h-6 w-6 shrink-0 items-center justify-center'>
|
||||
<div className="flex min-w-0 flex-1 items-center">
|
||||
<div className="flex h-6 w-6 shrink-0 items-center justify-center">
|
||||
{option.icon}
|
||||
</div>
|
||||
<span className='ml-2 truncate'>{option.name}</span>
|
||||
<span className="ml-2 truncate">{option.name}</span>
|
||||
</div>
|
||||
{shortcutKey && (
|
||||
<ShortcutsName keys={[shortcutKey]} className="ml-2" textColor="secondary" />
|
||||
@ -214,7 +213,7 @@ const TestRunMenu = forwardRef<TestRunMenuRef, TestRunMenuProps>(({
|
||||
<PortalToFollowElem
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement='bottom-start'
|
||||
placement="bottom-start"
|
||||
offset={{ mainAxis: 8, crossAxis: -4 }}
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild onClick={() => setOpen(!open)}>
|
||||
@ -222,16 +221,16 @@ const TestRunMenu = forwardRef<TestRunMenuRef, TestRunMenuProps>(({
|
||||
{children}
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[12]'>
|
||||
<div className='w-[284px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-1 shadow-lg'>
|
||||
<div className='mb-2 px-3 pt-2 text-sm font-medium text-text-primary'>
|
||||
{t('workflow.common.chooseStartNodeToRun')}
|
||||
<PortalToFollowElemContent className="z-[12]">
|
||||
<div className="w-[284px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-1 shadow-lg">
|
||||
<div className="mb-2 px-3 pt-2 text-sm font-medium text-text-primary">
|
||||
{t('common.chooseStartNodeToRun', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div>
|
||||
{hasUserInput && renderOption(options.userInput!)}
|
||||
|
||||
{(hasTriggers || hasRunAll) && hasUserInput && (
|
||||
<div className='mx-3 my-1 h-px bg-divider-subtle' />
|
||||
<div className="mx-3 my-1 h-px bg-divider-subtle" />
|
||||
)}
|
||||
|
||||
{hasRunAll && renderOption(options.runAll!)}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
RiArrowGoBackLine,
|
||||
RiArrowGoForwardFill,
|
||||
} from '@remixicon/react'
|
||||
import TipPopup from '../operator/tip-popup'
|
||||
import Divider from '../../base/divider'
|
||||
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
|
||||
import ViewWorkflowHistory from '@/app/components/workflow/header/view-workflow-history'
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager'
|
||||
import classNames from '@/utils/classnames'
|
||||
import ViewWorkflowHistory from '@/app/components/workflow/header/view-workflow-history'
|
||||
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Divider from '../../base/divider'
|
||||
import TipPopup from '../operator/tip-popup'
|
||||
|
||||
export type UndoRedoProps = { handleUndo: () => void; handleRedo: () => void }
|
||||
export type UndoRedoProps = { handleUndo: () => void, handleRedo: () => void }
|
||||
const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
|
||||
const { t } = useTranslation()
|
||||
const [buttonsDisabled, setButtonsDisabled] = useState({ undo: true, redo: true })
|
||||
@ -43,35 +43,34 @@ const UndoRedo: FC<UndoRedoProps> = ({ handleUndo, handleRedo }) => {
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
|
||||
return (
|
||||
<div className='flex items-center space-x-0.5 rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-lg backdrop-blur-[5px]'>
|
||||
<TipPopup title={t('workflow.common.undo')!} shortcuts={['ctrl', 'z']}>
|
||||
<div className="flex items-center space-x-0.5 rounded-lg border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg p-0.5 shadow-lg backdrop-blur-[5px]">
|
||||
<TipPopup title={t('common.undo', { ns: 'workflow' })!} shortcuts={['ctrl', 'z']}>
|
||||
<div
|
||||
data-tooltip-id='workflow.undo'
|
||||
data-tooltip-id="workflow.undo"
|
||||
className={
|
||||
classNames('system-sm-medium flex h-8 w-8 cursor-pointer select-none items-center rounded-md px-1.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
(nodesReadOnly || buttonsDisabled.undo)
|
||||
&& 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled')}
|
||||
cn('system-sm-medium flex h-8 w-8 cursor-pointer select-none items-center rounded-md px-1.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', (nodesReadOnly || buttonsDisabled.undo)
|
||||
&& 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled')
|
||||
}
|
||||
onClick={() => !nodesReadOnly && !buttonsDisabled.undo && handleUndo()}
|
||||
>
|
||||
<RiArrowGoBackLine className='h-4 w-4' />
|
||||
</div>
|
||||
</TipPopup >
|
||||
<TipPopup title={t('workflow.common.redo')!} shortcuts={['ctrl', 'y']}>
|
||||
<div
|
||||
data-tooltip-id='workflow.redo'
|
||||
className={
|
||||
classNames('system-sm-medium flex h-8 w-8 cursor-pointer select-none items-center rounded-md px-1.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
(nodesReadOnly || buttonsDisabled.redo)
|
||||
&& 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||
)}
|
||||
onClick={() => !nodesReadOnly && !buttonsDisabled.redo && handleRedo()}
|
||||
>
|
||||
<RiArrowGoForwardFill className='h-4 w-4' />
|
||||
<RiArrowGoBackLine className="h-4 w-4" />
|
||||
</div>
|
||||
</TipPopup>
|
||||
<Divider type='vertical' className="mx-0.5 h-3.5" />
|
||||
<TipPopup title={t('common.redo', { ns: 'workflow' })!} shortcuts={['ctrl', 'y']}>
|
||||
<div
|
||||
data-tooltip-id="workflow.redo"
|
||||
className={
|
||||
cn('system-sm-medium flex h-8 w-8 cursor-pointer select-none items-center rounded-md px-1.5 text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', (nodesReadOnly || buttonsDisabled.redo)
|
||||
&& 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled')
|
||||
}
|
||||
onClick={() => !nodesReadOnly && !buttonsDisabled.redo && handleRedo()}
|
||||
>
|
||||
<RiArrowGoForwardFill className="h-4 w-4" />
|
||||
</div>
|
||||
</TipPopup>
|
||||
<Divider type="vertical" className="mx-0.5 h-3.5" />
|
||||
<ViewWorkflowHistory />
|
||||
</div >
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
import React, { type FC, useCallback } from 'react'
|
||||
import type { FC } from 'react'
|
||||
import { RiHistoryLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Button from '../../base/button'
|
||||
import Tooltip from '../../base/tooltip'
|
||||
import { getKeyboardKeyCodeBySystem, getKeyboardKeyNameBySystem } from '../utils'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type VersionHistoryButtonProps = {
|
||||
onClick: () => Promise<unknown> | unknown
|
||||
@ -17,15 +19,15 @@ const VERSION_HISTORY_SHORTCUT = ['ctrl', '⇧', 'H']
|
||||
const PopupContent = React.memo(() => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<div className='flex items-center gap-x-1'>
|
||||
<div className='system-xs-medium px-0.5 text-text-secondary'>
|
||||
{t('workflow.common.versionHistory')}
|
||||
<div className="flex items-center gap-x-1">
|
||||
<div className="system-xs-medium px-0.5 text-text-secondary">
|
||||
{t('common.versionHistory', { ns: 'workflow' })}
|
||||
</div>
|
||||
<div className='flex items-center gap-x-0.5'>
|
||||
<div className="flex items-center gap-x-0.5">
|
||||
{VERSION_HISTORY_SHORTCUT.map(key => (
|
||||
<span
|
||||
key={key}
|
||||
className='system-kbd rounded-[4px] bg-components-kbd-bg-white px-[1px] text-text-tertiary'
|
||||
className="system-kbd rounded-[4px] bg-components-kbd-bg-white px-[1px] text-text-tertiary"
|
||||
>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</span>
|
||||
@ -50,22 +52,24 @@ const VersionHistoryButton: FC<VersionHistoryButtonProps> = ({
|
||||
handleViewVersionHistory()
|
||||
}, { exactMatch: true, useCapture: true })
|
||||
|
||||
return <Tooltip
|
||||
popupContent={<PopupContent />}
|
||||
noDecoration
|
||||
popupClassName='rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg
|
||||
shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px] p-1.5'
|
||||
>
|
||||
<Button
|
||||
className={cn(
|
||||
'p-2',
|
||||
theme === 'dark' && 'rounded-lg border border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
onClick={handleViewVersionHistory}
|
||||
return (
|
||||
<Tooltip
|
||||
popupContent={<PopupContent />}
|
||||
noDecoration
|
||||
popupClassName="rounded-lg border-[0.5px] border-components-panel-border bg-components-tooltip-bg
|
||||
shadow-lg shadow-shadow-shadow-5 backdrop-blur-[5px] p-1.5"
|
||||
>
|
||||
<RiHistoryLine className='h-4 w-4 text-components-button-secondary-text' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
className={cn(
|
||||
'rounded-lg border border-transparent p-2',
|
||||
theme === 'dark' && 'border-black/5 bg-white/10 backdrop-blur-sm',
|
||||
)}
|
||||
onClick={handleViewVersionHistory}
|
||||
>
|
||||
<RiHistoryLine className="h-4 w-4 text-components-button-secondary-text" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default VersionHistoryButton
|
||||
|
||||
@ -1,56 +1,51 @@
|
||||
import {
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { Fetcher } from 'swr'
|
||||
import useSWR from 'swr'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { noop } from 'lodash-es'
|
||||
import {
|
||||
RiCheckboxCircleLine,
|
||||
RiCloseLine,
|
||||
RiErrorWarningLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesInteractions,
|
||||
useWorkflowInteractions,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { ControlMode, WorkflowRunningStatus } from '../types'
|
||||
import { formatWorkflowRunIdentifier } from '../utils'
|
||||
import cn from '@/utils/classnames'
|
||||
memo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import {
|
||||
ClockPlay,
|
||||
ClockPlaySlim,
|
||||
} from '@/app/components/base/icons/src/vender/line/time'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import {
|
||||
ClockPlay,
|
||||
ClockPlaySlim,
|
||||
} from '@/app/components/base/icons/src/vender/line/time'
|
||||
import { AlertTriangle } from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '@/app/components/workflow/store'
|
||||
import type { WorkflowRunHistoryResponse } from '@/types/workflow'
|
||||
import { useInputFieldPanel } from '@/app/components/rag-pipeline/hooks'
|
||||
import { useFormatTimeFromNow } from '@/hooks/use-format-time-from-now'
|
||||
import { useWorkflowRunHistory } from '@/service/use-workflow'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesInteractions,
|
||||
useWorkflowInteractions,
|
||||
useWorkflowRun,
|
||||
} from '../hooks'
|
||||
import { ControlMode, WorkflowRunningStatus } from '../types'
|
||||
import { formatWorkflowRunIdentifier } from '../utils'
|
||||
|
||||
export type ViewHistoryProps = {
|
||||
withText?: boolean
|
||||
onClearLogAndMessageModal?: () => void
|
||||
historyUrl?: string
|
||||
historyFetcher?: Fetcher<WorkflowRunHistoryResponse, string>
|
||||
}
|
||||
const ViewHistory = ({
|
||||
withText,
|
||||
onClearLogAndMessageModal,
|
||||
historyUrl,
|
||||
historyFetcher,
|
||||
}: ViewHistoryProps) => {
|
||||
const { t } = useTranslation()
|
||||
const isChatMode = useIsChatMode()
|
||||
@ -68,11 +63,11 @@ const ViewHistory = ({
|
||||
const { handleBackupDraft } = useWorkflowRun()
|
||||
const { closeAllInputFieldPanels } = useInputFieldPanel()
|
||||
|
||||
const fetcher = historyFetcher ?? (noop as Fetcher<WorkflowRunHistoryResponse, string>)
|
||||
const shouldFetchHistory = open && !!historyUrl
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
} = useSWR((open && historyUrl && historyFetcher) ? historyUrl : null, fetcher)
|
||||
} = useWorkflowRunHistory(historyUrl, shouldFetchHistory)
|
||||
|
||||
return (
|
||||
(
|
||||
@ -92,18 +87,19 @@ const ViewHistory = ({
|
||||
'flex h-8 items-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg px-3 shadow-xs',
|
||||
'cursor-pointer text-[13px] font-medium text-components-button-secondary-text hover:bg-components-button-secondary-bg-hover',
|
||||
open && 'bg-components-button-secondary-bg-hover',
|
||||
)}>
|
||||
)}
|
||||
>
|
||||
<ClockPlay
|
||||
className={'mr-1 h-4 w-4'}
|
||||
className="mr-1 h-4 w-4"
|
||||
/>
|
||||
{t('workflow.common.showRunHistory')}
|
||||
{t('common.showRunHistory', { ns: 'workflow' })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!withText && (
|
||||
<Tooltip
|
||||
popupContent={t('workflow.common.viewRunHistory')}
|
||||
popupContent={t('common.viewRunHistory', { ns: 'workflow' })}
|
||||
>
|
||||
<div
|
||||
className={cn('group flex h-7 w-7 cursor-pointer items-center justify-center rounded-md hover:bg-state-accent-hover', open && 'bg-state-accent-hover')}
|
||||
@ -117,41 +113,41 @@ const ViewHistory = ({
|
||||
)
|
||||
}
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[12]'>
|
||||
<PortalToFollowElemContent className="z-[12]">
|
||||
<div
|
||||
className='ml-2 flex w-[240px] flex-col overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl'
|
||||
className="ml-2 flex w-[240px] flex-col overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
|
||||
style={{
|
||||
maxHeight: 'calc(2 / 3 * 100vh)',
|
||||
}}
|
||||
>
|
||||
<div className='sticky top-0 flex items-center justify-between bg-components-panel-bg px-4 pt-3 text-base font-semibold text-text-primary'>
|
||||
<div className='grow'>{t('workflow.common.runHistory')}</div>
|
||||
<div className="sticky top-0 flex items-center justify-between bg-components-panel-bg px-4 pt-3 text-base font-semibold text-text-primary">
|
||||
<div className="grow">{t('common.runHistory', { ns: 'workflow' })}</div>
|
||||
<div
|
||||
className='flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center'
|
||||
className="flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center"
|
||||
onClick={() => {
|
||||
onClearLogAndMessageModal?.()
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-tertiary' />
|
||||
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
isLoading && (
|
||||
<div className='flex h-10 items-center justify-center'>
|
||||
<div className="flex h-10 items-center justify-center">
|
||||
<Loading />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!isLoading && (
|
||||
<div className='p-2'>
|
||||
<div className="p-2">
|
||||
{
|
||||
!data?.data.length && (
|
||||
<div className='py-12'>
|
||||
<ClockPlaySlim className='mx-auto mb-2 h-8 w-8 text-text-quaternary' />
|
||||
<div className='text-center text-[13px] text-text-quaternary'>
|
||||
{t('workflow.common.notRunning')}
|
||||
<div className="py-12">
|
||||
<ClockPlaySlim className="mx-auto mb-2 h-8 w-8 text-text-quaternary" />
|
||||
<div className="text-center text-[13px] text-text-quaternary">
|
||||
{t('common.notRunning', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -180,17 +176,17 @@ const ViewHistory = ({
|
||||
>
|
||||
{
|
||||
!isChatMode && item.status === WorkflowRunningStatus.Stopped && (
|
||||
<AlertTriangle className='mr-1.5 mt-0.5 h-3.5 w-3.5 text-[#F79009]' />
|
||||
<AlertTriangle className="mr-1.5 mt-0.5 h-3.5 w-3.5 text-[#F79009]" />
|
||||
)
|
||||
}
|
||||
{
|
||||
!isChatMode && item.status === WorkflowRunningStatus.Failed && (
|
||||
<RiErrorWarningLine className='mr-1.5 mt-0.5 h-3.5 w-3.5 text-[#F04438]' />
|
||||
<RiErrorWarningLine className="mr-1.5 mt-0.5 h-3.5 w-3.5 text-[#F04438]" />
|
||||
)
|
||||
}
|
||||
{
|
||||
!isChatMode && item.status === WorkflowRunningStatus.Succeeded && (
|
||||
<RiCheckboxCircleLine className='mr-1.5 mt-0.5 h-3.5 w-3.5 text-[#12B76A]' />
|
||||
<RiCheckboxCircleLine className="mr-1.5 mt-0.5 h-3.5 w-3.5 text-[#12B76A]" />
|
||||
)
|
||||
}
|
||||
<div>
|
||||
@ -202,8 +198,11 @@ const ViewHistory = ({
|
||||
>
|
||||
{`Test ${isChatMode ? 'Chat' : 'Run'}${formatWorkflowRunIdentifier(item.finished_at)}`}
|
||||
</div>
|
||||
<div className='flex items-center text-xs leading-[18px] text-text-tertiary'>
|
||||
{item.created_by_account?.name} · {formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
|
||||
<div className="flex items-center text-xs leading-[18px] text-text-tertiary">
|
||||
{item.created_by_account?.name}
|
||||
{' '}
|
||||
·
|
||||
{formatTimeFromNow((item.finished_at || item.created_at) * 1000)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,31 +1,30 @@
|
||||
import type { WorkflowHistoryState } from '../workflow-history-store'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiHistoryLine,
|
||||
} from '@remixicon/react'
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import {
|
||||
RiCloseLine,
|
||||
RiHistoryLine,
|
||||
} from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflowHistory,
|
||||
} from '../hooks'
|
||||
import TipPopup from '../operator/tip-popup'
|
||||
import type { WorkflowHistoryState } from '../workflow-history-store'
|
||||
import Divider from '../../base/divider'
|
||||
import cn from '@/utils/classnames'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import classNames from '@/utils/classnames'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import Divider from '../../base/divider'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflowHistory,
|
||||
} from '../hooks'
|
||||
import TipPopup from '../operator/tip-popup'
|
||||
|
||||
type ChangeHistoryEntry = {
|
||||
label: string
|
||||
@ -84,7 +83,7 @@ const ViewWorkflowHistory = () => {
|
||||
return
|
||||
|
||||
const count = index < 0 ? index * -1 : index
|
||||
return `${index > 0 ? t('workflow.changeHistory.stepForward', { count }) : t('workflow.changeHistory.stepBackward', { count })}`
|
||||
return `${index > 0 ? t('changeHistory.stepForward', { ns: 'workflow', count }) : t('changeHistory.stepBackward', { ns: 'workflow', count })}`
|
||||
}, [t])
|
||||
|
||||
const calculateChangeList: ChangeHistoryList = useMemo(() => {
|
||||
@ -97,10 +96,12 @@ const ViewWorkflowHistory = () => {
|
||||
index: reverse ? list.length - 1 - index - startIndex : index - startIndex,
|
||||
state: {
|
||||
...state,
|
||||
workflowHistoryEventMeta: state.workflowHistoryEventMeta ? {
|
||||
...state.workflowHistoryEventMeta,
|
||||
nodeTitle: state.workflowHistoryEventMeta.nodeTitle || targetTitle,
|
||||
} : undefined,
|
||||
workflowHistoryEventMeta: state.workflowHistoryEventMeta
|
||||
? {
|
||||
...state.workflowHistoryEventMeta,
|
||||
nodeTitle: state.workflowHistoryEventMeta.nodeTitle || targetTitle,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
}).filter(Boolean)
|
||||
@ -128,7 +129,7 @@ const ViewWorkflowHistory = () => {
|
||||
return (
|
||||
(
|
||||
<PortalToFollowElem
|
||||
placement='bottom-end'
|
||||
placement="bottom-end"
|
||||
offset={{
|
||||
mainAxis: 4,
|
||||
crossAxis: 131,
|
||||
@ -138,14 +139,12 @@ const ViewWorkflowHistory = () => {
|
||||
>
|
||||
<PortalToFollowElemTrigger onClick={() => !nodesReadOnly && setOpen(v => !v)}>
|
||||
<TipPopup
|
||||
title={t('workflow.changeHistory.title')}
|
||||
title={t('changeHistory.title', { ns: 'workflow' })}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
classNames('flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary',
|
||||
open && 'bg-state-accent-active text-text-accent',
|
||||
nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled',
|
||||
)}
|
||||
cn('flex h-8 w-8 cursor-pointer items-center justify-center rounded-md text-text-tertiary hover:bg-state-base-hover hover:text-text-secondary', open && 'bg-state-accent-active text-text-accent', nodesReadOnly && 'cursor-not-allowed text-text-disabled hover:bg-transparent hover:text-text-disabled')
|
||||
}
|
||||
onClick={() => {
|
||||
if (nodesReadOnly)
|
||||
return
|
||||
@ -153,110 +152,115 @@ const ViewWorkflowHistory = () => {
|
||||
setShowMessageLogModal(false)
|
||||
}}
|
||||
>
|
||||
<RiHistoryLine className='h-4 w-4' />
|
||||
<RiHistoryLine className="h-4 w-4" />
|
||||
</div>
|
||||
</TipPopup>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[12]'>
|
||||
<PortalToFollowElemContent className="z-[12]">
|
||||
<div
|
||||
className='ml-2 flex min-w-[240px] max-w-[360px] flex-col overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl backdrop-blur-[5px]'
|
||||
className="ml-2 flex min-w-[240px] max-w-[360px] flex-col overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-xl backdrop-blur-[5px]"
|
||||
>
|
||||
<div className='sticky top-0 flex items-center justify-between px-4 pt-3'>
|
||||
<div className='system-mg-regular grow text-text-secondary'>{t('workflow.changeHistory.title')}</div>
|
||||
<div className="sticky top-0 flex items-center justify-between px-4 pt-3">
|
||||
<div className="system-mg-regular grow text-text-secondary">{t('changeHistory.title', { ns: 'workflow' })}</div>
|
||||
<div
|
||||
className='flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center'
|
||||
className="flex h-6 w-6 shrink-0 cursor-pointer items-center justify-center"
|
||||
onClick={() => {
|
||||
setCurrentLogItem()
|
||||
setShowMessageLogModal(false)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<RiCloseLine className='h-4 w-4 text-text-secondary' />
|
||||
<RiCloseLine className="h-4 w-4 text-text-secondary" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="overflow-y-auto p-2"
|
||||
style={{
|
||||
maxHeight: 'calc(1 / 2 * 100vh)',
|
||||
}}
|
||||
>
|
||||
{
|
||||
!calculateChangeList.statesCount && (
|
||||
<div className="py-12">
|
||||
<RiHistoryLine className="mx-auto mb-2 h-8 w-8 text-text-tertiary" />
|
||||
<div className="text-center text-[13px] text-text-tertiary">
|
||||
{t('changeHistory.placeholder', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="flex flex-col">
|
||||
{
|
||||
calculateChangeList.futureStates.map((item: ChangeHistoryEntry) => (
|
||||
<div
|
||||
key={item?.index}
|
||||
className={cn(
|
||||
'mb-0.5 flex cursor-pointer rounded-lg px-2 py-[7px] text-text-secondary hover:bg-state-base-hover',
|
||||
item?.index === currentHistoryStateIndex && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
handleSetState(item)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{composeHistoryItemLabel(
|
||||
item?.state?.workflowHistoryEventMeta?.nodeTitle,
|
||||
item?.label || t('changeHistory.sessionStart', { ns: 'workflow' }),
|
||||
)}
|
||||
{' '}
|
||||
(
|
||||
{calculateStepLabel(item?.index)}
|
||||
{item?.index === currentHistoryStateIndex && t('changeHistory.currentState', { ns: 'workflow' })}
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
calculateChangeList.pastStates.map((item: ChangeHistoryEntry) => (
|
||||
<div
|
||||
key={item?.index}
|
||||
className={cn(
|
||||
'mb-0.5 flex cursor-pointer rounded-lg px-2 py-[7px] hover:bg-state-base-hover',
|
||||
item?.index === calculateChangeList.statesCount - 1 && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
handleSetState(item)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{composeHistoryItemLabel(
|
||||
item?.state?.workflowHistoryEventMeta?.nodeTitle,
|
||||
item?.label || t('changeHistory.sessionStart', { ns: 'workflow' }),
|
||||
)}
|
||||
{' '}
|
||||
(
|
||||
{calculateStepLabel(item?.index)}
|
||||
)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
(
|
||||
<div
|
||||
className='overflow-y-auto p-2'
|
||||
style={{
|
||||
maxHeight: 'calc(1 / 2 * 100vh)',
|
||||
}}
|
||||
>
|
||||
{
|
||||
!calculateChangeList.statesCount && (
|
||||
<div className='py-12'>
|
||||
<RiHistoryLine className='mx-auto mb-2 h-8 w-8 text-text-tertiary' />
|
||||
<div className='text-center text-[13px] text-text-tertiary'>
|
||||
{t('workflow.changeHistory.placeholder')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='flex flex-col'>
|
||||
{
|
||||
calculateChangeList.futureStates.map((item: ChangeHistoryEntry) => (
|
||||
<div
|
||||
key={item?.index}
|
||||
className={cn(
|
||||
'mb-0.5 flex cursor-pointer rounded-lg px-2 py-[7px] text-text-secondary hover:bg-state-base-hover',
|
||||
item?.index === currentHistoryStateIndex && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
handleSetState(item)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{composeHistoryItemLabel(
|
||||
item?.state?.workflowHistoryEventMeta?.nodeTitle,
|
||||
item?.label || t('workflow.changeHistory.sessionStart'),
|
||||
)} ({calculateStepLabel(item?.index)}{item?.index === currentHistoryStateIndex && t('workflow.changeHistory.currentState')})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{
|
||||
calculateChangeList.pastStates.map((item: ChangeHistoryEntry) => (
|
||||
<div
|
||||
key={item?.index}
|
||||
className={cn(
|
||||
'mb-0.5 flex cursor-pointer rounded-lg px-2 py-[7px] hover:bg-state-base-hover',
|
||||
item?.index === calculateChangeList.statesCount - 1 && 'bg-state-base-hover',
|
||||
)}
|
||||
onClick={() => {
|
||||
handleSetState(item)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center text-[13px] font-medium leading-[18px] text-text-secondary',
|
||||
)}
|
||||
>
|
||||
{composeHistoryItemLabel(
|
||||
item?.state?.workflowHistoryEventMeta?.nodeTitle,
|
||||
item?.label || t('workflow.changeHistory.sessionStart'),
|
||||
)} ({calculateStepLabel(item?.index)})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
!!calculateChangeList.statesCount && (
|
||||
<div className='px-0.5'>
|
||||
<Divider className='m-0' />
|
||||
<div className="px-0.5">
|
||||
<Divider className="m-0" />
|
||||
<div
|
||||
className={cn(
|
||||
'my-0.5 flex cursor-pointer rounded-lg px-2 py-[7px] text-text-secondary',
|
||||
@ -273,16 +277,16 @@ const ViewWorkflowHistory = () => {
|
||||
'flex items-center text-[13px] font-medium leading-[18px]',
|
||||
)}
|
||||
>
|
||||
{t('workflow.changeHistory.clearHistory')}
|
||||
{t('changeHistory.clearHistory', { ns: 'workflow' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="w-[240px] px-3 py-2 text-xs text-text-tertiary" >
|
||||
<div className="mb-1 flex h-[22px] items-center font-medium uppercase">{t('workflow.changeHistory.hint')}</div>
|
||||
<div className="mb-1 leading-[18px] text-text-tertiary">{t('workflow.changeHistory.hintText')}</div>
|
||||
<div className="w-[240px] px-3 py-2 text-xs text-text-tertiary">
|
||||
<div className="mb-1 flex h-[22px] items-center font-medium uppercase">{t('changeHistory.hint', { ns: 'workflow' })}</div>
|
||||
<div className="mb-1 leading-[18px] text-text-tertiary">{t('changeHistory.hintText', { ns: 'workflow' })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemContent>
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { memo } from 'react'
|
||||
import { useViewport } from 'reactflow'
|
||||
import { useStore } from '../store'
|
||||
import type {
|
||||
HelpLineHorizontalPosition,
|
||||
HelpLineVerticalPosition,
|
||||
} from './types'
|
||||
import { memo } from 'react'
|
||||
import { useViewport } from 'reactflow'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const HelpLineHorizontal = memo(({
|
||||
top,
|
||||
@ -15,7 +15,7 @@ const HelpLineHorizontal = memo(({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-[9] h-px bg-primary-300'
|
||||
className="absolute z-[9] h-px bg-primary-300"
|
||||
style={{
|
||||
top: top * zoom + y,
|
||||
left: left * zoom + x,
|
||||
@ -35,7 +35,7 @@ const HelpLineVertical = memo(({
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-[9] w-[1px] bg-primary-300'
|
||||
className="absolute z-[9] w-[1px] bg-primary-300"
|
||||
style={{
|
||||
top: top * zoom + y,
|
||||
left: left * zoom + x,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import type { Shape } from './store'
|
||||
import {
|
||||
createContext,
|
||||
useEffect,
|
||||
@ -7,7 +8,6 @@ import { useStore } from 'reactflow'
|
||||
import {
|
||||
createHooksStore,
|
||||
} from './store'
|
||||
import type { Shape } from './store'
|
||||
|
||||
type HooksStore = ReturnType<typeof createHooksStore>
|
||||
export const HooksStoreContext = createContext<HooksStore | null | undefined>(null)
|
||||
|
||||
@ -1,26 +1,22 @@
|
||||
import type { FileUpload } from '../../base/features/types'
|
||||
import type {
|
||||
BlockEnum,
|
||||
Node,
|
||||
NodeDefault,
|
||||
ToolWithProvider,
|
||||
ValueSelector,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { useContext } from 'react'
|
||||
import {
|
||||
noop,
|
||||
} from 'lodash-es'
|
||||
import {
|
||||
useStore as useZustandStore,
|
||||
} from 'zustand'
|
||||
import { createStore } from 'zustand/vanilla'
|
||||
import { HooksStoreContext } from './provider'
|
||||
import type {
|
||||
BlockEnum,
|
||||
NodeDefault,
|
||||
ToolWithProvider,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { IOtherOptions } from '@/service/base'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import type {
|
||||
Node,
|
||||
ValueSelector,
|
||||
} from '@/app/components/workflow/types'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import type { FileUpload } from '../../base/features/types'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
|
||||
export type AvailableNodesMetaData = {
|
||||
nodes: NodeDefault[]
|
||||
@ -51,7 +47,7 @@ export type CommonHooksFnMap = {
|
||||
handleWorkflowTriggerPluginRunInWorkflow: (nodeId?: string) => void
|
||||
handleWorkflowRunAllTriggersInWorkflow: (nodeIds: string[]) => void
|
||||
availableNodesMetaData?: AvailableNodesMetaData
|
||||
getWorkflowRunAndTraceUrl: (runId?: string) => { runUrl: string; traceUrl: string }
|
||||
getWorkflowRunAndTraceUrl: (runId?: string) => { runUrl: string, traceUrl: string }
|
||||
exportCheck?: () => Promise<void>
|
||||
handleExportDSL?: (include?: boolean, flowId?: string) => Promise<void>
|
||||
fetchInspectVars: (params: { passInVars?: boolean, vars?: VarInInspect[], passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>, passedInSchemaTypeDefinitions?: SchemaTypeDefinition[] }) => Promise<void>
|
||||
|
||||
@ -1,27 +1,29 @@
|
||||
export * from './use-auto-generate-webhook-url'
|
||||
export * from './use-auto-generate-webhook-url'
|
||||
export * from './use-available-blocks'
|
||||
export * from './use-checklist'
|
||||
export * from './use-DSL'
|
||||
export * from './use-edges-interactions'
|
||||
export * from './use-inspect-vars-crud'
|
||||
export * from './use-node-data-update'
|
||||
export * from './use-nodes-interactions'
|
||||
export * from './use-nodes-sync-draft'
|
||||
export * from './use-workflow'
|
||||
export * from './use-workflow-run'
|
||||
export * from './use-checklist'
|
||||
export * from './use-selection-interactions'
|
||||
export * from './use-panel-interactions'
|
||||
export * from './use-workflow-start-run'
|
||||
export * from './use-nodes-layout'
|
||||
export * from './use-workflow-history'
|
||||
export * from './use-workflow-variables'
|
||||
export * from './use-nodes-meta-data'
|
||||
export * from './use-nodes-sync-draft'
|
||||
export * from './use-panel-interactions'
|
||||
export * from './use-selection-interactions'
|
||||
export * from './use-serial-async-callback'
|
||||
export * from './use-serial-async-callback'
|
||||
export * from './use-set-workflow-vars-with-value'
|
||||
export * from './use-shortcuts'
|
||||
export * from './use-tool-icon'
|
||||
export * from './use-workflow'
|
||||
export * from './use-workflow-comment'
|
||||
export * from './use-workflow-history'
|
||||
export * from './use-workflow-interactions'
|
||||
export * from './use-workflow-mode'
|
||||
export * from './use-nodes-meta-data'
|
||||
export * from './use-available-blocks'
|
||||
export * from './use-workflow-refresh-draft'
|
||||
export * from './use-tool-icon'
|
||||
export * from './use-DSL'
|
||||
export * from './use-inspect-vars-crud'
|
||||
export * from './use-set-workflow-vars-with-value'
|
||||
export * from './use-workflow-run'
|
||||
export * from './use-workflow-search'
|
||||
export * from './use-auto-generate-webhook-url'
|
||||
export * from './use-serial-async-callback'
|
||||
export * from './use-workflow-comment'
|
||||
export * from './use-workflow-start-run'
|
||||
export * from './use-workflow-variables'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
@ -23,8 +23,9 @@ export const useAvailableBlocks = (nodeType?: BlockEnum, inContainer?: boolean)
|
||||
const availablePrevBlocks = useMemo(() => {
|
||||
if (!nodeType || nodeType === BlockEnum.Start || nodeType === BlockEnum.DataSource
|
||||
|| nodeType === BlockEnum.TriggerPlugin || nodeType === BlockEnum.TriggerWebhook
|
||||
|| nodeType === BlockEnum.TriggerSchedule)
|
||||
|| nodeType === BlockEnum.TriggerSchedule) {
|
||||
return []
|
||||
}
|
||||
|
||||
return availableNodesType
|
||||
}, [availableNodesType, nodeType])
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges, useNodes, useStoreApi } from 'reactflow'
|
||||
import type { AgentNodeType } from '../nodes/agent/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import type { KnowledgeBaseNodeType } from '../nodes/knowledge-base/types'
|
||||
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import type {
|
||||
CommonEdgeType,
|
||||
CommonNodeType,
|
||||
@ -12,50 +11,53 @@ import type {
|
||||
Node,
|
||||
ValueSelector,
|
||||
} from '../types'
|
||||
import { BlockEnum } from '../types'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { I18nKeysWithPrefix } from '@/types/i18n'
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges, useStoreApi } from 'reactflow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { MAX_TREE_DEPTH } from '@/config'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
} from '../constants'
|
||||
import { useDatasetsDetailStore } from '../datasets-detail-store/store'
|
||||
import {
|
||||
useGetToolIcon,
|
||||
useNodesMetaData,
|
||||
} from '../hooks'
|
||||
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import { BlockEnum } from '../types'
|
||||
import {
|
||||
getDataSourceCheckParams,
|
||||
getToolCheckParams,
|
||||
getValidTreeNodes,
|
||||
} from '../utils'
|
||||
import { getTriggerCheckParams } from '../utils/trigger'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
} from '../constants'
|
||||
import {
|
||||
useGetToolIcon,
|
||||
useNodesMetaData,
|
||||
} from '../hooks'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import type { AgentNodeType } from '../nodes/agent/types'
|
||||
import { useStrategyProviders } from '@/service/use-strategy'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import { useDatasetsDetailStore } from '../datasets-detail-store/store'
|
||||
import type { KnowledgeRetrievalNodeType } from '../nodes/knowledge-retrieval/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { MAX_TREE_DEPTH } from '@/config'
|
||||
import useNodesAvailableVarList, { useGetNodesAvailableVarList } from './use-nodes-available-var-list'
|
||||
import { getNodeUsedVars, isSpecialVar } from '../nodes/_base/components/variable/utils'
|
||||
import type { Emoji } from '@/app/components/tools/types'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import type { KnowledgeBaseNodeType } from '../nodes/knowledge-base/types'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
|
||||
export type ChecklistItem = {
|
||||
id: string
|
||||
@ -65,6 +67,7 @@ export type ChecklistItem = {
|
||||
unConnected?: boolean
|
||||
errorMessage?: string
|
||||
canNavigate: boolean
|
||||
disableGoTo?: boolean
|
||||
}
|
||||
|
||||
const START_NODE_TYPES: BlockEnum[] = [
|
||||
@ -74,6 +77,13 @@ const START_NODE_TYPES: BlockEnum[] = [
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
|
||||
// Node types that depend on plugins
|
||||
const PLUGIN_DEPENDENT_TYPES: BlockEnum[] = [
|
||||
BlockEnum.Tool,
|
||||
BlockEnum.DataSource,
|
||||
BlockEnum.TriggerPlugin,
|
||||
]
|
||||
|
||||
export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
@ -156,7 +166,14 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
if (node.type === CUSTOM_NODE) {
|
||||
const checkData = getCheckData(node.data)
|
||||
const validator = nodesExtraData?.[node.data.type as BlockEnum]?.checkValid
|
||||
let errorMessage = validator ? validator(checkData, t, moreDataForCheckValid).errorMessage : undefined
|
||||
const isPluginMissing = PLUGIN_DEPENDENT_TYPES.includes(node.data.type as BlockEnum) && node.data._pluginInstallLocked
|
||||
|
||||
// Check if plugin is installed for plugin-dependent nodes first
|
||||
let errorMessage: string | undefined
|
||||
if (isPluginMissing)
|
||||
errorMessage = t('nodes.common.pluginNotInstalled', { ns: 'workflow' })
|
||||
else if (validator)
|
||||
errorMessage = validator(checkData, t, moreDataForCheckValid).errorMessage
|
||||
|
||||
if (!errorMessage) {
|
||||
const availableVars = map[node.id].availableVars
|
||||
@ -168,10 +185,10 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
if (usedNode) {
|
||||
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
|
||||
if (!usedVar)
|
||||
errorMessage = t('workflow.errorMsg.invalidVariable')
|
||||
errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' })
|
||||
}
|
||||
else {
|
||||
errorMessage = t('workflow.errorMsg.invalidVariable')
|
||||
errorMessage = t('errorMsg.invalidVariable', { ns: 'workflow' })
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -193,7 +210,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
toolIcon,
|
||||
unConnected: isUnconnected && !canSkipConnectionCheck,
|
||||
errorMessage,
|
||||
canNavigate: true,
|
||||
canNavigate: !isPluginMissing,
|
||||
disableGoTo: isPluginMissing,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -206,8 +224,8 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
list.push({
|
||||
id: 'start-node-required',
|
||||
type: BlockEnum.Start,
|
||||
title: t('workflow.panel.startNode'),
|
||||
errorMessage: t('workflow.common.needStartNode'),
|
||||
title: t('panel.startNode', { ns: 'workflow' }),
|
||||
errorMessage: t('common.needStartNode', { ns: 'workflow' }),
|
||||
canNavigate: false,
|
||||
})
|
||||
}
|
||||
@ -220,8 +238,11 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => {
|
||||
list.push({
|
||||
id: `${type}-need-added`,
|
||||
type,
|
||||
title: t(`workflow.blocks.${type}`),
|
||||
errorMessage: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }),
|
||||
// We don't have enough type info for t() here
|
||||
|
||||
title: t(`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }),
|
||||
|
||||
errorMessage: t('common.needAdd', { ns: 'workflow', node: t(`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }) }),
|
||||
canNavigate: false,
|
||||
})
|
||||
}
|
||||
@ -249,6 +270,8 @@ export const useChecklistBeforePublish = () => {
|
||||
const { data: buildInTools } = useAllBuiltInTools()
|
||||
const { data: customTools } = useAllCustomTools()
|
||||
const { data: workflowTools } = useAllWorkflowTools()
|
||||
const appMode = useAppStore.getState().appDetail?.mode
|
||||
const shouldCheckStartNode = appMode === AppModeEnum.WORKFLOW || appMode === AppModeEnum.ADVANCED_CHAT
|
||||
|
||||
const getCheckData = useCallback((data: CommonNodeType<{}>, datasets: DataSet[]) => {
|
||||
let checkData = data
|
||||
@ -291,7 +314,7 @@ export const useChecklistBeforePublish = () => {
|
||||
const { validNodes, maxDepth } = getValidTreeNodes(filteredNodes, edges)
|
||||
|
||||
if (maxDepth > MAX_TREE_DEPTH) {
|
||||
notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEPTH }) })
|
||||
notify({ type: 'error', message: t('common.maxTreeDepth', { ns: 'workflow', depth: MAX_TREE_DEPTH }) })
|
||||
return false
|
||||
}
|
||||
// Before publish, we need to fetch datasets detail, in case of the settings of datasets have been changed
|
||||
@ -355,28 +378,33 @@ export const useChecklistBeforePublish = () => {
|
||||
if (usedNode) {
|
||||
const usedVar = usedNode.vars.find(v => v.variable === variable?.[1])
|
||||
if (!usedVar) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.errorMsg.invalidVariable')}` })
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
else {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.errorMsg.invalidVariable')}` })
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('errorMsg.invalidVariable', { ns: 'workflow' })}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!validNodes.find(n => n.id === node.id)) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('workflow.common.needConnectTip')}` })
|
||||
const isStartNodeMeta = nodesExtraData?.[node.data.type as BlockEnum]?.metaData.isStart ?? false
|
||||
const canSkipConnectionCheck = shouldCheckStartNode ? isStartNodeMeta : true
|
||||
const isUnconnected = !validNodes.find(n => n.id === node.id)
|
||||
|
||||
if (isUnconnected && !canSkipConnectionCheck) {
|
||||
notify({ type: 'error', message: `[${node.data.title}] ${t('common.needConnectTip', { ns: 'workflow' })}` })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('workflow.common.needStartNode') })
|
||||
return false
|
||||
if (shouldCheckStartNode) {
|
||||
const startNodesFiltered = nodes.filter(node => START_NODE_TYPES.includes(node.data.type as BlockEnum))
|
||||
if (startNodesFiltered.length === 0) {
|
||||
notify({ type: 'error', message: t('common.needStartNode', { ns: 'workflow' }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const isRequiredNodesType = Object.keys(nodesExtraData!).filter((key: any) => (nodesExtraData as any)[key].metaData.isRequired)
|
||||
@ -385,13 +413,13 @@ export const useChecklistBeforePublish = () => {
|
||||
const type = isRequiredNodesType[i]
|
||||
|
||||
if (!filteredNodes.find(node => node.data.type === type)) {
|
||||
notify({ type: 'error', message: t('workflow.common.needAdd', { node: t(`workflow.blocks.${type}`) }) })
|
||||
notify({ type: 'error', message: t('common.needAdd', { ns: 'workflow', node: t(`blocks.${type}` as I18nKeysWithPrefix<'workflow', 'blocks.'>, { ns: 'workflow' }) }) })
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools])
|
||||
}, [store, notify, t, language, nodesExtraData, strategyProviders, updateDatasetsDetail, getCheckData, workflowStore, buildInTools, customTools, workflowTools, shouldCheckStartNode])
|
||||
|
||||
return {
|
||||
handleCheckBeforePublish,
|
||||
@ -400,14 +428,14 @@ export const useChecklistBeforePublish = () => {
|
||||
|
||||
export const useWorkflowRunValidation = () => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes<CommonNodeType>()
|
||||
const nodes = useNodes()
|
||||
const edges = useEdges<CommonEdgeType>()
|
||||
const needWarningNodes = useChecklist(nodes, edges)
|
||||
const { notify } = useToastContext()
|
||||
|
||||
const validateBeforeRun = useCallback(() => {
|
||||
if (needWarningNodes.length > 0) {
|
||||
notify({ type: 'error', message: t('workflow.panel.checklistTip') })
|
||||
notify({ type: 'error', message: t('panel.checklistTip', { ns: 'workflow' }) })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import type { ModelConfig, VisionSetting } from '@/app/components/workflow/types'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
import type { ModelConfig, VisionSetting } from '@/app/components/workflow/types'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useTextGenerationCurrentProviderAndModelAndModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { Resolution } from '@/types/app'
|
||||
import { useIsChatMode } from './use-workflow'
|
||||
|
||||
type Payload = {
|
||||
enabled: boolean
|
||||
@ -49,6 +49,9 @@ const useConfigVision = (model: ModelConfig, {
|
||||
variable_selector: ['sys', 'files'],
|
||||
}
|
||||
}
|
||||
else if (!enabled) {
|
||||
delete draft.configs
|
||||
}
|
||||
})
|
||||
onChange(newPayload)
|
||||
}, [isChatMode, onChange, payload])
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
import type { TestRunOptions, TriggerOption } from '../header/test-run-menu'
|
||||
import type { CommonNodeType } from '../types'
|
||||
import { useMemo } from 'react'
|
||||
import { useNodes } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { BlockEnum, type CommonNodeType } from '../types'
|
||||
import { getWorkflowEntryNode } from '../utils/workflow-entry'
|
||||
import { type TestRunOptions, type TriggerOption, TriggerType } from '../header/test-run-menu'
|
||||
import { TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { useStore } from '../store'
|
||||
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
||||
import { useAllTriggerPlugins } from '@/service/use-triggers'
|
||||
import BlockIcon from '../block-icon'
|
||||
import { TriggerType } from '../header/test-run-menu'
|
||||
import { useStore } from '../store'
|
||||
import { BlockEnum } from '../types'
|
||||
import { getWorkflowEntryNode } from '../utils/workflow-entry'
|
||||
|
||||
export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
const { t } = useTranslation()
|
||||
@ -25,17 +27,18 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
for (const node of nodes) {
|
||||
const nodeData = node.data as CommonNodeType
|
||||
|
||||
if (!nodeData?.type) continue
|
||||
if (!nodeData?.type)
|
||||
continue
|
||||
|
||||
if (nodeData.type === BlockEnum.Start) {
|
||||
userInput = {
|
||||
id: node.id,
|
||||
type: TriggerType.UserInput,
|
||||
name: nodeData.title || t('workflow.blocks.start'),
|
||||
name: nodeData.title || t('blocks.start', { ns: 'workflow' }),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.Start}
|
||||
size='md'
|
||||
size="md"
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
@ -46,11 +49,11 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Schedule,
|
||||
name: nodeData.title || t('workflow.blocks.trigger-schedule'),
|
||||
name: nodeData.title || t('blocks.trigger-schedule', { ns: 'workflow' }),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerSchedule}
|
||||
size='md'
|
||||
size="md"
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
@ -61,11 +64,11 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Webhook,
|
||||
name: nodeData.title || t('workflow.blocks.trigger-webhook'),
|
||||
name: nodeData.title || t('blocks.trigger-webhook', { ns: 'workflow' }),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerWebhook}
|
||||
size='md'
|
||||
size="md"
|
||||
/>
|
||||
),
|
||||
nodeId: node.id,
|
||||
@ -83,7 +86,7 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
const icon = (
|
||||
<BlockIcon
|
||||
type={BlockEnum.TriggerPlugin}
|
||||
size='md'
|
||||
size="md"
|
||||
toolIcon={triggerIcon}
|
||||
/>
|
||||
)
|
||||
@ -91,7 +94,7 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
allTriggers.push({
|
||||
id: node.id,
|
||||
type: TriggerType.Plugin,
|
||||
name: nodeData.title || (nodeData as any).plugin_name || t('workflow.blocks.trigger-plugin'),
|
||||
name: nodeData.title || (nodeData as any).plugin_name || t('blocks.trigger-plugin', { ns: 'workflow' }),
|
||||
icon,
|
||||
nodeId: node.id,
|
||||
enabled: true,
|
||||
@ -105,11 +108,11 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
userInput = {
|
||||
id: startNode.id,
|
||||
type: TriggerType.UserInput,
|
||||
name: (startNode.data as CommonNodeType)?.title || t('workflow.blocks.start'),
|
||||
name: (startNode.data as CommonNodeType)?.title || t('blocks.start', { ns: 'workflow' }),
|
||||
icon: (
|
||||
<BlockIcon
|
||||
type={BlockEnum.Start}
|
||||
size='md'
|
||||
size="md"
|
||||
/>
|
||||
),
|
||||
nodeId: startNode.id,
|
||||
@ -122,18 +125,20 @@ export const useDynamicTestRunOptions = (): TestRunOptions => {
|
||||
.map(trigger => trigger.nodeId)
|
||||
.filter((nodeId): nodeId is string => Boolean(nodeId))
|
||||
|
||||
const runAll: TriggerOption | undefined = triggerNodeIds.length > 1 ? {
|
||||
id: 'run-all',
|
||||
type: TriggerType.All,
|
||||
name: t('workflow.common.runAllTriggers'),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-purple-purple-500 text-white shadow-md">
|
||||
<TriggerAll className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
),
|
||||
relatedNodeIds: triggerNodeIds,
|
||||
enabled: true,
|
||||
} : undefined
|
||||
const runAll: TriggerOption | undefined = triggerNodeIds.length > 1
|
||||
? {
|
||||
id: 'run-all',
|
||||
type: TriggerType.All,
|
||||
name: t('common.runAllTriggers', { ns: 'workflow' }),
|
||||
icon: (
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-lg border-[0.5px] border-white/2 bg-util-colors-purple-purple-500 text-white shadow-md">
|
||||
<TriggerAll className="h-4.5 w-4.5" />
|
||||
</div>
|
||||
),
|
||||
relatedNodeIds: triggerNodeIds,
|
||||
enabled: true,
|
||||
}
|
||||
: undefined
|
||||
|
||||
return {
|
||||
userInput,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
|
||||
export const useEdgesInteractionsWithoutSync = () => {
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import type {
|
||||
EdgeMouseHandler,
|
||||
OnEdgesChange,
|
||||
} from 'reactflow'
|
||||
|
||||
import type {
|
||||
Node,
|
||||
} from '../types'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../utils'
|
||||
import { useCollaborativeWorkflow } from './use-collaborative-workflow'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||
import { useCollaborativeWorkflow } from './use-collaborative-workflow'
|
||||
import { useWorkflowHistory, WorkflowHistoryEvent } from './use-workflow-history'
|
||||
|
||||
export const useEdgesInteractions = () => {
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
@ -1,22 +1,21 @@
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { fetchAllInspectVars } from '@/service/workflow'
|
||||
import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/components/variable/use-match-schema-type'
|
||||
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
|
||||
import type { Node, ToolWithProvider } from '@/app/components/workflow/types'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import type { NodeWithVar, VarInInspect } from '@/types/workflow'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { useInvalidateConversationVarValues, useInvalidateSysVarValues } from '@/service/use-workflow'
|
||||
import { fetchAllInspectVars } from '@/service/workflow'
|
||||
import useMatchSchemaType, { getMatchedSchemaType } from '../nodes/_base/components/variable/use-match-schema-type'
|
||||
import { toNodeOutputVars } from '../nodes/_base/components/variable/utils'
|
||||
|
||||
type Params = {
|
||||
flowType: FlowType
|
||||
@ -99,9 +98,9 @@ export const useSetWorkflowVarsWithValue = ({
|
||||
}
|
||||
|
||||
const fetchInspectVars = useCallback(async (params: {
|
||||
passInVars?: boolean,
|
||||
vars?: VarInInspect[],
|
||||
passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>,
|
||||
passInVars?: boolean
|
||||
vars?: VarInInspect[]
|
||||
passedInAllPluginInfoList?: Record<string, ToolWithProvider[]>
|
||||
passedInSchemaTypeDefinitions?: SchemaTypeDefinition[]
|
||||
}) => {
|
||||
const { passInVars, vars, passedInAllPluginInfoList, passedInSchemaTypeDefinitions } = params
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { Node } from '../types'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { Node } from '../types'
|
||||
import { BlockEnum, isTriggerNode } from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { BlockEnum, isTriggerNode } from '../types'
|
||||
|
||||
// Entry node (Start/Trigger) wrapper offsets
|
||||
// The EntryNodeContainer adds a wrapper with status indicator above the actual node
|
||||
|
||||
@ -1,29 +1,28 @@
|
||||
import { fetchNodeInspectVars } from '@/service/workflow'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import type { ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { Node, ValueSelector } from '@/app/components/workflow/types'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import type { VarInInspect } from '@/types/workflow'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import {
|
||||
isConversationVar,
|
||||
isENV,
|
||||
isSystemVar,
|
||||
toNodeOutputVars,
|
||||
} from '@/app/components/workflow/nodes/_base/components/variable/utils'
|
||||
import { produce } from 'immer'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { useNodesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-nodes-interactions-without-sync'
|
||||
import { useEdgesInteractionsWithoutSync } from '@/app/components/workflow/hooks/use-edges-interactions-without-sync'
|
||||
import type { FlowType } from '@/types/common'
|
||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||
import useFLow from '@/service/use-flow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { SchemaTypeDefinition } from '@/service/use-common'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllMCPTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { fetchNodeInspectVars } from '@/service/workflow'
|
||||
import { VarInInspectType } from '@/types/workflow'
|
||||
|
||||
type Params = {
|
||||
flowId: string
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useStore } from '../store'
|
||||
import { produce } from 'immer'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import {
|
||||
useConversationVarValues,
|
||||
useSysVarValues,
|
||||
} from '@/service/use-workflow'
|
||||
import { FlowType } from '@/types/common'
|
||||
import { produce } from 'immer'
|
||||
import { useStore } from '../store'
|
||||
import { BlockEnum } from '../types'
|
||||
|
||||
const varsAppendStartNodeKeys = ['query', 'files']
|
||||
@ -16,19 +16,19 @@ const useInspectVarsCrud = () => {
|
||||
const { data: conversationVars } = useConversationVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const { data: allSystemVars } = useSysVarValues(configsMap?.flowType, !isRagPipeline ? configsMap?.flowId : '')
|
||||
const { varsAppendStartNode, systemVars } = (() => {
|
||||
if(allSystemVars?.length === 0)
|
||||
if (allSystemVars?.length === 0)
|
||||
return { varsAppendStartNode: [], systemVars: [] }
|
||||
const varsAppendStartNode = allSystemVars?.filter(({ name }) => varsAppendStartNodeKeys.includes(name)) || []
|
||||
const systemVars = allSystemVars?.filter(({ name }) => !varsAppendStartNodeKeys.includes(name)) || []
|
||||
return { varsAppendStartNode, systemVars }
|
||||
})()
|
||||
const nodesWithInspectVars = (() => {
|
||||
if(!partOfNodesWithInspectVars || partOfNodesWithInspectVars.length === 0)
|
||||
if (!partOfNodesWithInspectVars || partOfNodesWithInspectVars.length === 0)
|
||||
return []
|
||||
|
||||
const nodesWithInspectVars = produce(partOfNodesWithInspectVars, (draft) => {
|
||||
draft.forEach((nodeWithVars) => {
|
||||
if(nodeWithVars.nodeType === BlockEnum.Start)
|
||||
if (nodeWithVars.nodeType === BlockEnum.Start)
|
||||
nodeWithVars.vars = [...nodeWithVars.vars, ...varsAppendStartNode]
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import type { SyncCallback } from './use-nodes-sync-draft'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useCollaborativeWorkflow } from './use-collaborative-workflow'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
import { useCollaborativeWorkflow } from './use-collaborative-workflow'
|
||||
|
||||
type NodeDataUpdatePayload = {
|
||||
id: string
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { BlockEnum, type CommonNodeType } from '../types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import type { ToolNodeType } from '../nodes/tool/types'
|
||||
import type { PluginTriggerNodeType } from '../nodes/trigger-plugin/types'
|
||||
import type { DataSourceNodeType } from '../nodes/data-source/types'
|
||||
import type { CommonNodeType } from '../types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
@ -15,9 +16,9 @@ import {
|
||||
useAllTriggerPlugins,
|
||||
useInvalidateAllTriggerPlugins,
|
||||
} from '@/service/use-triggers'
|
||||
import { useInvalidDataSourceList } from '@/service/use-pipeline'
|
||||
import { useStore } from '../store'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { useStore } from '../store'
|
||||
import { BlockEnum } from '../types'
|
||||
|
||||
type InstallationState = {
|
||||
isChecking: boolean
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useWorkflow,
|
||||
useWorkflowVariables,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import { BlockEnum, type Node, type NodeOutPutVar, type ValueSelector, type Var } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
|
||||
type Params = {
|
||||
onlyLeafNodeVar?: boolean
|
||||
hideEnv?: boolean
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { NodeRunningStatus } from '../types'
|
||||
|
||||
@ -31,7 +31,7 @@ export const useNodesInteractionsWithoutSync = () => {
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if(node.data._runningStatus === NodeRunningStatus.Succeeded)
|
||||
if (node.data._runningStatus === NodeRunningStatus.Succeeded)
|
||||
node.data._runningStatus = undefined
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,4 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { produce } from 'immer'
|
||||
import type {
|
||||
NodeDragHandler,
|
||||
NodeMouseHandler,
|
||||
@ -10,15 +7,21 @@ import type {
|
||||
OnConnectStart,
|
||||
ResizeParamsWithDirection,
|
||||
} from 'reactflow'
|
||||
import type { PluginDefaultValue } from '../block-selector/types'
|
||||
import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
|
||||
import type { Edge, Node, OnNodeAdd } from '../types'
|
||||
import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import { produce } from 'immer'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
getConnectedEdges,
|
||||
getOutgoers,
|
||||
useReactFlow,
|
||||
} from 'reactflow'
|
||||
import type { PluginDefaultValue } from '../block-selector/types'
|
||||
import type { Edge, Node, OnNodeAdd } from '../types'
|
||||
import { BlockEnum, ControlMode, isTriggerNode } from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
import {
|
||||
CUSTOM_EDGE,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
@ -29,39 +32,39 @@ import {
|
||||
X_OFFSET,
|
||||
Y_OFFSET,
|
||||
} from '../constants'
|
||||
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
|
||||
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
|
||||
import { useNodeLoopInteractions } from '../nodes/loop/use-interactions'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { BlockEnum, ControlMode, isTriggerNode } from '../types'
|
||||
|
||||
import {
|
||||
genNewNodeTitleFromOld,
|
||||
generateNewNode,
|
||||
genNewNodeTitleFromOld,
|
||||
getNestedNodePosition,
|
||||
getNodeCustomTypeByNodeDataType,
|
||||
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||
getTopLeftNodePosition,
|
||||
} from '../utils'
|
||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||
import type { IterationNodeType } from '../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../nodes/loop/types'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
|
||||
import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types'
|
||||
import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions'
|
||||
import { useNodeLoopInteractions } from '../nodes/loop/use-interactions'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import { useWorkflowHistoryStore } from '../workflow-history-store'
|
||||
import { useAutoGenerateWebhookUrl } from './use-auto-generate-webhook-url'
|
||||
import { useCollaborativeWorkflow } from './use-collaborative-workflow'
|
||||
import { useHelpline } from './use-helpline'
|
||||
import useInspectVarsCrud from './use-inspect-vars-crud'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
useWorkflowReadOnly,
|
||||
} from './use-workflow'
|
||||
import {
|
||||
WorkflowHistoryEvent,
|
||||
useWorkflowHistory,
|
||||
WorkflowHistoryEvent,
|
||||
} from './use-workflow-history'
|
||||
import { useNodesMetaData } from './use-nodes-meta-data'
|
||||
import type { RAGPipelineVariables } from '@/models/pipeline'
|
||||
import useInspectVarsCrud from './use-inspect-vars-crud'
|
||||
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
|
||||
import { useCollaborativeWorkflow } from './use-collaborative-workflow'
|
||||
|
||||
// Entry node deletion restriction has been removed to allow empty workflows
|
||||
|
||||
@ -77,6 +80,7 @@ export const useNodesInteractions = () => {
|
||||
const collaborativeWorkflow = useCollaborativeWorkflow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const { store: workflowHistoryStore } = useWorkflowHistoryStore()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { getAfterNodesInSameBranch } = useWorkflow()
|
||||
const { getNodesReadOnly } = useNodesReadOnly()
|
||||
@ -87,30 +91,34 @@ export const useNodesInteractions = () => {
|
||||
const { handleNodeLoopChildDrag, handleNodeLoopChildrenCopy }
|
||||
= useNodeLoopInteractions()
|
||||
const dragNodeStartPosition = useRef({ x: 0, y: 0 } as {
|
||||
x: number;
|
||||
y: number;
|
||||
x: number
|
||||
y: number
|
||||
})
|
||||
const { nodesMap: nodesMetaDataMap } = useNodesMetaData()
|
||||
|
||||
const { saveStateToHistory } = useWorkflowHistory()
|
||||
const { saveStateToHistory, undo } = useWorkflowHistory()
|
||||
const autoGenerateWebhookUrl = useAutoGenerateWebhookUrl()
|
||||
|
||||
const handleNodeDragStart = useCallback<NodeDragHandler>(
|
||||
(_, node) => {
|
||||
workflowStore.setState({ nodeAnimation: false })
|
||||
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (
|
||||
node.type === CUSTOM_ITERATION_START_NODE
|
||||
|| node.type === CUSTOM_NOTE_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === CUSTOM_LOOP_START_NODE
|
||||
|| node.type === CUSTOM_NOTE_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
dragNodeStartPosition.current = {
|
||||
x: node.position.x,
|
||||
@ -122,11 +130,14 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodeDrag = useCallback<NodeDragHandler>(
|
||||
(e, node: Node) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE) return
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE)
|
||||
return
|
||||
|
||||
if (node.type === CUSTOM_LOOP_START_NODE) return
|
||||
if (node.type === CUSTOM_LOOP_START_NODE)
|
||||
return
|
||||
|
||||
e.stopPropagation()
|
||||
|
||||
@ -192,14 +203,17 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [getNodesReadOnly, collaborativeWorkflow, handleNodeIterationChildDrag, handleNodeLoopChildDrag, handleSetHelpline])
|
||||
},
|
||||
[getNodesReadOnly, collaborativeWorkflow, handleNodeIterationChildDrag, handleNodeLoopChildDrag, handleSetHelpline],
|
||||
)
|
||||
|
||||
const handleNodeDragStop = useCallback<NodeDragHandler>(
|
||||
(_, node) => {
|
||||
const { setHelpLineHorizontal, setHelpLineVertical }
|
||||
= workflowStore.getState()
|
||||
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { x, y } = dragNodeStartPosition.current
|
||||
if (!(x === node.position.x && y === node.position.y)) {
|
||||
@ -225,19 +239,22 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodeEnter = useCallback<NodeMouseHandler>(
|
||||
(_, node) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (
|
||||
node.type === CUSTOM_NOTE_NODE
|
||||
|| node.type === CUSTOM_ITERATION_START_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === CUSTOM_LOOP_START_NODE
|
||||
|| node.type === CUSTOM_NOTE_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const { nodes, edges, setNodes, setEdges } = collaborativeWorkflow.getState()
|
||||
const {
|
||||
@ -245,7 +262,8 @@ export const useNodesInteractions = () => {
|
||||
setEnteringNodePayload,
|
||||
} = workflowStore.getState()
|
||||
if (connectingNodePayload) {
|
||||
if (connectingNodePayload.nodeId === node.id) return
|
||||
if (connectingNodePayload.nodeId === node.id)
|
||||
return
|
||||
const connectingNode: Node = nodes.find(
|
||||
n => n.id === connectingNodePayload.nodeId,
|
||||
)!
|
||||
@ -262,22 +280,23 @@ export const useNodesInteractions = () => {
|
||||
draft.forEach((n) => {
|
||||
if (
|
||||
n.id === node.id
|
||||
&& fromType === 'source'
|
||||
&& (node.data.type === BlockEnum.VariableAssigner
|
||||
|| node.data.type === BlockEnum.VariableAggregator)
|
||||
&& fromType === 'source'
|
||||
&& (node.data.type === BlockEnum.VariableAssigner
|
||||
|| node.data.type === BlockEnum.VariableAggregator)
|
||||
) {
|
||||
if (!node.data.advanced_settings?.group_enabled)
|
||||
n.data._isEntering = true
|
||||
}
|
||||
if (
|
||||
n.id === node.id
|
||||
&& fromType === 'target'
|
||||
&& (connectingNode.data.type === BlockEnum.VariableAssigner
|
||||
|| connectingNode.data.type === BlockEnum.VariableAggregator)
|
||||
&& node.data.type !== BlockEnum.IfElse
|
||||
&& node.data.type !== BlockEnum.QuestionClassifier
|
||||
)
|
||||
&& fromType === 'target'
|
||||
&& (connectingNode.data.type === BlockEnum.VariableAssigner
|
||||
|| connectingNode.data.type === BlockEnum.VariableAggregator)
|
||||
&& node.data.type !== BlockEnum.IfElse
|
||||
&& node.data.type !== BlockEnum.QuestionClassifier
|
||||
) {
|
||||
n.data._isEntering = true
|
||||
}
|
||||
})
|
||||
})
|
||||
setNodes(newNodes, false)
|
||||
@ -288,7 +307,8 @@ export const useNodesInteractions = () => {
|
||||
|
||||
connectedEdges.forEach((edge) => {
|
||||
const currentEdge = draft.find(e => e.id === edge.id)
|
||||
if (currentEdge) currentEdge.data._connectedNodeIsHovering = true
|
||||
if (currentEdge)
|
||||
currentEdge.data._connectedNodeIsHovering = true
|
||||
})
|
||||
})
|
||||
setEdges(newEdges, false)
|
||||
@ -298,19 +318,22 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodeLeave = useCallback<NodeMouseHandler>(
|
||||
(_, node) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (
|
||||
node.type === CUSTOM_NOTE_NODE
|
||||
|| node.type === CUSTOM_ITERATION_START_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === CUSTOM_NOTE_NODE
|
||||
|| node.type === CUSTOM_LOOP_START_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const { setEnteringNodePayload } = workflowStore.getState()
|
||||
setEnteringNodePayload(undefined)
|
||||
@ -342,11 +365,13 @@ export const useNodesInteractions = () => {
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
const selectedNode = nodes.find(node => node.data.selected)
|
||||
|
||||
if (!cancelSelection && selectedNode?.id === nodeId) return
|
||||
if (!cancelSelection && selectedNode?.id === nodeId)
|
||||
return
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.id === nodeId) node.data.selected = !cancelSelection
|
||||
if (node.id === nodeId)
|
||||
node.data.selected = !cancelSelection
|
||||
else node.data.selected = false
|
||||
})
|
||||
})
|
||||
@ -373,16 +398,23 @@ export const useNodesInteractions = () => {
|
||||
})
|
||||
})
|
||||
setEdges(newEdges, false)
|
||||
}, [collaborativeWorkflow])
|
||||
},
|
||||
[collaborativeWorkflow],
|
||||
)
|
||||
|
||||
const handleNodeClick = useCallback<NodeMouseHandler>(
|
||||
(_, node) => {
|
||||
const { controlMode } = workflowStore.getState()
|
||||
if (controlMode === ControlMode.Comment) return
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE) return
|
||||
if (node.type === CUSTOM_LOOP_START_NODE) return
|
||||
if (node.data.type === BlockEnum.DataSourceEmpty) return
|
||||
if (node.data._pluginInstallLocked) return
|
||||
if (controlMode === ControlMode.Comment)
|
||||
return
|
||||
if (node.type === CUSTOM_ITERATION_START_NODE)
|
||||
return
|
||||
if (node.type === CUSTOM_LOOP_START_NODE)
|
||||
return
|
||||
if (node.data.type === BlockEnum.DataSourceEmpty)
|
||||
return
|
||||
if (node.data._pluginInstallLocked)
|
||||
return
|
||||
handleNodeSelect(node.id)
|
||||
},
|
||||
[handleNodeSelect, workflowStore],
|
||||
@ -390,20 +422,24 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodeConnect = useCallback<OnConnect>(
|
||||
({ source, sourceHandle, target, targetHandle }) => {
|
||||
if (source === target) return
|
||||
if (getNodesReadOnly()) return
|
||||
if (source === target)
|
||||
return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { nodes, edges, setNodes, setEdges } = collaborativeWorkflow.getState()
|
||||
const targetNode = nodes.find(node => node.id === target!)
|
||||
const sourceNode = nodes.find(node => node.id === source!)
|
||||
|
||||
if (targetNode?.parentId !== sourceNode?.parentId) return
|
||||
if (targetNode?.parentId !== sourceNode?.parentId)
|
||||
return
|
||||
|
||||
if (
|
||||
sourceNode?.type === CUSTOM_NOTE_NODE
|
||||
|| targetNode?.type === CUSTOM_NOTE_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
edges.find(
|
||||
@ -413,8 +449,9 @@ export const useNodesInteractions = () => {
|
||||
&& edge.target === target
|
||||
&& edge.targetHandle === targetHandle,
|
||||
)
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const parendNode = nodes.find(node => node.id === targetNode?.parentId)
|
||||
const isInIteration
|
||||
@ -480,20 +517,24 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodeConnectStart = useCallback<OnConnectStart>(
|
||||
(_, { nodeId, handleType, handleId }) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
if (nodeId && handleType) {
|
||||
const { setConnectingNodePayload } = workflowStore.getState()
|
||||
const { nodes } = collaborativeWorkflow.getState()
|
||||
const node = nodes.find(n => n.id === nodeId)!
|
||||
|
||||
if (node.type === CUSTOM_NOTE_NODE) return
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
return
|
||||
|
||||
if (
|
||||
node.data.type === BlockEnum.VariableAggregator
|
||||
|| node.data.type === BlockEnum.VariableAssigner
|
||||
)
|
||||
if (handleType === 'target') return
|
||||
) {
|
||||
if (handleType === 'target')
|
||||
return
|
||||
}
|
||||
|
||||
setConnectingNodePayload({
|
||||
nodeId,
|
||||
@ -502,11 +543,14 @@ export const useNodesInteractions = () => {
|
||||
handleId,
|
||||
})
|
||||
}
|
||||
}, [collaborativeWorkflow, workflowStore, getNodesReadOnly])
|
||||
},
|
||||
[collaborativeWorkflow, workflowStore, getNodesReadOnly],
|
||||
)
|
||||
|
||||
const handleNodeConnectEnd = useCallback<OnConnectEnd>(
|
||||
(e: any) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
connectingNodePayload,
|
||||
@ -527,7 +571,8 @@ export const useNodesInteractions = () => {
|
||||
const toNode = nodes.find(n => n.id === enteringNodePayload.nodeId)!
|
||||
const toParentNode = nodes.find(n => n.id === toNode.parentId)
|
||||
|
||||
if (fromNode.parentId !== toNode.parentId) return
|
||||
if (fromNode.parentId !== toNode.parentId)
|
||||
return
|
||||
|
||||
const { x, y } = screenToFlowPosition({ x: e.x, y: e.y })
|
||||
|
||||
@ -582,19 +627,22 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodeDelete = useCallback(
|
||||
(nodeId: string) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
|
||||
const currentNode = nodes[currentNodeIndex]
|
||||
|
||||
if (!currentNode) return
|
||||
if (!currentNode)
|
||||
return
|
||||
|
||||
if (
|
||||
nodesMetaDataMap?.[currentNode.data.type as BlockEnum]?.metaData
|
||||
.isUndeletable
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
deleteNodeInspectorVars(nodeId)
|
||||
if (currentNode.data.type === BlockEnum.Iteration) {
|
||||
@ -620,8 +668,8 @@ export const useNodesInteractions = () => {
|
||||
|
||||
if (!showConfirm) {
|
||||
setShowConfirm({
|
||||
title: t('workflow.nodes.iteration.deleteTitle'),
|
||||
desc: t('workflow.nodes.iteration.deleteDesc') || '',
|
||||
title: t('nodes.iteration.deleteTitle', { ns: 'workflow' }),
|
||||
desc: t('nodes.iteration.deleteDesc', { ns: 'workflow' }) || '',
|
||||
onConfirm: () => {
|
||||
iterationChildren.forEach((child) => {
|
||||
handleNodeDelete(child.id)
|
||||
@ -660,8 +708,8 @@ export const useNodesInteractions = () => {
|
||||
|
||||
if (!showConfirm) {
|
||||
setShowConfirm({
|
||||
title: t('workflow.nodes.loop.deleteTitle'),
|
||||
desc: t('workflow.nodes.loop.deleteDesc') || '',
|
||||
title: t('nodes.loop.deleteTitle', { ns: 'workflow' }),
|
||||
desc: t('nodes.loop.deleteDesc', { ns: 'workflow' }) || '',
|
||||
onConfirm: () => {
|
||||
loopChildren.forEach((child) => {
|
||||
handleNodeDelete(child.id)
|
||||
@ -684,7 +732,8 @@ export const useNodesInteractions = () => {
|
||||
if (ragPipelineVariables && setRagPipelineVariables) {
|
||||
const newRagPipelineVariables: RAGPipelineVariables = []
|
||||
ragPipelineVariables.forEach((variable) => {
|
||||
if (variable.belong_to_node_id === id) return
|
||||
if (variable.belong_to_node_id === id)
|
||||
return
|
||||
newRagPipelineVariables.push(variable)
|
||||
})
|
||||
setRagPipelineVariables(newRagPipelineVariables)
|
||||
@ -759,7 +808,8 @@ export const useNodesInteractions = () => {
|
||||
},
|
||||
{ prevNodeId, prevNodeSourceHandle, nextNodeId, nextNodeTargetHandle },
|
||||
) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
const nodesWithSameType = nodes.filter(
|
||||
@ -912,9 +962,11 @@ export const useNodesInteractions = () => {
|
||||
})
|
||||
draft.push(newNode)
|
||||
|
||||
if (newIterationStartNode) draft.push(newIterationStartNode)
|
||||
if (newIterationStartNode)
|
||||
draft.push(newIterationStartNode)
|
||||
|
||||
if (newLoopStartNode) draft.push(newLoopStartNode)
|
||||
if (newLoopStartNode)
|
||||
draft.push(newLoopStartNode)
|
||||
})
|
||||
|
||||
if (
|
||||
@ -941,7 +993,8 @@ export const useNodesInteractions = () => {
|
||||
_connectedNodeIsSelected: false,
|
||||
}
|
||||
})
|
||||
if (newEdge) draft.push(newEdge)
|
||||
if (newEdge)
|
||||
draft.push(newEdge)
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
@ -953,8 +1006,9 @@ export const useNodesInteractions = () => {
|
||||
if (
|
||||
nodeType !== BlockEnum.IfElse
|
||||
&& nodeType !== BlockEnum.QuestionClassifier
|
||||
)
|
||||
) {
|
||||
newNode.data._connectedSourceHandleIds = [sourceHandle]
|
||||
}
|
||||
newNode.data._connectedTargetHandleIds = []
|
||||
newNode.position = {
|
||||
x: nextNode.position.x,
|
||||
@ -1078,8 +1132,10 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
})
|
||||
draft.push(newNode)
|
||||
if (newIterationStartNode) draft.push(newIterationStartNode)
|
||||
if (newLoopStartNode) draft.push(newLoopStartNode)
|
||||
if (newIterationStartNode)
|
||||
draft.push(newIterationStartNode)
|
||||
if (newLoopStartNode)
|
||||
draft.push(newLoopStartNode)
|
||||
})
|
||||
if (newEdge) {
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
@ -1169,10 +1225,10 @@ export const useNodesInteractions = () => {
|
||||
= nodes.find(node => node.id === nextNode.parentId) || null
|
||||
const isNextNodeInIteration
|
||||
= !!nextNodeParentNode
|
||||
&& nextNodeParentNode.data.type === BlockEnum.Iteration
|
||||
&& nextNodeParentNode.data.type === BlockEnum.Iteration
|
||||
const isNextNodeInLoop
|
||||
= !!nextNodeParentNode
|
||||
&& nextNodeParentNode.data.type === BlockEnum.Loop
|
||||
&& nextNodeParentNode.data.type === BlockEnum.Loop
|
||||
|
||||
if (
|
||||
nodeType !== BlockEnum.IfElse
|
||||
@ -1251,8 +1307,10 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
})
|
||||
draft.push(newNode)
|
||||
if (newIterationStartNode) draft.push(newIterationStartNode)
|
||||
if (newLoopStartNode) draft.push(newLoopStartNode)
|
||||
if (newIterationStartNode)
|
||||
draft.push(newIterationStartNode)
|
||||
if (newLoopStartNode)
|
||||
draft.push(newLoopStartNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
if (
|
||||
@ -1280,9 +1338,11 @@ export const useNodesInteractions = () => {
|
||||
_connectedNodeIsSelected: false,
|
||||
}
|
||||
})
|
||||
if (newPrevEdge) draft.push(newPrevEdge)
|
||||
if (newPrevEdge)
|
||||
draft.push(newPrevEdge)
|
||||
|
||||
if (newNextEdge) draft.push(newNextEdge)
|
||||
if (newNextEdge)
|
||||
draft.push(newNextEdge)
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
@ -1307,7 +1367,8 @@ export const useNodesInteractions = () => {
|
||||
sourceHandle: string,
|
||||
pluginDefaultValue?: PluginDefaultValue,
|
||||
) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === currentNodeId)!
|
||||
@ -1364,8 +1425,10 @@ export const useNodesInteractions = () => {
|
||||
const index = draft.findIndex(node => node.id === currentNodeId)
|
||||
|
||||
draft.splice(index, 1, newCurrentNode)
|
||||
if (newIterationStartNode) draft.push(newIterationStartNode)
|
||||
if (newLoopStartNode) draft.push(newLoopStartNode)
|
||||
if (newIterationStartNode)
|
||||
draft.push(newIterationStartNode)
|
||||
if (newLoopStartNode)
|
||||
draft.push(newLoopStartNode)
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
@ -1379,7 +1442,14 @@ export const useNodesInteractions = () => {
|
||||
return filtered
|
||||
})
|
||||
setEdges(newEdges)
|
||||
handleSyncWorkflowDraft()
|
||||
if (nodeType === BlockEnum.TriggerWebhook) {
|
||||
handleSyncWorkflowDraft(true, true, {
|
||||
onSuccess: () => autoGenerateWebhookUrl(newCurrentNode.id),
|
||||
})
|
||||
}
|
||||
else {
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodeChange, {
|
||||
nodeId: currentNodeId,
|
||||
@ -1391,6 +1461,7 @@ export const useNodesInteractions = () => {
|
||||
handleSyncWorkflowDraft,
|
||||
saveStateToHistory,
|
||||
nodesMetaDataMap,
|
||||
autoGenerateWebhookUrl,
|
||||
],
|
||||
)
|
||||
|
||||
@ -1409,14 +1480,16 @@ export const useNodesInteractions = () => {
|
||||
if (
|
||||
node.type === CUSTOM_NOTE_NODE
|
||||
|| node.type === CUSTOM_ITERATION_START_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === CUSTOM_NOTE_NODE
|
||||
|| node.type === CUSTOM_LOOP_START_NODE
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
@ -1435,7 +1508,8 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodesCopy = useCallback(
|
||||
(nodeId?: string) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { setClipboardElements } = workflowStore.getState()
|
||||
|
||||
@ -1453,15 +1527,19 @@ export const useNodesInteractions = () => {
|
||||
&& node.data.type !== BlockEnum.KnowledgeBase
|
||||
&& node.data.type !== BlockEnum.DataSourceEmpty,
|
||||
)
|
||||
if (nodeToCopy) setClipboardElements([nodeToCopy])
|
||||
if (nodeToCopy)
|
||||
setClipboardElements([nodeToCopy])
|
||||
}
|
||||
else {
|
||||
// If no nodeId is provided, fall back to the current behavior
|
||||
const bundledNodes = nodes.filter((node) => {
|
||||
if (!node.data._isBundled) return false
|
||||
if (node.type === CUSTOM_NOTE_NODE) return true
|
||||
if (!node.data._isBundled)
|
||||
return false
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
return true
|
||||
const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum]
|
||||
if (metaData.isSingleton) return false
|
||||
if (metaData.isSingleton)
|
||||
return false
|
||||
return !node.data.isInIteration && !node.data.isInLoop
|
||||
})
|
||||
|
||||
@ -1471,20 +1549,24 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
const selectedNode = nodes.find((node) => {
|
||||
if (!node.data.selected) return false
|
||||
if (node.type === CUSTOM_NOTE_NODE) return true
|
||||
if (!node.data.selected)
|
||||
return false
|
||||
if (node.type === CUSTOM_NOTE_NODE)
|
||||
return true
|
||||
const { metaData } = nodesMetaDataMap![node.data.type as BlockEnum]
|
||||
return !metaData.isSingleton
|
||||
})
|
||||
|
||||
if (selectedNode) setClipboardElements([selectedNode])
|
||||
if (selectedNode)
|
||||
setClipboardElements([selectedNode])
|
||||
}
|
||||
},
|
||||
[getNodesReadOnly, collaborativeWorkflow, workflowStore],
|
||||
)
|
||||
|
||||
const handleNodesPaste = useCallback(() => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { clipboardElements, mousePosition } = workflowStore.getState()
|
||||
|
||||
@ -1503,6 +1585,7 @@ export const useNodesInteractions = () => {
|
||||
const offsetX = currentPosition.x - x
|
||||
const offsetY = currentPosition.y - y
|
||||
let idMapping: Record<string, string> = {}
|
||||
const parentChildrenToAppend: { parentId: string, childId: string, childType: BlockEnum }[] = []
|
||||
clipboardElements.forEach((nodeToPaste, index) => {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
|
||||
@ -1516,6 +1599,7 @@ export const useNodesInteractions = () => {
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
_dimmed: false,
|
||||
title: genNewNodeTitleFromOld(nodeToPaste.data.title),
|
||||
},
|
||||
position: {
|
||||
@ -1583,34 +1667,32 @@ export const useNodesInteractions = () => {
|
||||
return
|
||||
|
||||
// handle paste to nested block
|
||||
if (selectedNode.data.type === BlockEnum.Iteration) {
|
||||
newNode.data.isInIteration = true
|
||||
newNode.data.iteration_id = selectedNode.data.iteration_id
|
||||
newNode.parentId = selectedNode.id
|
||||
newNode.positionAbsolute = {
|
||||
x: newNode.position.x,
|
||||
y: newNode.position.y,
|
||||
}
|
||||
// set position base on parent node
|
||||
newNode.position = getNestedNodePosition(newNode, selectedNode)
|
||||
}
|
||||
else if (selectedNode.data.type === BlockEnum.Loop) {
|
||||
newNode.data.isInLoop = true
|
||||
newNode.data.loop_id = selectedNode.data.loop_id
|
||||
if (selectedNode.data.type === BlockEnum.Iteration || selectedNode.data.type === BlockEnum.Loop) {
|
||||
const isIteration = selectedNode.data.type === BlockEnum.Iteration
|
||||
|
||||
newNode.data.isInIteration = isIteration
|
||||
newNode.data.iteration_id = isIteration ? selectedNode.id : undefined
|
||||
newNode.data.isInLoop = !isIteration
|
||||
newNode.data.loop_id = !isIteration ? selectedNode.id : undefined
|
||||
|
||||
newNode.parentId = selectedNode.id
|
||||
newNode.zIndex = isIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX
|
||||
newNode.positionAbsolute = {
|
||||
x: newNode.position.x,
|
||||
y: newNode.position.y,
|
||||
}
|
||||
// set position base on parent node
|
||||
newNode.position = getNestedNodePosition(newNode, selectedNode)
|
||||
// update parent children array like native add
|
||||
parentChildrenToAppend.push({ parentId: selectedNode.id, childId: newNode.id, childType: newNode.data.type })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nodesToPaste.push(newNode)
|
||||
|
||||
if (newChildren.length) nodesToPaste.push(...newChildren)
|
||||
if (newChildren.length)
|
||||
nodesToPaste.push(...newChildren)
|
||||
})
|
||||
|
||||
// only handle edge when paste nested block
|
||||
@ -1633,7 +1715,17 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
})
|
||||
|
||||
setNodes([...nodes, ...nodesToPaste])
|
||||
const newNodes = produce(nodes, (draft: Node[]) => {
|
||||
parentChildrenToAppend.forEach(({ parentId, childId, childType }) => {
|
||||
const p = draft.find(n => n.id === parentId)
|
||||
if (p) {
|
||||
p.data._children?.push({ nodeId: childId, nodeType: childType })
|
||||
}
|
||||
})
|
||||
draft.push(...nodesToPaste)
|
||||
})
|
||||
|
||||
setNodes(newNodes)
|
||||
setEdges([...edges, ...edgesToPaste])
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodePaste, {
|
||||
nodeId: nodesToPaste?.[0]?.id,
|
||||
@ -1654,7 +1746,8 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodesDuplicate = useCallback(
|
||||
(nodeId?: string) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
handleNodesCopy(nodeId)
|
||||
handleNodesPaste()
|
||||
@ -1663,7 +1756,8 @@ export const useNodesInteractions = () => {
|
||||
)
|
||||
|
||||
const handleNodesDelete = useCallback(() => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { nodes, edges } = collaborativeWorkflow.getState()
|
||||
|
||||
@ -1678,18 +1772,21 @@ export const useNodesInteractions = () => {
|
||||
}
|
||||
|
||||
const edgeSelected = edges.some(edge => edge.selected)
|
||||
if (edgeSelected) return
|
||||
if (edgeSelected)
|
||||
return
|
||||
|
||||
const selectedNode = nodes.find(
|
||||
node => node.data.selected,
|
||||
)
|
||||
|
||||
if (selectedNode) handleNodeDelete(selectedNode.id)
|
||||
if (selectedNode)
|
||||
handleNodeDelete(selectedNode.id)
|
||||
}, [collaborativeWorkflow, getNodesReadOnly, handleNodeDelete])
|
||||
|
||||
const handleNodeResize = useCallback(
|
||||
(nodeId: string, params: ResizeParamsWithDirection) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { nodes, setNodes } = collaborativeWorkflow.getState()
|
||||
const { x, y, width, height } = params
|
||||
@ -1713,8 +1810,9 @@ export const useNodesInteractions = () => {
|
||||
if (
|
||||
n.position.y + n.height!
|
||||
> bottomNode.position.y + bottomNode.height!
|
||||
)
|
||||
) {
|
||||
bottomNode = n
|
||||
}
|
||||
}
|
||||
else {
|
||||
bottomNode = n
|
||||
@ -1733,8 +1831,9 @@ export const useNodesInteractions = () => {
|
||||
if (
|
||||
height
|
||||
< bottomNode.position.y + bottomNode.height! + paddingMap.bottom
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((n) => {
|
||||
@ -1757,7 +1856,8 @@ export const useNodesInteractions = () => {
|
||||
|
||||
const handleNodeDisconnect = useCallback(
|
||||
(nodeId: string) => {
|
||||
if (getNodesReadOnly()) return
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)!
|
||||
@ -1794,7 +1894,8 @@ export const useNodesInteractions = () => {
|
||||
)
|
||||
|
||||
const handleHistoryBack = useCallback(() => {
|
||||
if (getNodesReadOnly() || getWorkflowReadOnly()) return
|
||||
if (getNodesReadOnly() || getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
// Use collaborative undo from Loro
|
||||
const undoResult = collaborationManager.undo()
|
||||
@ -1807,13 +1908,24 @@ export const useNodesInteractions = () => {
|
||||
else {
|
||||
console.log('Nothing to undo')
|
||||
}
|
||||
const { edges, nodes } = workflowHistoryStore.getState()
|
||||
if (edges.length === 0 && nodes.length === 0)
|
||||
return
|
||||
const { setNodes, setEdges } = collaborativeWorkflow.getState()
|
||||
|
||||
setEdges(edges)
|
||||
setNodes(nodes)
|
||||
}, [
|
||||
collaborativeWorkflow,
|
||||
undo,
|
||||
workflowHistoryStore,
|
||||
getNodesReadOnly,
|
||||
getWorkflowReadOnly,
|
||||
])
|
||||
|
||||
const handleHistoryForward = useCallback(() => {
|
||||
if (getNodesReadOnly() || getWorkflowReadOnly()) return
|
||||
if (getNodesReadOnly() || getWorkflowReadOnly())
|
||||
return
|
||||
|
||||
// Use collaborative redo from Loro
|
||||
const redoResult = collaborationManager.redo()
|
||||
@ -1826,6 +1938,13 @@ export const useNodesInteractions = () => {
|
||||
else {
|
||||
console.log('Nothing to redo')
|
||||
}
|
||||
const { edges, nodes } = workflowHistoryStore.getState()
|
||||
if (edges.length === 0 && nodes.length === 0)
|
||||
return
|
||||
const { setNodes, setEdges } = collaborativeWorkflow.getState()
|
||||
|
||||
setEdges(edges)
|
||||
setNodes(nodes)
|
||||
}, [
|
||||
getNodesReadOnly,
|
||||
getWorkflowReadOnly,
|
||||
@ -1834,11 +1953,13 @@ export const useNodesInteractions = () => {
|
||||
const [isDimming, setIsDimming] = useState(false)
|
||||
/** Add opacity-30 to all nodes except the nodeId */
|
||||
const dimOtherNodes = useCallback(() => {
|
||||
if (isDimming) return
|
||||
if (isDimming)
|
||||
return
|
||||
const { nodes, setNodes, edges, setEdges } = collaborativeWorkflow.getState()
|
||||
|
||||
const selectedNode = nodes.find(n => n.data.selected)
|
||||
if (!selectedNode) return
|
||||
if (!selectedNode)
|
||||
return
|
||||
|
||||
setIsDimming(true)
|
||||
|
||||
@ -1849,8 +1970,10 @@ export const useNodesInteractions = () => {
|
||||
const dependencyNodes: Node[] = []
|
||||
usedVars.forEach((valueSelector) => {
|
||||
const node = workflowNodes.find(node => node.id === valueSelector?.[0])
|
||||
if (node)
|
||||
if (!dependencyNodes.includes(node)) dependencyNodes.push(node)
|
||||
if (node) {
|
||||
if (!dependencyNodes.includes(node))
|
||||
dependencyNodes.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges)
|
||||
@ -1859,7 +1982,8 @@ export const useNodesInteractions = () => {
|
||||
const outgoersForNode = getOutgoers(node, nodes as Node[], edges)
|
||||
outgoersForNode.forEach((item) => {
|
||||
const existed = outgoers.some(v => v.id === item.id)
|
||||
if (!existed) outgoers.push(item)
|
||||
if (!existed)
|
||||
outgoers.push(item)
|
||||
})
|
||||
}
|
||||
|
||||
@ -1869,7 +1993,8 @@ export const useNodesInteractions = () => {
|
||||
const used = usedVars.some(v => v?.[0] === selectedNode.id)
|
||||
if (used) {
|
||||
const existed = dependentNodes.some(v => v.id === node.id)
|
||||
if (!existed) dependentNodes.push(node)
|
||||
if (!existed)
|
||||
dependentNodes.push(node)
|
||||
}
|
||||
})
|
||||
|
||||
@ -1878,7 +2003,8 @@ export const useNodesInteractions = () => {
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((n) => {
|
||||
const dimNode = dimNodes.find(v => v.id === n.id)
|
||||
if (!dimNode) n.data._dimmed = true
|
||||
if (!dimNode)
|
||||
n.data._dimmed = true
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
import { useCallback } from 'react'
|
||||
import ELK from 'elkjs/lib/elk.bundled.js'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
} from '../types'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import ELK from 'elkjs/lib/elk.bundled.js'
|
||||
import { cloneDeep } from 'es-toolkit/object'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import { AUTO_LAYOUT_OFFSET } from '../constants'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
|
||||
const layoutOptions = {
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { AvailableNodesMetaData } from '@/app/components/workflow/hooks-store'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { useMemo } from 'react'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import { canFindTool } from '@/utils'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import {
|
||||
useAllBuiltInTools,
|
||||
useAllCustomTools,
|
||||
useAllWorkflowTools,
|
||||
} from '@/service/use-tools'
|
||||
import { canFindTool } from '@/utils'
|
||||
|
||||
export const useNodesMetaData = () => {
|
||||
const availableNodesMetaData = useHooksStore(s => s.availableNodesMetaData)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
import { useStore } from '../store'
|
||||
import { useNodesReadOnly } from './use-workflow'
|
||||
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||
|
||||
export type SyncCallback = {
|
||||
onSuccess?: () => void
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user