pull/21998/merge
Minamiyama 7 months ago committed by GitHub
commit fd8a862b98
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -134,7 +134,8 @@ const CustomEdge = ({
style={{
stroke,
strokeWidth: 2,
opacity: data._waitingRun ? 0.7 : 1,
opacity: data._dimmed ? 0.3 : (data._waitingRun ? 0.7 : 1),
strokeDasharray: data._isTemp ? '8 8' : undefined,
}}
/>
<EdgeLabelRenderer>

@ -1,5 +1,5 @@
import type { MouseEvent } from 'react'
import { useCallback, useRef } from 'react'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import produce from 'immer'
import type {
@ -61,6 +61,7 @@ import {
} from './use-workflow'
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
import useInspectVarsCrud from './use-inspect-vars-crud'
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
export const useNodesInteractions = () => {
const { t } = useTranslation()
@ -1530,6 +1531,139 @@ export const useNodesInteractions = () => {
setNodes(nodes)
}, [redo, store, workflowHistoryStore, getNodesReadOnly, getWorkflowReadOnly])
const [isDimming, setIsDimming] = useState(false)
/** 让除 nodeId 以外的节点都加上 opacity-30 */
const dimOtherNodes = useCallback(() => {
if (isDimming)
return
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
const selectedNode = nodes.find(n => n.data.selected)
if (!selectedNode)
return
setIsDimming(true)
// const workflowNodes = useStore(s => s.getNodes())
const workflowNodes = nodes
const usedVars = getNodeUsedVars(selectedNode)
const dependencyNodes: Node[] = []
usedVars.forEach((valueSelector) => {
const node = workflowNodes.find(node => node.id === valueSelector?.[0])
if (node) {
if (!dependencyNodes.includes(node))
dependencyNodes.push(node)
}
})
const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges)
for (let currIdx = 0; currIdx < outgoers.length; currIdx++) {
const node = outgoers[currIdx]
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)
})
}
const dependentNodes: Node[] = []
outgoers.forEach((node) => {
const usedVars = getNodeUsedVars(node)
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)
}
})
const dimNodes = [...dependencyNodes, ...dependentNodes, selectedNode]
const newNodes = produce(nodes, (draft) => {
console.log('==========selecteds: ')
draft.forEach((n) => {
const dimNode = dimNodes.find(v => v.id === n.id)
if (!dimNode)
n.data._dimmed = true
})
console.log('====================end of selecteds')
})
setNodes(newNodes)
/* == ② 生成临时连线 == */
const tempEdges: Edge[] = []
dependencyNodes.forEach((n) => {
tempEdges.push({
id: `tmp_${n.id}-source-${selectedNode.id}-target`,
type: CUSTOM_EDGE, // 复用自定义 Edge也可以写一个 DIM_EDGE
source: n.id, // 依赖 → 选中
sourceHandle: 'source_tmp',
target: selectedNode.id,
targetHandle: 'target_tmp',
animated: true,
data: {
sourceType: n.data.type,
targetType: selectedNode.data.type,
_isTemp: true,
_connectedNodeIsHovering: true,
},
})
})
dependentNodes.forEach((n) => {
tempEdges.push({
id: `tmp_${selectedNode.id}-source-${n.id}-target`,
type: CUSTOM_EDGE, // 复用自定义 Edge也可以写一个 DIM_EDGE
source: selectedNode.id, // 依赖 → 选中
sourceHandle: 'source_tmp',
target: n.id,
targetHandle: 'target_tmp',
animated: true,
data: {
sourceType: selectedNode.data.type,
targetType: n.data.type,
_isTemp: true,
_connectedNodeIsHovering: true,
},
})
})
const newEdges = produce(edges, (draft) => {
draft.forEach((e) => {
e.data._dimmed = true
})
draft.push(...tempEdges)
})
setEdges(newEdges)
}, [isDimming, store])
/** 把所有节点恢复为不透明 */
const undimAllNodes = useCallback(() => {
const { getNodes, setNodes, edges, setEdges } = store.getState()
const nodes = getNodes()
setIsDimming(false)
const newNodes = produce(nodes, (draft) => {
draft.forEach((n) => {
n.data._dimmed = false
// handleNodeLeave(null as unknown as MouseEvent, n)
})
})
setNodes(newNodes)
const newEdges = produce(edges.filter(e => !e.data._isTemp), (draft) => {
draft.forEach((e) => {
e.data._dimmed = false
})
})
setEdges(newEdges)
}, [store])
return {
handleNodeDragStart,
handleNodeDrag,
@ -1554,5 +1688,7 @@ export const useNodesInteractions = () => {
handleNodeDisconnect,
handleHistoryBack,
handleHistoryForward,
dimOtherNodes,
undimAllNodes,
}
}

