Commit a17c0e5b authored by StyleZhang's avatar StyleZhang

init

parent 20d5fdea
'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)
import { memo } from 'react'
import type { EdgeProps } from 'reactflow' import type { EdgeProps } from 'reactflow'
import { import {
BaseEdge, BaseEdge,
getBezierPath, EdgeLabelRenderer,
getSmoothStepPath,
} from 'reactflow' } from 'reactflow'
const CustomEdge = ({ const CustomEdge = ({
...@@ -11,19 +13,39 @@ const CustomEdge = ({ ...@@ -11,19 +13,39 @@ const CustomEdge = ({
targetX, targetX,
targetY, targetY,
}: EdgeProps) => { }: EdgeProps) => {
const [edgePath] = getBezierPath({ const [
edgePath,
labelX,
labelY,
] = getSmoothStepPath({
sourceX, sourceX,
sourceY, sourceY,
targetX, targetX,
targetY, targetY,
borderRadius: 30,
}) })
console.log('edgePath', edgePath)
return ( return (
<> <>
<BaseEdge id={id} path={edgePath} /> <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)
import { import {
useCallback, useCallback,
useMemo,
useState, useState,
} from 'react' } from 'react'
import type { Node } from './types'
export const useWorkflow = () => { export const useWorkflow = (nodes: Node[]) => {
const [selectedNodeId, setSelectedNodeId] = useState('') 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 { return {
selectedNodeId, selectedNodeId,
handleSelectedNodeId, selectedNode,
handleSelectedNodeIdChange,
} }
} }
import type { FC } from 'react'
import ReactFlow, { import ReactFlow, {
Background, Background,
ReactFlowProvider,
useEdgesState,
useNodesState,
} from 'reactflow' } from 'reactflow'
import 'reactflow/dist/style.css' import 'reactflow/dist/style.css'
import {
WorkflowContext,
useWorkflowContext,
} from './context'
import { useWorkflow } from './hooks'
import Header from './header' import Header from './header'
import CustomNode from './nodes' import CustomNode, {
Panel,
} from './nodes'
import CustomEdge from './custom-edge' import CustomEdge from './custom-edge'
const nodeTypes = { const nodeTypes = {
...@@ -13,47 +24,69 @@ const edgeTypes = { ...@@ -13,47 +24,69 @@ const edgeTypes = {
custom: CustomEdge, 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 Workflow = () => {
const {
nodes,
edges,
} = useWorkflowContext()
return ( return (
<div className='relative w-full h-full'> <div className='relative w-full h-full'>
<Header /> <Header />
<Panel />
<ReactFlow <ReactFlow
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
edgeTypes={edgeTypes} edgeTypes={edgeTypes}
nodes={[ nodes={nodes}
{ edges={edges}
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',
},
]}
> >
<Background <Background
gap={[14, 14]} gap={[14, 14]}
...@@ -64,4 +97,34 @@ const Workflow = () => { ...@@ -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 { ...@@ -3,6 +3,8 @@ import type {
ReactNode, ReactNode,
} from 'react' } from 'react'
import { memo } 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' import { Plus } from '@/app/components/base/icons/src/vender/line/general'
type BaseNodeProps = { type BaseNodeProps = {
...@@ -12,12 +14,20 @@ type BaseNodeProps = { ...@@ -12,12 +14,20 @@ type BaseNodeProps = {
const BaseNode: FC<BaseNodeProps> = ({ const BaseNode: FC<BaseNodeProps> = ({
children, children,
}) => { }) => {
const nodeId = useNodeId()
const {
selectedNodeId,
handleSelectedNodeIdChange,
} = useWorkflowContext()
return ( return (
<div <div
className={` 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 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='flex items-center px-3 pt-3 pb-2'>
<div className='mr-2'></div> <div className='mr-2'></div>
......
...@@ -2,6 +2,14 @@ import type { ...@@ -2,6 +2,14 @@ import type {
FC, FC,
ReactNode, ReactNode,
} from 'react' } 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 = { type BasePanelProps = {
defaultElement?: ReactNode defaultElement?: ReactNode
...@@ -14,26 +22,63 @@ const BasePanel: FC<BasePanelProps> = ({ ...@@ -14,26 +22,63 @@ const BasePanel: FC<BasePanelProps> = ({
inputsElement, inputsElement,
ouputsElement, ouputsElement,
}) => { }) => {
const initialActiveTab = inputsElement ? TabEnum.Inputs : ouputsElement ? TabEnum.Outputs : ''
const [activeTab, setActiveTab] = useState(initialActiveTab)
const { handleSelectedNodeIdChange } = useWorkflowContext()
return ( 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='flex items-center px-4 pt-3'>
<div className='mr-2 w-6 h-6'></div> <div className='shrink-0 mr-2 w-6 h-6'></div>
<div className='py-1 text-base text-gray-900 font-semibold '>LLM</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>
<div className='p-2'> <div className='p-2'>
<div className='py-[5px] pl-1.5 pr-2 text-xs text-gray-400'> <div className='py-[5px] pl-1.5 pr-2 text-xs text-gray-400'>
Add description... Add description...
</div> </div>
</div> </div>
<div className='flex items-center px-4 h-[42px]'> {
inputs (inputsElement || ouputsElement) && (
</div> <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'> <div className='py-2 border-t-[0.5px] border-b-[0.5px] border-black/5'>
{defaultElement} {defaultElement}
{inputsElement} {activeTab === TabEnum.Inputs && inputsElement}
{ouputsElement} {activeTab === TabEnum.Outputs && ouputsElement}
</div>
<div className='p-4'>
next step
</div> </div>
<div className='p-4'></div>
</div> </div>
) )
} }
......
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,
}
import { memo } from 'react'
import { import {
Handle, Handle,
Position, Position,
useNodeId,
} from 'reactflow' } from 'reactflow'
import StartNode from './start/node' import { useWorkflowContext } from '../context'
import {
const NodeMap = { NodeMap,
'start-node': StartNode, PanelMap,
} } from './constants'
const CustomNode = () => { 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 ( return (
<> <>
<Handle <Handle
...@@ -16,7 +23,7 @@ const CustomNode = () => { ...@@ -16,7 +23,7 @@ const CustomNode = () => {
position={Position.Top} position={Position.Top}
className='!-top-0.5 !w-2 !h-0.5 !bg-primary-500 !rounded-none !border-none !min-h-[2px]' className='!-top-0.5 !w-2 !h-0.5 !bg-primary-500 !rounded-none !border-none !min-h-[2px]'
/> />
<StartNode /> <NodeComponent />
<Handle <Handle
type='source' type='source'
position={Position.Bottom} position={Position.Bottom}
...@@ -26,4 +33,16 @@ const CustomNode = () => { ...@@ -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)
import type { FC } from 'react'
import BaseNode from '../_base/node' import BaseNode from '../_base/node'
const Node = () => { const Node: FC = () => {
return ( return (
<BaseNode> <BaseNode>
start node <div>start node</div>
</BaseNode> </BaseNode>
) )
} }
......
import type { FC } from 'react'
import BasePanel from '../_base/panel' import BasePanel from '../_base/panel'
const Panel = () => { const Panel: FC = () => {
return ( return (
<BasePanel <BasePanel
inputsElement={<div>start panel</div>} inputsElement={<div>start panel inputs</div>}
ouputsElement={<div>start panel outputs</div>}
/> />
) )
} }
......
import type { Node as ReactFlowNode } from 'reactflow'
export type NodeData = {
type: string
name?: string
icon?: any
description?: string
}
export type Node = ReactFlowNode<NodeData>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment