mirror of
https://github.com/langgenius/dify.git
synced 2026-07-25 05:28:35 +08:00
332 lines
10 KiB
TypeScript
332 lines
10 KiB
TypeScript
import type { Dispatch, FC, ReactNode, SetStateAction } from 'react'
|
|
import type { BlockEnum, NodeDefault, OnSelectBlock, ToolWithProvider } from '../types'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
|
import { useSuspenseQuery } from '@tanstack/react-query'
|
|
import { memo, useEffect, useMemo } from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { useDocLink } from '@/context/i18n'
|
|
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
|
import { useFeaturedToolsRecommendations } from '@/service/use-plugins'
|
|
import {
|
|
useAllBuiltInTools,
|
|
useAllCustomTools,
|
|
useAllMCPTools,
|
|
useAllWorkflowTools,
|
|
useInvalidateAllBuiltInTools,
|
|
} from '@/service/use-tools'
|
|
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 { TabsEnum } from './types'
|
|
|
|
type TabsProps = {
|
|
activeTab: TabsEnum
|
|
onActiveTabChange: (activeTab: TabsEnum) => void
|
|
searchText: string
|
|
tags: string[]
|
|
onTagsChange: Dispatch<SetStateAction<string[]>>
|
|
onSelect: OnSelectBlock
|
|
availableBlocksTypes?: BlockEnum[]
|
|
blocks: NodeDefault[]
|
|
dataSources?: ToolWithProvider[]
|
|
tabs: Array<{
|
|
key: TabsEnum
|
|
name: string
|
|
disabled?: boolean
|
|
disabledTip?: ReactNode
|
|
disabledTipLinkKey?: 'startNodesDocs'
|
|
}>
|
|
filterElem: React.ReactNode
|
|
noBlocks?: boolean
|
|
noTools?: boolean
|
|
forceShowStartContent?: boolean // Force show Start content even when noBlocks=true
|
|
allowStartNodeSelection?: boolean // Allow user input option even when trigger node already exists (e.g. change-node flow or when no Start node yet).
|
|
hasUserInputNode?: boolean
|
|
hasTriggerNode?: boolean
|
|
snippetsElem?: React.ReactNode
|
|
}
|
|
|
|
const normalizeToolList = (list: ToolWithProvider[] | undefined, currentBasePath?: string) => {
|
|
if (!list || !currentBasePath) return list
|
|
|
|
let changed = false
|
|
const normalized = list.map((provider) => {
|
|
if (typeof provider.icon !== 'string') return provider
|
|
|
|
const shouldPrefix =
|
|
provider.icon.startsWith('/') && !provider.icon.startsWith(`${currentBasePath}/`)
|
|
|
|
if (!shouldPrefix) return provider
|
|
|
|
changed = true
|
|
return {
|
|
...provider,
|
|
icon: `${currentBasePath}${provider.icon}`,
|
|
}
|
|
})
|
|
|
|
return changed ? normalized : list
|
|
}
|
|
|
|
const getStoreToolUpdates = ({
|
|
state,
|
|
buildInTools,
|
|
customTools,
|
|
workflowTools,
|
|
mcpTools,
|
|
}: {
|
|
state: {
|
|
buildInTools?: ToolWithProvider[]
|
|
customTools?: ToolWithProvider[]
|
|
workflowTools?: ToolWithProvider[]
|
|
mcpTools?: ToolWithProvider[]
|
|
}
|
|
buildInTools?: ToolWithProvider[]
|
|
customTools?: ToolWithProvider[]
|
|
workflowTools?: ToolWithProvider[]
|
|
mcpTools?: ToolWithProvider[]
|
|
}) => {
|
|
const updates: Partial<typeof state> = {}
|
|
|
|
if (buildInTools !== undefined && state.buildInTools !== buildInTools)
|
|
updates.buildInTools = buildInTools
|
|
if (customTools !== undefined && state.customTools !== customTools)
|
|
updates.customTools = customTools
|
|
if (workflowTools !== undefined && state.workflowTools !== workflowTools)
|
|
updates.workflowTools = workflowTools
|
|
if (mcpTools !== undefined && state.mcpTools !== mcpTools) updates.mcpTools = mcpTools
|
|
|
|
return updates
|
|
}
|
|
|
|
const TabHeaderItem = ({
|
|
tab,
|
|
activeTab,
|
|
onActiveTabChange,
|
|
disabledTip,
|
|
disabledTipLinkHref,
|
|
disabledTipLinkLabel,
|
|
}: {
|
|
tab: TabsProps['tabs'][number]
|
|
activeTab: TabsEnum
|
|
onActiveTabChange: (activeTab: TabsEnum) => void
|
|
disabledTip: ReactNode
|
|
disabledTipLinkHref?: string
|
|
disabledTipLinkLabel?: string
|
|
}) => {
|
|
const className = cn(
|
|
'relative mr-0.5 flex h-8 items-center rounded-t-lg px-3 system-sm-medium',
|
|
tab.disabled
|
|
? 'cursor-not-allowed text-text-disabled opacity-60'
|
|
: activeTab === tab.key
|
|
? 'sm-no-bottom cursor-default bg-components-panel-bg text-text-accent'
|
|
: 'cursor-pointer text-text-tertiary',
|
|
)
|
|
|
|
const handleClick = () => {
|
|
if (tab.disabled || activeTab === tab.key) return
|
|
onActiveTabChange(tab.key)
|
|
}
|
|
|
|
if (tab.disabled) {
|
|
return (
|
|
<Tooltip key={tab.key}>
|
|
<TooltipTrigger
|
|
render={
|
|
<button
|
|
type="button"
|
|
className={className}
|
|
aria-disabled={tab.disabled}
|
|
onClick={handleClick}
|
|
>
|
|
{tab.name}
|
|
</button>
|
|
}
|
|
/>
|
|
<TooltipContent placement="top" className="max-w-[230px] rounded-xl px-4 py-3.5">
|
|
<div className="flex flex-col items-start gap-1 system-xs-regular text-text-secondary">
|
|
<p>{disabledTip}</p>
|
|
{disabledTipLinkHref && disabledTipLinkLabel && (
|
|
<a
|
|
className="text-text-accent hover:underline"
|
|
href={disabledTipLinkHref}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
{disabledTipLinkLabel}
|
|
</a>
|
|
)}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div key={tab.key} className={className} aria-disabled={tab.disabled} onClick={handleClick}>
|
|
{tab.name}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const Tabs: FC<TabsProps> = ({
|
|
activeTab,
|
|
onActiveTabChange,
|
|
tags,
|
|
onTagsChange,
|
|
searchText,
|
|
onSelect,
|
|
availableBlocksTypes,
|
|
blocks,
|
|
dataSources = [],
|
|
tabs = [],
|
|
filterElem,
|
|
noBlocks,
|
|
noTools,
|
|
forceShowStartContent = false,
|
|
allowStartNodeSelection = false,
|
|
hasUserInputNode = false,
|
|
hasTriggerNode = false,
|
|
snippetsElem,
|
|
}) => {
|
|
const { t } = useTranslation()
|
|
const docLink = useDocLink()
|
|
const { data: buildInTools } = useAllBuiltInTools()
|
|
const { data: customTools } = useAllCustomTools()
|
|
const { data: workflowTools } = useAllWorkflowTools()
|
|
const { data: mcpTools } = useAllMCPTools()
|
|
const invalidateBuiltInTools = useInvalidateAllBuiltInTools()
|
|
const { data: enable_marketplace } = useSuspenseQuery({
|
|
...systemFeaturesQueryOptions(),
|
|
select: (s) => s.enable_marketplace,
|
|
})
|
|
const workflowStore = useWorkflowStore()
|
|
const inRAGPipeline = dataSources.length > 0
|
|
const { plugins: featuredPlugins = [], isLoading: isFeaturedLoading } =
|
|
useFeaturedToolsRecommendations(enable_marketplace && !inRAGPipeline)
|
|
const normalizedBuiltInTools = useMemo(
|
|
() => normalizeToolList(buildInTools, basePath),
|
|
[buildInTools],
|
|
)
|
|
const normalizedCustomTools = useMemo(
|
|
() => normalizeToolList(customTools, basePath),
|
|
[customTools],
|
|
)
|
|
const normalizedWorkflowTools = useMemo(
|
|
() => normalizeToolList(workflowTools, basePath),
|
|
[workflowTools],
|
|
)
|
|
const normalizedMcpTools = useMemo(() => normalizeToolList(mcpTools, basePath), [mcpTools])
|
|
const disabledTip = t(($) => $['tabs.startDisabledTip'], { ns: 'workflow' })
|
|
|
|
useEffect(() => {
|
|
workflowStore.setState((state) => {
|
|
const updates = getStoreToolUpdates({
|
|
state,
|
|
buildInTools: normalizedBuiltInTools,
|
|
customTools: normalizedCustomTools,
|
|
workflowTools: normalizedWorkflowTools,
|
|
mcpTools: normalizedMcpTools,
|
|
})
|
|
if (!Object.keys(updates).length) return state
|
|
return {
|
|
...state,
|
|
...updates,
|
|
}
|
|
})
|
|
}, [
|
|
normalizedBuiltInTools,
|
|
normalizedCustomTools,
|
|
normalizedMcpTools,
|
|
normalizedWorkflowTools,
|
|
workflowStore,
|
|
])
|
|
|
|
return (
|
|
<div className="w-full min-w-0" onClick={(e) => e.stopPropagation()}>
|
|
{!noBlocks && (
|
|
<div className="relative flex w-full min-w-0 bg-background-section-burn pt-1 pl-1">
|
|
{tabs.map((tab) => (
|
|
<TabHeaderItem
|
|
key={tab.key}
|
|
tab={tab}
|
|
activeTab={activeTab}
|
|
onActiveTabChange={onActiveTabChange}
|
|
disabledTip={tab.disabledTip || disabledTip}
|
|
disabledTipLinkHref={
|
|
tab.disabledTipLinkKey === 'startNodesDocs'
|
|
? docLink('/use-dify/nodes/trigger/overview')
|
|
: undefined
|
|
}
|
|
disabledTipLinkLabel={
|
|
tab.disabledTipLinkKey === 'startNodesDocs'
|
|
? t(($) => $['tabs.startDisabledTipLearnMore'], { ns: 'workflow' })
|
|
: undefined
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
{filterElem}
|
|
{activeTab === TabsEnum.Start && (!noBlocks || forceShowStartContent) && (
|
|
<div className="border-t border-divider-subtle">
|
|
<AllStartBlocks
|
|
allowUserInputSelection={allowStartNodeSelection}
|
|
hasUserInputNode={hasUserInputNode}
|
|
hasTriggerNode={hasTriggerNode}
|
|
searchText={searchText}
|
|
onSelect={onSelect}
|
|
availableBlocksTypes={availableBlocksTypes}
|
|
tags={tags}
|
|
/>
|
|
</div>
|
|
)}
|
|
{activeTab === TabsEnum.Blocks && !noBlocks && (
|
|
<div className="border-t border-divider-subtle">
|
|
<Blocks
|
|
searchText={searchText}
|
|
onSelect={onSelect}
|
|
availableBlocksTypes={availableBlocksTypes}
|
|
blocks={blocks}
|
|
/>
|
|
</div>
|
|
)}
|
|
{activeTab === TabsEnum.Sources && !!dataSources.length && (
|
|
<div className="border-t border-divider-subtle">
|
|
<DataSources searchText={searchText} onSelect={onSelect} dataSources={dataSources} />
|
|
</div>
|
|
)}
|
|
{activeTab === TabsEnum.Tools && !noTools && (
|
|
<AllTools
|
|
searchText={searchText}
|
|
onSelect={onSelect}
|
|
tags={tags}
|
|
canNotSelectMultiple
|
|
buildInTools={normalizedBuiltInTools || []}
|
|
customTools={normalizedCustomTools || []}
|
|
workflowTools={normalizedWorkflowTools || []}
|
|
mcpTools={normalizedMcpTools || []}
|
|
onTagsChange={onTagsChange}
|
|
isInRAGPipeline={inRAGPipeline}
|
|
featuredPlugins={featuredPlugins}
|
|
featuredLoading={isFeaturedLoading}
|
|
showFeatured={enable_marketplace && !inRAGPipeline}
|
|
onFeaturedInstallSuccess={async () => {
|
|
invalidateBuiltInTools()
|
|
}}
|
|
/>
|
|
)}
|
|
{activeTab === TabsEnum.Snippets && Boolean(snippetsElem) ? (
|
|
<div className="border-t border-divider-subtle">{snippetsElem}</div>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default memo(Tabs)
|