@ -25,6 +25,8 @@ export const useShortcuts = (): void => {
handleNodesDelete,
handleHistoryBack,
handleHistoryForward,
dimOtherNodes,
undimAllNodes,
} = useNodesInteractions()
const { handleStartWorkflowRun } = useWorkflowStartRun()
const { shortcutsEnabled: workflowHistoryShortcutsEnabled } = useWorkflowHistoryStore()
@ -211,4 +213,40 @@ export const useShortcuts = (): void => {
exactMatch: true,
useCapture: true,
})
// Shift ↓(按下)
useKeyPress(
'shift',
(e) => {
console.log('Shift down', e)
if (shouldHandleShortcut(e)) {
// handleSelectedNodeHighlight()
dimOtherNodes()
}
},
{
exactMatch: true,
useCapture: true,
events: ['keydown'],
},
)
// Shift ↑
useKeyPress(
(e) => {
return e.key === 'Shift'
},
(e) => {
if (shouldHandleShortcut(e)) {
console.log('Shift up 2: ', e)
// handleSelectedNodeUnhighlight()
undimAllNodes()
}
},
{
exactMatch: true,
useCapture: true,
events: ['keyup'],
},
)
}

@ -76,7 +76,7 @@ const VarList: FC<Props> = ({
})
onChange(newList)
}
}, [list, onVarNameChange, onChange])
}, [list, onVarNameChange, onChange, t])
const handleVarReferenceChange = useCallback((index: number) => {
return (value: ValueSelector | string, varKindType: VarKindType, varInfo?: Var) => {

@ -59,6 +59,8 @@ import { useLogs } from '@/app/components/workflow/run/hooks'
import PanelWrap from '../before-run-form/panel-wrap'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import Relations from './relations'
import { RelationType } from './relations/types'
import {
AuthorizedInNode,
PluginAuth,
@ -484,6 +486,30 @@ const BasePanel: FC<BasePanelProps> = ({
{...passedLogParams}
/>
)}
{tabType === TabType.relations && (
<>
<div className='border-t-[0.5px] border-divider-regular p-4'>
<div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'>
{t('workflow.debug.relations.dependencies').toLocaleUpperCase()}
</div>
<div className='system-xs-regular mb-2 text-text-tertiary'>
{t('workflow.debug.relations.dependenciesDescription')}
</div>
<Relations selectedNode={{ id, data } as Node} relationType={RelationType.dependencies}/>
</div>
<Split />
<div className='border-t-[0.5px] border-divider-regular p-4'>
<div className='system-sm-semibold-uppercase mb-1 flex items-center text-text-secondary'>
{t('workflow.debug.relations.dependents').toLocaleUpperCase()}
</div>
<div className='system-xs-regular mb-2 text-text-tertiary'>
{t('workflow.debug.relations.dependentsDescription')}
</div>
<Relations selectedNode={{ id, data } as Node} relationType={RelationType.dependents}/>
</div>
</>
)}
</div>
</div>
)

@ -0,0 +1,35 @@
import Empty from './empty'
import Item from './item'
import type { Node } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
import { RelationType } from './types'
import { useTranslation } from 'react-i18next'
type ContainerProps = {
nextNode?: Node
relationType: RelationType
}
const Container = ({
nextNode,
relationType,
}: ContainerProps) => {
const { t } = useTranslation()
return (
<div className={cn(
'space-y-0.5 rounded-[10px] bg-background-section-burn p-0.5',
)}>
{nextNode && (
<Item
key={nextNode.id}
nodeId={nextNode.id}
data={nextNode.data}
/>
)}
{!nextNode && (
<Empty display={relationType === RelationType.dependencies ? t('workflow.debug.relations.noDependencies') : t('workflow.debug.relations.noDependents')} />
)}
</div>
)
}
export default Container

@ -0,0 +1,27 @@
import { memo } from 'react'
type EmptyProps = {
display: string
}
const Empty = ({
display = '',
}: EmptyProps) => {
return (
<>
<div
className={`
bg-dropzone-bg hover:bg-dropzone-bg-hover relative flex h-9 cursor-default items-center rounded-lg border border-dashed
border-divider-regular !bg-components-dropzone-bg-alt px-2 text-xs
text-text-placeholder
`}
>
<div className='flex items-center uppercase'>
{display}
</div>
</div>
</>
)
}
export default memo(Empty)

@ -0,0 +1,122 @@
import { memo } from 'react'
import { isEqual } from 'lodash-es'
import {
getOutgoers,
useStore,
} from 'reactflow'
import { useToolIcon } from '@/app/components/workflow/hooks'
import BlockIcon from '@/app/components/workflow/block-icon'
import type {
Node,
} from '@/app/components/workflow/types'
import Line from './line'
import Container from './container'
import { getNodeUsedVars } from '../../variable/utils'
import { RelationType } from './types'
type RelationsProps = {
selectedNode: Node
relationType: RelationType
}
const Relations = ({
selectedNode,
relationType,
}: RelationsProps) => {
const data = selectedNode.data
const toolIcon = useToolIcon(data)
const edges = useStore(s => s.edges.map(edge => ({
id: edge.id,
source: edge.source,
sourceHandle: edge.sourceHandle,
target: edge.target,
targetHandle: edge.targetHandle,
})), isEqual)
const nodes = useStore(s => s.getNodes().map(node => ({
id: node.id,
data: node.data,
})), isEqual)
const workflowNodes = useStore(s => s.getNodes())
const list: Node[] = []
if (relationType === RelationType.dependencies) {
const usedVars = getNodeUsedVars(selectedNode)
const dependencyNodes: Node[] = []
usedVars.forEach((valueSelector) => {
const node = workflowNodes.find(node => node.id === valueSelector?.[0])
if (node) {
if (!dependencyNodes.includes(node))
dependencyNodes.push(node)
}
})
list.push(...dependencyNodes)
}
else {
const outgoers = getOutgoers(selectedNode as Node, nodes as Node[], edges)
for (let currIdx = 0; currIdx < outgoers.length; currIdx++) {
const node = outgoers[currIdx]
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)
})
}
const dependentNodes: Node[] = []
outgoers.forEach((node) => {
const usedVars = getNodeUsedVars(node)
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)
}
})
list.push(...dependentNodes)
}
const getThisNode = () => {
return (
<div className='relative flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border-[0.5px] border-divider-regular bg-background-default shadow-xs'>
<BlockIcon
type={selectedNode!.data.type}
toolIcon={toolIcon}
/>
</div>
)
}
const getLinkedNodes = () => {
return (
<div className='grow space-y-2'>
{list.length > 0 ? (
list.map((item, index) => {
return (
<Container
key={index}
nextNode={item}
relationType={relationType}
/>
)
})
) : (
<Container key={0} relationType={relationType}/>
)}
</div>
)
}
return (
<div className='flex py-1'>
{getThisNode()}
<Line
rowCount={Math.max(1, list.length)}
relationType={relationType}
/>
{getLinkedNodes()}
</div>
)
}
export default memo(Relations)

