mirror of
https://github.com/langgenius/dify.git
synced 2026-06-13 12:12:36 +08:00
239 lines
8.4 KiB
TypeScript
239 lines
8.4 KiB
TypeScript
import type { BlockEnum, CommonNodeType } from '../types'
|
|
import type { TriggerDefaultValue } from './types'
|
|
import { cn } from '@langgenius/dify-ui/cn'
|
|
import {
|
|
createPreviewCardHandle,
|
|
PreviewCard,
|
|
PreviewCardContent,
|
|
PreviewCardTrigger,
|
|
} from '@langgenius/dify-ui/preview-card'
|
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
|
import {
|
|
memo,
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
} from 'react'
|
|
import { useTranslation } from 'react-i18next'
|
|
import useNodes from '@/app/components/workflow/store/workflow/use-nodes'
|
|
import BlockIcon from '../block-icon'
|
|
import { BlockEnum as BlockEnumValues } from '../types'
|
|
import { BlockSelectorRow } from './block-selector-row'
|
|
// import { useNodeMetaData } from '../hooks'
|
|
import { START_BLOCKS } from './constants'
|
|
|
|
type StartBlocksProps = {
|
|
searchText: string
|
|
onSelect: (type: BlockEnum, triggerDefaultValue?: TriggerDefaultValue) => void
|
|
availableBlocksTypes?: BlockEnum[]
|
|
onContentStateChange?: (hasContent: boolean) => void
|
|
hideUserInput?: boolean
|
|
showMostCommonBadge?: boolean
|
|
showUserInputAdded?: boolean
|
|
showUserInputDisabled?: boolean
|
|
disabled?: boolean
|
|
}
|
|
type StartBlockPreviewPayload = {
|
|
block: typeof START_BLOCKS[number]
|
|
}
|
|
|
|
const StartBlocks = ({
|
|
searchText,
|
|
onSelect,
|
|
availableBlocksTypes = [],
|
|
onContentStateChange,
|
|
hideUserInput = false, // Allow parent to explicitly hide Start node option (e.g. when one already exists).
|
|
showMostCommonBadge = false,
|
|
showUserInputAdded = false,
|
|
showUserInputDisabled = false,
|
|
disabled = false,
|
|
}: StartBlocksProps) => {
|
|
const { t } = useTranslation()
|
|
const nodes = useNodes()
|
|
const previewCardHandle = useMemo(() => createPreviewCardHandle<StartBlockPreviewPayload>(), [])
|
|
// const nodeMetaData = useNodeMetaData()
|
|
|
|
const filteredBlocks = useMemo(() => {
|
|
// Check if Start node already exists in workflow
|
|
const hasStartNode = nodes.some(node => (node.data as CommonNodeType)?.type === BlockEnumValues.Start)
|
|
const normalizedSearch = searchText.toLowerCase()
|
|
const getDisplayName = (blockType: BlockEnum) => {
|
|
if (blockType === BlockEnumValues.TriggerWebhook)
|
|
return t('customWebhook', { ns: 'workflow' })
|
|
|
|
return t(`blocks.${blockType}`, { ns: 'workflow' })
|
|
}
|
|
|
|
return START_BLOCKS.filter((block) => {
|
|
// Hide User Input (Start) if it already exists in workflow or if hideUserInput is true.
|
|
// In read-only conflict modes, keep it visible so the row can show Added or disabled tooltip state.
|
|
if (block.type === BlockEnumValues.Start && (hasStartNode || hideUserInput) && !showUserInputAdded && !showUserInputDisabled)
|
|
return false
|
|
|
|
// Filter by search text
|
|
const displayName = getDisplayName(block.type).toLowerCase()
|
|
if (!displayName.includes(normalizedSearch) && !block.title.toLowerCase().includes(normalizedSearch))
|
|
return false
|
|
|
|
// availableBlocksTypes now contains properly filtered entry node types from parent
|
|
return availableBlocksTypes.includes(block.type)
|
|
})
|
|
}, [searchText, availableBlocksTypes, nodes, t, hideUserInput, showUserInputAdded, showUserInputDisabled])
|
|
|
|
const isEmpty = filteredBlocks.length === 0
|
|
|
|
useEffect(() => {
|
|
onContentStateChange?.(!isEmpty)
|
|
}, [isEmpty, onContentStateChange])
|
|
|
|
// Preview is supplementary: the block icon, title and description all become
|
|
// reachable from the inspector + canvas once the row is clicked to insert
|
|
// the start node, so hover/focus-only activation is a11y-safe. See
|
|
// packages/dify-ui/AGENTS.md → Overlay Primitive Selection.
|
|
const renderBlock = useCallback((block: typeof START_BLOCKS[number]) => {
|
|
const isUserInput = block.type === BlockEnumValues.Start
|
|
const isUserInputDisabled = isUserInput && showUserInputDisabled
|
|
const isRowDisabled = disabled || (isUserInput && showUserInputAdded) || isUserInputDisabled
|
|
const label = t(`blocks.${block.type}`, { ns: 'workflow' })
|
|
const disabledReason = t('nodes.startPlaceholder.userInputConflictTip', { ns: 'workflow' })
|
|
const row = (
|
|
<BlockSelectorRow
|
|
aria-disabled={isRowDisabled}
|
|
aria-label={isUserInputDisabled ? `${label}. ${disabledReason}` : label}
|
|
disabled={isRowDisabled}
|
|
onClick={() => {
|
|
if (isRowDisabled)
|
|
return
|
|
onSelect(block.type)
|
|
}}
|
|
>
|
|
<div className={cn('flex min-w-0 flex-1 items-center', isUserInputDisabled && 'opacity-30')}>
|
|
<BlockIcon
|
|
className="mr-2 shrink-0"
|
|
type={block.type}
|
|
size="sm"
|
|
/>
|
|
<div className="flex w-0 grow items-center justify-between text-sm text-text-secondary">
|
|
<span className="truncate system-sm-medium">{label}</span>
|
|
{isUserInput && showUserInputAdded && (
|
|
<span className="ml-2 shrink-0 system-xs-regular text-text-tertiary">
|
|
{t('operation.added', { ns: 'common' })}
|
|
</span>
|
|
)}
|
|
{isUserInput && showMostCommonBadge && !showUserInputAdded && (
|
|
<span className="ml-2 shrink-0 rounded-[5px] border border-divider-deep px-1 py-0.5 system-2xs-medium-uppercase text-text-tertiary">
|
|
{t('blocks.mostCommon', { ns: 'workflow' })}
|
|
</span>
|
|
)}
|
|
{isUserInput && !showMostCommonBadge && !showUserInputAdded && !showUserInputDisabled && (
|
|
<span className="ml-2 shrink-0 system-xs-regular text-text-quaternary">{t('blocks.originalStartNode', { ns: 'workflow' })}</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</BlockSelectorRow>
|
|
)
|
|
|
|
if (isUserInputDisabled) {
|
|
return (
|
|
<Tooltip key={block.type}>
|
|
<TooltipTrigger render={row} />
|
|
<TooltipContent placement="right" sideOffset={8} className="max-w-[240px] rounded-xl border-[0.5px] border-components-panel-border bg-components-tooltip-bg px-4 py-3.5 shadow-lg">
|
|
<p className="system-xs-regular text-text-secondary">
|
|
{disabledReason}
|
|
</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<PreviewCardTrigger
|
|
key={block.type}
|
|
delay={150}
|
|
closeDelay={150}
|
|
handle={previewCardHandle}
|
|
payload={{ block }}
|
|
render={row}
|
|
/>
|
|
)
|
|
}, [disabled, onSelect, previewCardHandle, showMostCommonBadge, showUserInputAdded, showUserInputDisabled, t])
|
|
|
|
if (isEmpty)
|
|
return null
|
|
|
|
return (
|
|
<div className="p-1">
|
|
<div>
|
|
{filteredBlocks.map((block, index) => (
|
|
<div key={block.type}>
|
|
{renderBlock(block)}
|
|
{block.type === BlockEnumValues.Start && !showMostCommonBadge && index < filteredBlocks.length - 1 && (
|
|
<div className="my-1 px-3">
|
|
<div className="border-t border-divider-subtle" />
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
<PreviewCard handle={previewCardHandle}>
|
|
{({ payload }) => (
|
|
<StartBlockPreviewCard
|
|
payload={payload as StartBlockPreviewPayload | undefined}
|
|
t={t}
|
|
/>
|
|
)}
|
|
</PreviewCard>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
type StartBlockPreviewCardProps = {
|
|
payload?: StartBlockPreviewPayload
|
|
t: ReturnType<typeof useTranslation>['t']
|
|
}
|
|
|
|
function StartBlockPreviewCard({
|
|
payload,
|
|
t,
|
|
}: StartBlockPreviewCardProps) {
|
|
if (!payload)
|
|
return null
|
|
|
|
const { block } = payload
|
|
const description = block.type === BlockEnumValues.Start
|
|
? t('nodes.start.userInputTipDescription', { ns: 'workflow' })
|
|
: t(`blocksAbout.${block.type}`, { ns: 'workflow' })
|
|
const showDifyTeamAuthor = [
|
|
BlockEnumValues.Start,
|
|
BlockEnumValues.TriggerWebhook,
|
|
BlockEnumValues.TriggerSchedule,
|
|
].includes(block.type)
|
|
|
|
return (
|
|
<PreviewCardContent placement="right" popupClassName="w-[224px] px-3 pt-3 pb-2.5">
|
|
<div>
|
|
<BlockIcon
|
|
size="md"
|
|
className="mb-2"
|
|
type={block.type}
|
|
/>
|
|
<div className="mb-1 system-md-medium text-text-primary">
|
|
{t(`blocks.${block.type}`, { ns: 'workflow' })}
|
|
</div>
|
|
<div className="system-xs-regular wrap-break-word text-text-secondary">
|
|
{description}
|
|
</div>
|
|
{showDifyTeamAuthor && (
|
|
<div className="mt-1 system-xs-regular text-text-tertiary">
|
|
{t('author', { ns: 'tools' })}
|
|
{' '}
|
|
{t('difyTeam', { ns: 'workflow' })}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</PreviewCardContent>
|
|
)
|
|
}
|
|
|
|
export default memo(StartBlocks)
|