mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
init
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext } from 'use-context-selector'
|
||||
import type { Edge } from 'reactflow'
|
||||
import type { Node } from './types'
|
||||
|
||||
export type WorkflowContextValue = {
|
||||
nodes: Node[]
|
||||
edges: Edge[]
|
||||
selectedNodeId?: string
|
||||
handleSelectedNodeIdChange: (nodeId: string) => void
|
||||
selectedNode?: Node
|
||||
}
|
||||
|
||||
export const WorkflowContext = createContext<WorkflowContextValue>({
|
||||
nodes: [],
|
||||
edges: [],
|
||||
handleSelectedNodeIdChange: () => {},
|
||||
})
|
||||
export const useWorkflowContext = () => useContext(WorkflowContext)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { memo } from 'react'
|
||||
import type { EdgeProps } from 'reactflow'
|
||||
import {
|
||||
BaseEdge,
|
||||
getBezierPath,
|
||||
EdgeLabelRenderer,
|
||||
getSmoothStepPath,
|
||||
} from 'reactflow'
|
||||
|
||||
const CustomEdge = ({
|
||||
@@ -11,19 +13,39 @@ const CustomEdge = ({
|
||||
targetX,
|
||||
targetY,
|
||||
}: EdgeProps) => {
|
||||
const [edgePath] = getBezierPath({
|
||||
const [
|
||||
edgePath,
|
||||
labelX,
|
||||
labelY,
|
||||
] = getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
borderRadius: 30,
|
||||
})
|
||||
console.log('edgePath', edgePath)
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge id={id} path={edgePath} />
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-2 h-6 bg-white rounded-lg shadow-xs
|
||||
text-[10px] font-semibold text-gray-700
|
||||
nodrag nopan
|
||||
`}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
>
|
||||
Topic 2
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomEdge
|
||||
export default memo(CustomEdge)
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type { Node } from './types'
|
||||
|
||||
export const useWorkflow = () => {
|
||||
export const useWorkflow = (nodes: Node[]) => {
|
||||
const [selectedNodeId, setSelectedNodeId] = useState('')
|
||||
|
||||
const handleSelectedNodeId = useCallback((nodeId: string) => setSelectedNodeId(nodeId), [])
|
||||
const handleSelectedNodeIdChange = useCallback((nodeId: string) => setSelectedNodeId(nodeId), [])
|
||||
|
||||
const selectedNode = useMemo(() => {
|
||||
return nodes.find(node => node.id === selectedNodeId)
|
||||
}, [nodes, selectedNodeId])
|
||||
|
||||
return {
|
||||
selectedNodeId,
|
||||
handleSelectedNodeId,
|
||||
selectedNode,
|
||||
handleSelectedNodeIdChange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import type { FC } from 'react'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ReactFlowProvider,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import {
|
||||
WorkflowContext,
|
||||
useWorkflowContext,
|
||||
} from './context'
|
||||
import { useWorkflow } from './hooks'
|
||||
import Header from './header'
|
||||
import CustomNode from './nodes'
|
||||
import CustomNode, {
|
||||
Panel,
|
||||
} from './nodes'
|
||||
import CustomEdge from './custom-edge'
|
||||
|
||||
const nodeTypes = {
|
||||
@@ -13,47 +24,69 @@ const edgeTypes = {
|
||||
custom: CustomEdge,
|
||||
}
|
||||
|
||||
const initialNodes = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'custom',
|
||||
position: { x: 330, y: 30 },
|
||||
data: { type: 'start' },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'custom',
|
||||
position: { x: 330, y: 212 },
|
||||
data: { type: 'start' },
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'custom',
|
||||
position: { x: 150, y: 394 },
|
||||
data: { type: 'start' },
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'custom',
|
||||
position: { x: 510, y: 394 },
|
||||
data: { type: 'start' },
|
||||
},
|
||||
]
|
||||
|
||||
const initialEdges = [
|
||||
{
|
||||
id: '1',
|
||||
source: '1',
|
||||
target: '2',
|
||||
type: 'custom',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
source: '2',
|
||||
target: '3',
|
||||
type: 'custom',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
source: '2',
|
||||
target: '4',
|
||||
type: 'custom',
|
||||
},
|
||||
]
|
||||
|
||||
const Workflow = () => {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
} = useWorkflowContext()
|
||||
|
||||
return (
|
||||
<div className='relative w-full h-full'>
|
||||
<Header />
|
||||
<Panel />
|
||||
<ReactFlow
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
nodes={[
|
||||
{
|
||||
id: 'start',
|
||||
type: 'custom',
|
||||
position: { x: 330, y: 30 },
|
||||
data: { list: [] },
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'custom',
|
||||
position: { x: 400, y: 250 },
|
||||
data: { list: [] },
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'custom',
|
||||
position: { x: 100, y: 250 },
|
||||
data: { list: [] },
|
||||
},
|
||||
]}
|
||||
edges={[
|
||||
{
|
||||
id: 'e1-2',
|
||||
source: 'start',
|
||||
target: '1',
|
||||
type: 'custom',
|
||||
},
|
||||
{
|
||||
id: 'e1-3',
|
||||
source: 'start',
|
||||
target: '2',
|
||||
type: 'custom',
|
||||
},
|
||||
]}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
>
|
||||
<Background
|
||||
gap={[14, 14]}
|
||||
@@ -64,4 +97,34 @@ const Workflow = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Workflow
|
||||
const WorkflowWrap: FC = () => {
|
||||
const [nodes] = useNodesState(initialNodes)
|
||||
const [edges] = useEdgesState(initialEdges)
|
||||
const {
|
||||
selectedNodeId,
|
||||
handleSelectedNodeIdChange,
|
||||
selectedNode,
|
||||
} = useWorkflow(nodes)
|
||||
|
||||
return (
|
||||
<WorkflowContext.Provider value={{
|
||||
selectedNodeId,
|
||||
handleSelectedNodeIdChange,
|
||||
selectedNode,
|
||||
nodes,
|
||||
edges,
|
||||
}}>
|
||||
<Workflow />
|
||||
</WorkflowContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const WorkflowWrapWithReactFlowProvider = () => {
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<WorkflowWrap />
|
||||
</ReactFlowProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowWrapWithReactFlowProvider
|
||||
|
||||
@@ -3,6 +3,8 @@ import type {
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useNodeId } from 'reactflow'
|
||||
import { useWorkflowContext } from '../../context'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
type BaseNodeProps = {
|
||||
@@ -12,12 +14,20 @@ type BaseNodeProps = {
|
||||
const BaseNode: FC<BaseNodeProps> = ({
|
||||
children,
|
||||
}) => {
|
||||
const nodeId = useNodeId()
|
||||
const {
|
||||
selectedNodeId,
|
||||
handleSelectedNodeIdChange,
|
||||
} = useWorkflowContext()
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
group relative pb-2 w-[296px] bg-[#fcfdff] border border-white rounded-2xl shadow-xs
|
||||
group relative pb-2 w-[296px] bg-[#fcfdff] rounded-2xl shadow-xs
|
||||
hover:shadow-lg
|
||||
${selectedNodeId === nodeId ? 'border-[2px] border-primary-600' : 'border border-white'}
|
||||
`}
|
||||
onClick={() => handleSelectedNodeIdChange(nodeId || '')}
|
||||
>
|
||||
<div className='flex items-center px-3 pt-3 pb-2'>
|
||||
<div className='mr-2'></div>
|
||||
|
||||
@@ -2,6 +2,14 @@ import type {
|
||||
FC,
|
||||
ReactNode,
|
||||
} from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useWorkflowContext } from '../../context'
|
||||
import { XClose } from '@/app/components/base/icons/src/vender/line/general'
|
||||
|
||||
enum TabEnum {
|
||||
Inputs = 'inputs',
|
||||
Outputs = 'outputs',
|
||||
}
|
||||
|
||||
type BasePanelProps = {
|
||||
defaultElement?: ReactNode
|
||||
@@ -14,26 +22,63 @@ const BasePanel: FC<BasePanelProps> = ({
|
||||
inputsElement,
|
||||
ouputsElement,
|
||||
}) => {
|
||||
const initialActiveTab = inputsElement ? TabEnum.Inputs : ouputsElement ? TabEnum.Outputs : ''
|
||||
const [activeTab, setActiveTab] = useState(initialActiveTab)
|
||||
const { handleSelectedNodeIdChange } = useWorkflowContext()
|
||||
|
||||
return (
|
||||
<div className='absolute top-2 right-2 bottom-2 w-[420px] shadow-lg border-[0.5px] border-gray-200 rounded-2xl'>
|
||||
<div className='absolute top-2 right-2 bottom-2 w-[420px] bg-white shadow-lg border-[0.5px] border-gray-200 rounded-2xl z-20'>
|
||||
<div className='flex items-center px-4 pt-3'>
|
||||
<div className='mr-2 w-6 h-6'></div>
|
||||
<div className='py-1 text-base text-gray-900 font-semibold '>LLM</div>
|
||||
<div className='shrink-0 mr-2 w-6 h-6'></div>
|
||||
<div className='grow py-1 text-base text-gray-900 font-semibold '>LLM</div>
|
||||
<div className='shrink-0 flex items-center'>
|
||||
<div
|
||||
className='w-6 h-6 cursor-pointer'
|
||||
onClick={() => handleSelectedNodeIdChange('')}
|
||||
>
|
||||
<XClose className='w-4 h-4 text-gray-500' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='p-2'>
|
||||
<div className='py-[5px] pl-1.5 pr-2 text-xs text-gray-400'>
|
||||
Add description...
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex items-center px-4 h-[42px]'>
|
||||
inputs
|
||||
</div>
|
||||
{
|
||||
(inputsElement || ouputsElement) && (
|
||||
<div className='flex items-center px-4 h-[42px]'>
|
||||
{
|
||||
inputsElement && (
|
||||
<div
|
||||
className='cursor-pointer'
|
||||
onClick={() => setActiveTab(TabEnum.Inputs)}
|
||||
>
|
||||
inputs
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
ouputsElement && (
|
||||
<div
|
||||
className='ml-4 cursor-pointer'
|
||||
onClick={() => setActiveTab(TabEnum.Outputs)}
|
||||
>
|
||||
outpus
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className='py-2 border-t-[0.5px] border-b-[0.5px] border-black/5'>
|
||||
{defaultElement}
|
||||
{inputsElement}
|
||||
{ouputsElement}
|
||||
{activeTab === TabEnum.Inputs && inputsElement}
|
||||
{activeTab === TabEnum.Outputs && ouputsElement}
|
||||
</div>
|
||||
<div className='p-4'>
|
||||
next step
|
||||
</div>
|
||||
<div className='p-4'></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
11
web/app/components/workflow/nodes/constants.ts
Normal file
11
web/app/components/workflow/nodes/constants.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ComponentType } from 'react'
|
||||
import StartNode from './start/node'
|
||||
import StartPanel from './start/panel'
|
||||
|
||||
export const NodeMap: Record<string, ComponentType> = {
|
||||
start: StartNode,
|
||||
}
|
||||
|
||||
export const PanelMap: Record<string, ComponentType> = {
|
||||
start: StartPanel,
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
Handle,
|
||||
Position,
|
||||
useNodeId,
|
||||
} from 'reactflow'
|
||||
import StartNode from './start/node'
|
||||
|
||||
const NodeMap = {
|
||||
'start-node': StartNode,
|
||||
}
|
||||
import { useWorkflowContext } from '../context'
|
||||
import {
|
||||
NodeMap,
|
||||
PanelMap,
|
||||
} from './constants'
|
||||
|
||||
const CustomNode = () => {
|
||||
const nodeId = useNodeId()
|
||||
const { nodes } = useWorkflowContext()
|
||||
const currentNode = nodes.find(node => node.id === nodeId)
|
||||
const NodeComponent = NodeMap[currentNode!.data.type as string]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle
|
||||
@@ -16,7 +23,7 @@ const CustomNode = () => {
|
||||
position={Position.Top}
|
||||
className='!-top-0.5 !w-2 !h-0.5 !bg-primary-500 !rounded-none !border-none !min-h-[2px]'
|
||||
/>
|
||||
<StartNode />
|
||||
<NodeComponent />
|
||||
<Handle
|
||||
type='source'
|
||||
position={Position.Bottom}
|
||||
@@ -26,4 +33,16 @@ const CustomNode = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomNode
|
||||
export const Panel = () => {
|
||||
const { selectedNode } = useWorkflowContext()
|
||||
const PanelComponent = PanelMap[selectedNode?.data.type || '']
|
||||
|
||||
if (!PanelComponent)
|
||||
return null
|
||||
|
||||
return (
|
||||
<PanelComponent />
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CustomNode)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { FC } from 'react'
|
||||
import BaseNode from '../_base/node'
|
||||
|
||||
const Node = () => {
|
||||
const Node: FC = () => {
|
||||
return (
|
||||
<BaseNode>
|
||||
start node
|
||||
<div>start node</div>
|
||||
</BaseNode>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { FC } from 'react'
|
||||
import BasePanel from '../_base/panel'
|
||||
|
||||
const Panel = () => {
|
||||
const Panel: FC = () => {
|
||||
return (
|
||||
<BasePanel
|
||||
inputsElement={<div>start panel</div>}
|
||||
inputsElement={<div>start panel inputs</div>}
|
||||
ouputsElement={<div>start panel outputs</div>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Node as ReactFlowNode } from 'reactflow'
|
||||
|
||||
export type NodeData = {
|
||||
type: string
|
||||
name?: string
|
||||
icon?: any
|
||||
description?: string
|
||||
}
|
||||
export type Node = ReactFlowNode<NodeData>
|
||||
|
||||
Reference in New Issue
Block a user