mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(builder): UX and style updates (#7550)
- Handles: - Add `NodeHandle` to draw input and output handles - Position handles relatively - Make entire handle label clickable/connectable - Add input/output types below labels - Change color on hover and when connected - "Connected" no longer shows up when connected - Edges: - Draw edge above node when connecting to the same node - Add custom `ConnectionLine`; drawn when making a connection - Add `CustomEdge`; drawn for existing connections - Add arrow to the edge end - Colorize depending on type - Input field modal: - Select all text when opened - Disable node dragging - CSS: - Remove not needed styling - Use tailwind classes instead of css for some components - Minor style changes - Add shadcn switch - Change bottom node buttons (for properties and advanced) to switches - Format code
This commit is contained in:
committed by
GitHub
parent
ab0df04bfe
commit
902d2a8924
22
rnd/autogpt_builder/src/components/ConnectionLine.tsx
Normal file
22
rnd/autogpt_builder/src/components/ConnectionLine.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { BaseEdge, ConnectionLineComponentProps, getBezierPath, Position } from "reactflow";
|
||||
|
||||
const ConnectionLine: React.FC<ConnectionLineComponentProps> = ({ fromPosition, fromHandle, fromX, fromY, toPosition, toX, toY }) => {
|
||||
|
||||
const sourceX = fromPosition === Position.Right ?
|
||||
fromX + (fromHandle?.width! / 2 - 5) : fromX - (fromHandle?.width! / 2 - 5);
|
||||
|
||||
const [path] = getBezierPath({
|
||||
sourceX: sourceX,
|
||||
sourceY: fromY,
|
||||
sourcePosition: fromPosition,
|
||||
targetX: toX,
|
||||
targetY: toY,
|
||||
targetPosition: toPosition,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseEdge path={path} style={{ strokeWidth: 2, stroke: '#555' }} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectionLine;
|
||||
37
rnd/autogpt_builder/src/components/CustomEdge.tsx
Normal file
37
rnd/autogpt_builder/src/components/CustomEdge.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import { FC, memo, useMemo } from "react";
|
||||
import { BaseEdge, EdgeProps, getBezierPath, XYPosition } from "reactflow";
|
||||
|
||||
export type CustomEdgeData = {
|
||||
edgeColor: string
|
||||
sourcePos: XYPosition
|
||||
}
|
||||
|
||||
const CustomEdgeFC: FC<EdgeProps<CustomEdgeData>> = ({ data, selected, source, sourcePosition, sourceX, sourceY, target, targetPosition, targetX, targetY, markerEnd }) => {
|
||||
|
||||
const [path] = getBezierPath({
|
||||
sourceX: sourceX - 5,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX: targetX + 4,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
// Calculate y difference between source and source node, to adjust self-loop edge
|
||||
const yDifference = useMemo(() => sourceY - data!.sourcePos.y, [data!.sourcePos.y]);
|
||||
|
||||
// Define special edge path for self-loop
|
||||
const edgePath = source === target ?
|
||||
`M ${sourceX - 5} ${sourceY} C ${sourceX + 128} ${sourceY - yDifference - 128} ${targetX - 128} ${sourceY - yDifference - 128} ${targetX + 3}, ${targetY}` :
|
||||
path;
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
style={{ strokeWidth: 2, stroke: (data?.edgeColor ?? '#555555') + (selected ? '' : '80') }}
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const CustomEdge = memo(CustomEdgeFC);
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useState, useEffect, FC, memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from 'reactflow';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import './customnode.css';
|
||||
import ModalComponent from './ModalComponent';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { BlockSchema } from '@/lib/types';
|
||||
import SchemaTooltip from './SchemaTooltip';
|
||||
import { beautifyString } from '@/lib/utils';
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import NodeHandle from './NodeHandle';
|
||||
|
||||
type CustomNodeData = {
|
||||
blockType: string;
|
||||
@@ -43,12 +44,12 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
console.log(`Node ${id} data:`, data);
|
||||
}, [id, data]);
|
||||
|
||||
const toggleOutput = () => {
|
||||
setIsOutputOpen(!isOutputOpen);
|
||||
const toggleOutput = (checked: boolean) => {
|
||||
setIsOutputOpen(checked);
|
||||
};
|
||||
|
||||
const toggleAdvancedSettings = () => {
|
||||
setIsAdvancedOpen(!isAdvancedOpen);
|
||||
const toggleAdvancedSettings = (checked: boolean) => {
|
||||
setIsAdvancedOpen(checked);
|
||||
};
|
||||
|
||||
const hasOptionalFields = () => {
|
||||
@@ -57,33 +58,12 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const generateHandles = (schema: BlockSchema, type: 'source' | 'target') => {
|
||||
const generateOutputHandles = (schema: BlockSchema) => {
|
||||
if (!schema?.properties) return null;
|
||||
const keys = Object.keys(schema.properties);
|
||||
return keys.map((key) => (
|
||||
<div key={key} className="handle-container">
|
||||
{type === 'target' && (
|
||||
<>
|
||||
<Handle
|
||||
type={type}
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%', width: '10px', height: '10px' }}
|
||||
/>
|
||||
<span className="handle-label">{schema.properties[key].title || beautifyString(key)}</span>
|
||||
</>
|
||||
)}
|
||||
{type === 'source' && (
|
||||
<>
|
||||
<span className="handle-label">{schema.properties[key].title || beautifyString(key)}</span>
|
||||
<Handle
|
||||
type={type}
|
||||
position={Position.Right}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%', width: '10px', height: '10px' }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div key={key}>
|
||||
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} schema={schema.properties[key]} side="right" />
|
||||
</div>
|
||||
));
|
||||
};
|
||||
@@ -113,9 +93,11 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
return data.connections && data.connections.some((conn: any) => {
|
||||
if (typeof conn === 'string') {
|
||||
const [source, target] = conn.split(' -> ');
|
||||
return target.includes(key) && target.includes(data.title);
|
||||
return (target.includes(key) && target.includes(data.title)) ||
|
||||
(source.includes(key) && source.includes(data.title));
|
||||
}
|
||||
return conn.target === id && conn.targetHandle === key;
|
||||
return (conn.target === id && conn.targetHandle === key) ||
|
||||
(conn.source === id && conn.sourceHandle === key);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -159,7 +141,7 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
}
|
||||
|
||||
if (isHandleConnected(fullKey)) {
|
||||
return <div className="connected-input">Connected</div>;
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const renderClickableInput = (value: string | null = null, placeholder: string = "", secret: boolean = false) => {
|
||||
@@ -420,33 +402,24 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
|
||||
return (
|
||||
<div className={`custom-node dark-theme ${data.status === 'RUNNING' ? 'running' : data.status === 'COMPLETED' ? 'completed' : data.status === 'FAILED' ? 'failed' : ''}`}>
|
||||
<div className="node-header">
|
||||
<div className="node-title">{beautifyString(data.blockType?.replace(/Block$/, '') || data.title)}</div>
|
||||
<div className="mb-2">
|
||||
<div className="text-lg font-bold">{beautifyString(data.blockType?.replace(/Block$/, '') || data.title)}</div>
|
||||
</div>
|
||||
<div className="node-content">
|
||||
<div className="input-section">
|
||||
<div>
|
||||
{data.inputSchema &&
|
||||
Object.entries(data.inputSchema.properties).map(([key, schema]) => {
|
||||
const isRequired = data.inputSchema.required?.includes(key);
|
||||
return (isRequired || isAdvancedOpen) && (
|
||||
<div key={key}>
|
||||
<div className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={key}
|
||||
style={{ background: '#555', borderRadius: '50%', width: '10px', height: '10px' }}
|
||||
/>
|
||||
<span className="handle-label">{schema.title || beautifyString(key)}</span>
|
||||
<SchemaTooltip schema={schema} />
|
||||
</div>
|
||||
<NodeHandle keyName={key} isConnected={isHandleConnected(key)} schema={schema} side="left" />
|
||||
{renderInputField(key, schema, '', schema.title || beautifyString(key))}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="output-section">
|
||||
{data.outputSchema && generateHandles(data.outputSchema, 'source')}
|
||||
<div>
|
||||
{data.outputSchema && generateOutputHandles(data.outputSchema)}
|
||||
</div>
|
||||
</div>
|
||||
{isOutputOpen && (
|
||||
@@ -463,14 +436,14 @@ const CustomNode: FC<NodeProps<CustomNodeData>> = ({ data, id }) => {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="node-footer">
|
||||
<Button onClick={toggleOutput} className="toggle-button">
|
||||
Toggle Output
|
||||
</Button>
|
||||
<div className="flex items-center mt-2.5">
|
||||
<Switch onCheckedChange={toggleOutput} className='custom-switch' />
|
||||
<span className='m-1 mr-4'>Output</span>
|
||||
{hasOptionalFields() && (
|
||||
<Button onClick={toggleAdvancedSettings} className="toggle-button">
|
||||
Toggle Advanced
|
||||
</Button>
|
||||
<>
|
||||
<Switch onCheckedChange={toggleAdvancedSettings} className='custom-switch' />
|
||||
<span className='m-1'>Advanced</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<ModalComponent
|
||||
|
||||
@@ -11,6 +11,8 @@ import ReactFlow, {
|
||||
OnConnect,
|
||||
NodeTypes,
|
||||
Connection,
|
||||
EdgeTypes,
|
||||
MarkerType,
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
import CustomNode from './CustomNode';
|
||||
@@ -19,8 +21,10 @@ import AutoGPTServerAPI, { Block, Graph, ObjectSchema } from '@/lib/autogpt-serv
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { ChevronRight, ChevronLeft } from "lucide-react";
|
||||
import { deepEquals } from '@/lib/utils';
|
||||
import { deepEquals, getTypeColor } from '@/lib/utils';
|
||||
import { beautifyString } from '@/lib/utils';
|
||||
import { CustomEdge, CustomEdgeData } from './CustomEdge';
|
||||
import ConnectionLine from './ConnectionLine';
|
||||
|
||||
|
||||
type CustomNodeData = {
|
||||
@@ -73,7 +77,7 @@ const FlowEditor: React.FC<{
|
||||
className?: string;
|
||||
}> = ({ flowID, template, className }) => {
|
||||
const [nodes, setNodes] = useState<Node<CustomNodeData>[]>([]);
|
||||
const [edges, setEdges] = useState<Edge[]>([]);
|
||||
const [edges, setEdges] = useState<Edge<CustomEdgeData>[]>([]);
|
||||
const [nodeId, setNodeId] = useState<number>(1);
|
||||
const [availableNodes, setAvailableNodes] = useState<Block[]>([]);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
@@ -116,6 +120,7 @@ const FlowEditor: React.FC<{
|
||||
}, [flowID, template, availableNodes]);
|
||||
|
||||
const nodeTypes: NodeTypes = useMemo(() => ({ custom: CustomNode }), []);
|
||||
const edgeTypes: EdgeTypes = useMemo(() => ({ custom: CustomEdge }), []);
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
@@ -127,58 +132,81 @@ const FlowEditor: React.FC<{
|
||||
[]
|
||||
);
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
(connection: Connection) => {
|
||||
setEdges((eds) => addEdge(connection, eds));
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === connection.target) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: [
|
||||
...node.data.connections,
|
||||
{
|
||||
source: connection.source,
|
||||
sourceHandle: connection.sourceHandle,
|
||||
target: connection.target,
|
||||
targetHandle: connection.targetHandle,
|
||||
} as { source: string; sourceHandle: string; target: string; targetHandle: string },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
},
|
||||
[setEdges, setNodes]
|
||||
);
|
||||
const getOutputType = (id: string, handleId: string) => {
|
||||
const node = nodes.find((node) => node.id === id);
|
||||
if (!node) return 'unknown';
|
||||
|
||||
const outputSchema = node.data.outputSchema;
|
||||
if (!outputSchema) return 'unknown';
|
||||
|
||||
const outputType = outputSchema.properties[handleId].type;
|
||||
return outputType;
|
||||
}
|
||||
|
||||
const getNodePos = (id: string) => {
|
||||
const node = nodes.find((node) => node.id === id);
|
||||
if (!node) return 0;
|
||||
|
||||
return node.position;
|
||||
}
|
||||
|
||||
const onConnect: OnConnect = (connection: Connection) => {
|
||||
const edgeColor = getTypeColor(getOutputType(connection.source!, connection.sourceHandle!));
|
||||
const sourcePos = getNodePos(connection.source!)
|
||||
console.log('sourcePos', sourcePos);
|
||||
setEdges((eds) => addEdge({
|
||||
type: 'custom',
|
||||
markerEnd: { type: MarkerType.ArrowClosed, strokeWidth: 2, color: edgeColor },
|
||||
data: { edgeColor, sourcePos },
|
||||
...connection
|
||||
}, eds));
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === connection.target || node.id === connection.source) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: [
|
||||
...node.data.connections,
|
||||
{
|
||||
source: connection.source,
|
||||
sourceHandle: connection.sourceHandle,
|
||||
target: connection.target,
|
||||
targetHandle: connection.targetHandle,
|
||||
} as { source: string; sourceHandle: string; target: string; targetHandle: string },
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const onEdgesDelete = useCallback(
|
||||
(edgesToDelete: Edge[]) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: node.data.connections.filter(
|
||||
(conn: any) =>
|
||||
!edgesToDelete.some(
|
||||
(edge) =>
|
||||
edge.source === conn.source &&
|
||||
edge.target === conn.target &&
|
||||
edge.sourceHandle === conn.sourceHandle &&
|
||||
edge.targetHandle === conn.targetHandle
|
||||
)
|
||||
),
|
||||
},
|
||||
}))
|
||||
);
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
(edgesToDelete: Edge<CustomEdgeData>[]) => {
|
||||
setNodes((nds) =>
|
||||
nds.map((node) => ({
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
connections: node.data.connections.filter(
|
||||
(conn: any) =>
|
||||
!edgesToDelete.some(
|
||||
(edge) =>
|
||||
edge.source === conn.source &&
|
||||
edge.target === conn.target &&
|
||||
edge.sourceHandle === conn.sourceHandle &&
|
||||
edge.targetHandle === conn.targetHandle
|
||||
)
|
||||
),
|
||||
},
|
||||
}))
|
||||
);
|
||||
},
|
||||
[setNodes]
|
||||
);
|
||||
|
||||
const addNode = (blockId: string, nodeType: string) => {
|
||||
const nodeSchema = availableNodes.find(node => node.id === blockId);
|
||||
@@ -247,14 +275,20 @@ const FlowEditor: React.FC<{
|
||||
|
||||
setEdges(graph.links.map(link => ({
|
||||
id: `${link.source_id}_${link.source_name}_${link.sink_id}_${link.sink_name}`,
|
||||
type: 'custom',
|
||||
data: {
|
||||
edgeColor: getTypeColor(getOutputType(link.source_id, link.source_name!)),
|
||||
sourcePos: getNodePos(link.source_id)
|
||||
},
|
||||
markerEnd: { type: MarkerType.ArrowClosed, strokeWidth: 2, color: getTypeColor(getOutputType(link.source_id, link.source_name!)) },
|
||||
source: link.source_id,
|
||||
target: link.sink_id,
|
||||
sourceHandle: link.source_name || undefined,
|
||||
targetHandle: link.sink_name || undefined
|
||||
})));
|
||||
}) as Edge<CustomEdgeData>));
|
||||
}
|
||||
|
||||
const prepareNodeInputData = (node: Node<CustomNodeData>, allNodes: Node<CustomNodeData>[], allEdges: Edge[]) => {
|
||||
const prepareNodeInputData = (node: Node<CustomNodeData>, allNodes: Node<CustomNodeData>[], allEdges: Edge<CustomEdgeData>[]) => {
|
||||
console.log("Preparing input data for node:", node.id, node.data.blockType);
|
||||
|
||||
const blockSchema = availableNodes.find(n => n.id === node.data.block_id)?.inputSchema;
|
||||
@@ -376,13 +410,13 @@ const FlowEditor: React.FC<{
|
||||
|
||||
return frontendNode
|
||||
? {
|
||||
...frontendNode,
|
||||
position: backendNode.metadata.position,
|
||||
data: {
|
||||
...frontendNode.data,
|
||||
backend_id: backendNode.id,
|
||||
},
|
||||
}
|
||||
...frontendNode,
|
||||
position: backendNode.metadata.position,
|
||||
data: {
|
||||
...frontendNode.data,
|
||||
backend_id: backendNode.id,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
}).filter(node => node !== null);
|
||||
|
||||
@@ -433,20 +467,20 @@ const FlowEditor: React.FC<{
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: isSidebarOpen ? '350px' : '10px',
|
||||
zIndex: 10000,
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{isSidebarOpen ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={toggleSidebar}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: isSidebarOpen ? '350px' : '10px',
|
||||
zIndex: 10000,
|
||||
backgroundColor: 'black',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
{isSidebarOpen ? <ChevronLeft className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Sidebar isOpen={isSidebarOpen} availableNodes={availableNodes} addNode={addNode} />
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
@@ -455,6 +489,8 @@ const FlowEditor: React.FC<{
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
connectionLineComponent={ConnectionLine}
|
||||
onEdgesDelete={onEdgesDelete}
|
||||
deleteKeyCode={["Backspace", "Delete"]}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { FC, useEffect } from 'react';
|
||||
import React, { FC, useEffect, useRef } from 'react';
|
||||
import { Button } from './ui/button';
|
||||
import { Textarea } from './ui/textarea';
|
||||
|
||||
@@ -11,10 +11,14 @@ interface ModalProps {
|
||||
|
||||
const ModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
|
||||
const [tempValue, setTempValue] = React.useState(value);
|
||||
const textAreaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setTempValue(value);
|
||||
if (textAreaRef.current) {
|
||||
textAreaRef.current.select();
|
||||
}
|
||||
}
|
||||
}, [isOpen, value]);
|
||||
|
||||
@@ -28,10 +32,11 @@ const ModalComponent: FC<ModalProps> = ({ isOpen, onClose, onSave, value }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-white bg-opacity-60 flex justify-center items-center">
|
||||
<div className="nodrag fixed inset-0 bg-white bg-opacity-60 flex justify-center items-center">
|
||||
<div className="bg-white p-5 rounded-lg w-[500px] max-w-[90%]">
|
||||
<center><h1>Enter input text</h1></center>
|
||||
<Textarea
|
||||
ref={textAreaRef}
|
||||
className="w-full h-[200px] p-2.5 rounded border border-[#dfdfdf] text-black bg-[#dfdfdf]"
|
||||
value={tempValue}
|
||||
onChange={(e) => setTempValue(e.target.value)}
|
||||
|
||||
74
rnd/autogpt_builder/src/components/NodeHandle.tsx
Normal file
74
rnd/autogpt_builder/src/components/NodeHandle.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { BlockSchema } from "@/lib/types";
|
||||
import { beautifyString, getTypeBgColor, getTypeTextColor } from "@/lib/utils";
|
||||
import { FC } from "react";
|
||||
import { Handle, Position } from "reactflow";
|
||||
import SchemaTooltip from "./SchemaTooltip";
|
||||
|
||||
type HandleProps = {
|
||||
keyName: string,
|
||||
schema: BlockSchema,
|
||||
isConnected: boolean,
|
||||
side: 'left' | 'right'
|
||||
}
|
||||
|
||||
const NodeHandle: FC<HandleProps> = ({ keyName, isConnected, schema, side }) => {
|
||||
|
||||
const typeName: Record<string, string> = {
|
||||
string: 'text',
|
||||
number: 'number',
|
||||
boolean: 'true/false',
|
||||
object: 'complex',
|
||||
array: 'list',
|
||||
null: 'null',
|
||||
};
|
||||
|
||||
const typeClass = `text-sm ${getTypeTextColor(schema.type)} ${side === 'left' ? 'text-left' : 'text-right'}`;
|
||||
|
||||
const label = (
|
||||
<div className="flex flex-col flex-grow">
|
||||
<span className="text-m text-gray-900 -mb-1 green">{schema.title || beautifyString(keyName)}</span>
|
||||
<span className={typeClass}>{typeName[schema.type]}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const dot = (
|
||||
<div className={`w-4 h-4 m-1 ${isConnected ? getTypeBgColor(schema.type) : 'bg-gray-600'} rounded-full transition-colors duration-100 group-hover:bg-gray-300`} />
|
||||
);
|
||||
|
||||
if (side === 'left') {
|
||||
return (
|
||||
<div key={keyName} className="handle-container">
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
id={keyName}
|
||||
className='group -ml-[29px]'
|
||||
>
|
||||
<div className="pointer-events-none flex items-center">
|
||||
{dot}
|
||||
{label}
|
||||
</div>
|
||||
</Handle>
|
||||
<SchemaTooltip schema={schema} />
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<div key={keyName} className="handle-container justify-end">
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={keyName}
|
||||
className='group -mr-[29px]'
|
||||
>
|
||||
<div className="pointer-events-none flex items-center">
|
||||
{label}
|
||||
{dot}
|
||||
</div>
|
||||
</Handle>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default NodeHandle;
|
||||
@@ -14,7 +14,7 @@ const SchemaTooltip: React.FC<{ schema: BlockSchema }> = ({ schema }) => {
|
||||
return (
|
||||
<TooltipProvider delayDuration={400}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="flex items-center justify-center" asChild>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="p-1 rounded-full hover:bg-gray-300" size={24} />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="max-w-xs tooltip-content">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.custom-node {
|
||||
padding: 15px;
|
||||
border: 3px solid #000; /* Thicker border */
|
||||
border: 3px solid #4b5563;
|
||||
border-radius: 12px;
|
||||
background: #ffffff; /* White background */
|
||||
color: #000000;
|
||||
@@ -9,24 +9,6 @@
|
||||
transition: border-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.node-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.node-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.node-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -34,39 +16,23 @@
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.input-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
color: #000000;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.toggle-button:hover {
|
||||
color: #000000;
|
||||
background: #d1d1d1;
|
||||
}
|
||||
|
||||
.handle-label {
|
||||
color: #000000;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.output-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.handle-container {
|
||||
display: flex;
|
||||
position: relative;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 0px;
|
||||
padding: 5px;
|
||||
min-height: 44px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.react-flow__handle {
|
||||
background: transparent;
|
||||
width: auto;
|
||||
height: auto;
|
||||
border: 0;
|
||||
position: relative;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
@@ -206,13 +172,6 @@
|
||||
border-color: #c0392b; /* Red border for failed nodes */
|
||||
}
|
||||
|
||||
/* Adjust handle size */
|
||||
.custom-node .react-flow__handle {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
/* Make edges thicker */
|
||||
.react-flow__edge {
|
||||
stroke-width: 2px !important;
|
||||
.custom-switch {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,47 @@ export function deepEquals(x: any, y: any): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
/** Get tailwind text color class from type name */
|
||||
export function getTypeTextColor(type: string | null): string {
|
||||
if (type === null) return 'bg-gray-500';
|
||||
return {
|
||||
string: 'text-green-500',
|
||||
number: 'text-blue-500',
|
||||
boolean: 'text-yellow-500',
|
||||
object: 'text-purple-500',
|
||||
array: 'text-indigo-500',
|
||||
null: 'text-gray-500',
|
||||
'': 'text-gray-500',
|
||||
}[type] || 'text-gray-500';
|
||||
}
|
||||
|
||||
/** Get tailwind bg color class from type name */
|
||||
export function getTypeBgColor(type: string | null): string {
|
||||
if (type === null) return 'bg-gray-500';
|
||||
return {
|
||||
string: 'bg-green-500',
|
||||
number: 'bg-blue-500',
|
||||
boolean: 'bg-yellow-500',
|
||||
object: 'bg-purple-500',
|
||||
array: 'bg-indigo-500',
|
||||
null: 'bg-gray-500',
|
||||
'': 'bg-gray-500',
|
||||
}[type] || 'bg-gray-500';
|
||||
}
|
||||
|
||||
export function getTypeColor(type: string | null): string {
|
||||
if (type === null) return 'bg-gray-500';
|
||||
return {
|
||||
string: '#22c55e',
|
||||
number: '#3b82f6',
|
||||
boolean: '#eab308',
|
||||
object: '#a855f7',
|
||||
array: '#6366f1',
|
||||
null: '#6b7280',
|
||||
'': '#6b7280',
|
||||
}[type] || '#6b7280';
|
||||
}
|
||||
|
||||
export function beautifyString(name: string): string {
|
||||
// Regular expression to identify places to split, considering acronyms
|
||||
const result = name
|
||||
|
||||
Reference in New Issue
Block a user