@ -0,0 +1,59 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import type {
CommonNodeType,
} from '@/app/components/workflow/types'
import BlockIcon from '@/app/components/workflow/block-icon'
import {
useNodesInteractions,
useNodesReadOnly,
useToolIcon,
} from '@/app/components/workflow/hooks'
import Button from '@/app/components/base/button'
type ItemProps = {
nodeId: string
data: CommonNodeType
}
const Item = ({
nodeId,
data,
}: ItemProps) => {
const { t } = useTranslation()
const { nodesReadOnly } = useNodesReadOnly()
const { handleNodeSelect } = useNodesInteractions()
const toolIcon = useToolIcon(data)
return (
<div
className='group relative flex h-9 cursor-pointer items-center rounded-lg border-[0.5px] border-divider-regular bg-background-default px-2 text-xs text-text-secondary shadow-xs last-of-type:mb-0 hover:bg-background-default-hover'
>
<BlockIcon
type={data.type}
toolIcon={toolIcon}
className='mr-1.5 shrink-0'
/>
<div
className='system-xs-medium grow truncate text-text-secondary'
title={data.title}
>
{data.title}
</div>
{
!nodesReadOnly && (
<>
<Button
className='mr-1 hidden shrink-0 group-hover:flex'
size='small'
onClick={() => handleNodeSelect(nodeId)}
>
{t('workflow.common.jumpToNode')}
</Button>
</>
)
}
</div>
)
}
export default memo(Item)

@ -0,0 +1,92 @@
import { memo } from 'react'
import { RelationType } from './types'
type LineProps = {
rowCount: number
relationType: RelationType
}
const Line = ({
rowCount = 1,
relationType,
}: LineProps) => {
const list: number[] = []
const listH = 40
const ySpacing = 8
const svgHeight = listH * rowCount + ySpacing * (rowCount - 1)
const lineWidth = 48
const arrowStrokeWidth = 1
const arrowLineLength = 6
return (
<svg className='w-12 shrink-0' style={{ height: svgHeight }}>
{
Array.from({ length: rowCount }).map((_, index) => {
const space = index * listH + index * ySpacing + 16
return (
<g key={index}>
{
index === 0 && (
<>
<path
d={`M0,18 L${lineWidth},18`}
strokeWidth={1}
fill='none'
className='stroke-divider-solid'
/>
<path
d={relationType === RelationType.dependencies
? `M${6 + arrowLineLength},${18 - arrowLineLength} L6,${18} L${6 + arrowLineLength},${18 + arrowLineLength}`
: `M${lineWidth - 6 - arrowLineLength},${18 - arrowLineLength} L${lineWidth - 6},${18} L${lineWidth - 6 - arrowLineLength},${18 + arrowLineLength}`
}
strokeWidth={arrowStrokeWidth}
fill='none'
className='stroke-divider-solid-alt'
/>
<rect
x={lineWidth - 1}
y={16}
width={1}
height={4}
className='fill-divider-solid-alt'
/>
</>
)
}
{
index > 0 && (
<>
<path
d={(`M0,18 L${lineWidth / 2 - 12},18 Q${lineWidth / 2},18 ${lineWidth / 2},28 L${lineWidth / 2},${space - 10 + 2} Q${lineWidth / 2},${space + 2} ${lineWidth / 2 + 12},${space + 2} L${lineWidth},${space + 2}`)}
strokeWidth={1}
fill='none'
className='stroke-divider-solid'
/>
{relationType === RelationType.dependents && (
<path
d={`M${lineWidth - 6 - arrowLineLength},${space + 2 - arrowLineLength} L${lineWidth - 6},${space + 2} L${lineWidth - 6 - arrowLineLength},${space + 2 + arrowLineLength}`}
strokeWidth={arrowStrokeWidth}
fill='none'
className='stroke-divider-solid-alt'
/>
)}
</>
)
}
<rect
x={lineWidth - 1}
y={space}
width={1}
height={4}
className='fill-divider-solid-alt'
/>
</g>
)
})
}
</svg>
)
}
export default memo(Line)

@ -0,0 +1,4 @@
export enum RelationType {
dependencies,
dependents,
}

@ -7,6 +7,7 @@ import { useTranslation } from 'react-i18next'
export enum TabType {
settings = 'settings',
lastRun = 'lastRun',
relations = 'relations',
}
type Props = {
@ -24,6 +25,7 @@ const Tab: FC<Props> = ({
items={[
{ id: TabType.settings, name: t('workflow.debug.settingsTab').toLocaleUpperCase() },
{ id: TabType.lastRun, name: t('workflow.debug.lastRunTab').toLocaleUpperCase() },
{ id: TabType.relations, name: t('workflow.debug.relationsTab').toLocaleUpperCase() },
]}
itemClassName='ml-0'
value={value}

@ -143,6 +143,7 @@ const BaseNode: FC<BaseNodeProps> = ({
showSelectedBorder ? 'border-components-option-card-option-selected-border' : 'border-transparent',
!showSelectedBorder && data._inParallelHovering && 'border-workflow-block-border-highlight',
data._waitingRun && 'opacity-70',
data._dimmed && 'opacity-30',
)}
ref={nodeRef}
style={{

@ -94,6 +94,7 @@ export type CommonNodeType<T = {}> = {
retry_config?: WorkflowRetryConfig
default_value?: DefaultValueForm[]
credential_id?: string
_dimmed?: boolean
} & T & Partial<Pick<ToolDefaultValue, 'provider_id' | 'provider_type' | 'provider_name' | 'tool_name'>>
export type CommonEdgeType = {
@ -109,7 +110,8 @@ export type CommonEdgeType = {
isInLoop?: boolean
loop_id?: string
sourceType: BlockEnum
targetType: BlockEnum
targetType: BlockEnum,
_isTemp?: boolean,
}
export type Node<T = {}> = ReactFlowNode<CommonNodeType<T>>

@ -941,6 +941,7 @@ const translation = {
debug: {
settingsTab: 'Settings',
lastRunTab: 'Last Run',
relationsTab: 'Relations',
noData: {
description: 'The results of the last run will be displayed here',
runThisNode: 'Run this node',
@ -966,6 +967,14 @@ const translation = {
chatNode: 'Conversation',
systemNode: 'System',
},
relations: {
dependencies: 'Dependencies',
dependents: 'Dependents',
dependenciesDescription: 'Nodes that this node relies on',
dependentsDescription: 'Nodes that rely on this node',
noDependencies: 'No dependencies',
noDependents: 'No dependents',
},
},
}

@ -969,6 +969,15 @@ const translation = {
},
settingsTab: '設定',
lastRunTab: '最後の実行',
relationsTab: '関係',
relations: {
dependencies: '依存元',
dependents: '依存先',
dependenciesDescription: 'このノードが依存している他のノード',
dependentsDescription: 'このノードに依存している他のノード',
noDependencies: '依存元なし',
noDependents: '依存先なし',
},
},
}

@ -942,6 +942,7 @@ const translation = {
debug: {
settingsTab: '设置',
lastRunTab: '上次运行',
relationsTab: '关系',
noData: {
description: '上次运行的结果将显示在这里',
runThisNode: '运行此节点',
@ -967,6 +968,14 @@ const translation = {
chatNode: '会话变量',
systemNode: '系统变量',
},
relations: {
dependencies: '依赖',
dependents: '被依赖',
dependenciesDescription: '本节点依赖的其他节点',
dependentsDescription: '依赖于本节点的其他节点',
noDependencies: '无依赖',
noDependents: '无被依赖',
},
},
}

@ -943,6 +943,9 @@ const translation = {
defaultName: '未命名版本',
},
debug: {
settingsTab: '設定',
lastRunTab: '最後一次運行',
relationsTab: '關係',
noData: {
runThisNode: '運行此節點',
description: '上次運行的結果將顯示在這裡',
@ -968,8 +971,14 @@ const translation = {
emptyTip: '在畫布上逐步執行節點或逐步運行節點後,您可以在變數檢視中查看節點變數的當前值。',
resetConversationVar: '將對話變數重置為默認值',
},
settingsTab: '設定',
lastRunTab: '最後一次運行',
relations: {
dependencies: '依賴',
dependents: '被依賴',
dependenciesDescription: '此節點所依賴的其他節點',
dependentsDescription: '依賴此節點的其他節點',
noDependencies: '無依賴',
noDependents: '無被依賴',
},
},
}

Loading…
Cancel
Save