feat: 新增技能编排引擎和工作流构建器组件
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
refactor: 统一Hands系统常量到单个源文件 refactor: 更新Hands中文名称和描述 fix: 修复技能市场在连接状态变化时重新加载 fix: 修复身份变更提案的错误处理逻辑 docs: 更新多个功能文档的验证状态和实现位置 docs: 更新Hands系统文档 test: 添加测试文件验证工作区路径
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Condition Node Component
|
||||
*
|
||||
* Node for conditional branching.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { ConditionNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
export const ConditionNode = memo(({ data, selected }: NodeProps<ConditionNodeData>) => {
|
||||
const branchCount = data.branches.length + (data.hasDefault ? 1 : 0);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[200px]
|
||||
bg-orange-50 border-orange-300
|
||||
${selected ? 'border-orange-500 shadow-lg shadow-orange-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-orange-400 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">🔀</span>
|
||||
<span className="font-medium text-orange-800">{data.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Condition Preview */}
|
||||
<div className="text-sm text-orange-600 bg-orange-100 rounded px-2 py-1 font-mono mb-2">
|
||||
{data.condition || 'No condition'}
|
||||
</div>
|
||||
|
||||
{/* Branches */}
|
||||
<div className="space-y-1">
|
||||
{data.branches.map((branch, index) => (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="relative">
|
||||
{/* Branch Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id={`branch-${index}`}
|
||||
style={{ top: `${((index + 1) / (branchCount + 1)) * 100}%` }}
|
||||
className="w-3 h-3 bg-orange-400 border-2 border-white"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs text-orange-500 truncate max-w-[120px]">
|
||||
{branch.label || branch.when}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{data.hasDefault && (
|
||||
<div className="flex items-center justify-between">
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
id="default"
|
||||
style={{ top: '100%' }}
|
||||
className="w-3 h-3 bg-gray-400 border-2 border-white"
|
||||
/>
|
||||
<span className="text-xs text-gray-500">Default</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ConditionNode.displayName = 'ConditionNode';
|
||||
|
||||
export default ConditionNode;
|
||||
72
desktop/src/components/WorkflowBuilder/nodes/ExportNode.tsx
Normal file
72
desktop/src/components/WorkflowBuilder/nodes/ExportNode.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Export Node Component
|
||||
*
|
||||
* Node for exporting workflow results to various formats.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { ExportNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
export const ExportNode = memo(({ data, selected }: NodeProps<ExportNodeData>) => {
|
||||
const formatLabels: Record<string, string> = {
|
||||
pptx: 'PowerPoint',
|
||||
html: 'HTML',
|
||||
pdf: 'PDF',
|
||||
markdown: 'Markdown',
|
||||
json: 'JSON',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[180px]
|
||||
bg-blue-50 border-blue-300
|
||||
${selected ? 'border-blue-500 shadow-lg shadow-blue-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-blue-400 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-blue-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">📤</span>
|
||||
<span className="font-medium text-blue-800">{data.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Formats */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{data.formats.map((format) => (
|
||||
<span
|
||||
key={format}
|
||||
className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded"
|
||||
>
|
||||
{formatLabels[format] || format}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Output Directory */}
|
||||
{data.outputDir && (
|
||||
<div className="text-xs text-blue-500 mt-2 truncate">
|
||||
📁 {data.outputDir}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ExportNode.displayName = 'ExportNode';
|
||||
|
||||
export default ExportNode;
|
||||
74
desktop/src/components/WorkflowBuilder/nodes/HandNode.tsx
Normal file
74
desktop/src/components/WorkflowBuilder/nodes/HandNode.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Hand Node Component
|
||||
*
|
||||
* Node for executing hand actions.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { HandNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
export const HandNode = memo(({ data, selected }: NodeProps<HandNodeData>) => {
|
||||
const hasHand = Boolean(data.handId);
|
||||
const hasAction = Boolean(data.action);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[180px]
|
||||
bg-rose-50 border-rose-300
|
||||
${selected ? 'border-rose-500 shadow-lg shadow-rose-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-rose-400 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-rose-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">✋</span>
|
||||
<span className="font-medium text-rose-800">{data.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Hand Info */}
|
||||
<div className="space-y-1">
|
||||
<div className={`text-sm ${hasHand ? 'text-rose-600' : 'text-rose-400 italic'}`}>
|
||||
{hasHand ? (
|
||||
<span className="font-mono bg-rose-100 px-1.5 py-0.5 rounded">
|
||||
{data.handName || data.handId}
|
||||
</span>
|
||||
) : (
|
||||
'No hand selected'
|
||||
)}
|
||||
</div>
|
||||
|
||||
{hasAction && (
|
||||
<div className="text-xs text-rose-500">
|
||||
Action: <span className="font-mono">{data.action}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Params Count */}
|
||||
{Object.keys(data.params).length > 0 && (
|
||||
<div className="text-xs text-rose-500 mt-1">
|
||||
{Object.keys(data.params).length} param(s)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
HandNode.displayName = 'HandNode';
|
||||
|
||||
export default HandNode;
|
||||
81
desktop/src/components/WorkflowBuilder/nodes/HttpNode.tsx
Normal file
81
desktop/src/components/WorkflowBuilder/nodes/HttpNode.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* HTTP Node Component
|
||||
*
|
||||
* Node for making HTTP requests.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { HttpNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
const methodColors: Record<string, string> = {
|
||||
GET: 'bg-green-100 text-green-700',
|
||||
POST: 'bg-blue-100 text-blue-700',
|
||||
PUT: 'bg-yellow-100 text-yellow-700',
|
||||
DELETE: 'bg-red-100 text-red-700',
|
||||
PATCH: 'bg-purple-100 text-purple-700',
|
||||
};
|
||||
|
||||
export const HttpNode = memo(({ data, selected }: NodeProps<HttpNodeData>) => {
|
||||
const hasUrl = Boolean(data.url);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[200px]
|
||||
bg-slate-50 border-slate-300
|
||||
${selected ? 'border-slate-500 shadow-lg shadow-slate-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-slate-400 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-slate-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">🌐</span>
|
||||
<span className="font-medium text-slate-800">{data.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Method Badge */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded ${methodColors[data.method]}`}>
|
||||
{data.method}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* URL */}
|
||||
<div className={`text-sm font-mono bg-slate-100 rounded px-2 py-1 truncate ${hasUrl ? 'text-slate-600' : 'text-slate-400 italic'}`}>
|
||||
{hasUrl ? data.url : 'No URL specified'}
|
||||
</div>
|
||||
|
||||
{/* Headers Count */}
|
||||
{Object.keys(data.headers).length > 0 && (
|
||||
<div className="text-xs text-slate-500 mt-2">
|
||||
{Object.keys(data.headers).length} header(s)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body Indicator */}
|
||||
{data.body && (
|
||||
<div className="text-xs text-slate-500 mt-1">
|
||||
Has body content
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
HttpNode.displayName = 'HttpNode';
|
||||
|
||||
export default HttpNode;
|
||||
54
desktop/src/components/WorkflowBuilder/nodes/InputNode.tsx
Normal file
54
desktop/src/components/WorkflowBuilder/nodes/InputNode.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Input Node Component
|
||||
*
|
||||
* Node for defining workflow input variables.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { InputNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
export const InputNode = memo(({ data, selected }: NodeProps<InputNodeData>) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[180px]
|
||||
bg-emerald-50 border-emerald-300
|
||||
${selected ? 'border-emerald-500 shadow-lg shadow-emerald-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-emerald-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">📥</span>
|
||||
<span className="font-medium text-emerald-800">{data.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Variable Name */}
|
||||
<div className="text-sm text-emerald-600">
|
||||
<span className="font-mono bg-emerald-100 px-1.5 py-0.5 rounded">
|
||||
{data.variableName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Default Value Indicator */}
|
||||
{data.defaultValue !== undefined && (
|
||||
<div className="text-xs text-emerald-500 mt-1">
|
||||
default: {typeof data.defaultValue === 'string'
|
||||
? `"${data.defaultValue}"`
|
||||
: JSON.stringify(data.defaultValue)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
InputNode.displayName = 'InputNode';
|
||||
|
||||
export default InputNode;
|
||||
70
desktop/src/components/WorkflowBuilder/nodes/LlmNode.tsx
Normal file
70
desktop/src/components/WorkflowBuilder/nodes/LlmNode.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* LLM Node Component
|
||||
*
|
||||
* Node for LLM generation actions.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { LlmNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
export const LlmNode = memo(({ data, selected }: NodeProps<LlmNodeData>) => {
|
||||
const templatePreview = data.template.length > 50
|
||||
? data.template.slice(0, 50) + '...'
|
||||
: data.template || 'No template';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[200px]
|
||||
bg-violet-50 border-violet-300
|
||||
${selected ? 'border-violet-500 shadow-lg shadow-violet-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-violet-400 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-violet-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">🤖</span>
|
||||
<span className="font-medium text-violet-800">{data.label}</span>
|
||||
{data.jsonMode && (
|
||||
<span className="text-xs bg-violet-200 text-violet-700 px-1.5 py-0.5 rounded">
|
||||
JSON
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Template Preview */}
|
||||
<div className="text-sm text-violet-600 bg-violet-100 rounded px-2 py-1 font-mono">
|
||||
{data.isTemplateFile ? '📄 ' : ''}
|
||||
{templatePreview}
|
||||
</div>
|
||||
|
||||
{/* Model Info */}
|
||||
{(data.model || data.temperature !== undefined) && (
|
||||
<div className="flex gap-2 mt-2 text-xs text-violet-500">
|
||||
{data.model && <span>Model: {data.model}</span>}
|
||||
{data.temperature !== undefined && (
|
||||
<span>Temp: {data.temperature}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
LlmNode.displayName = 'LlmNode';
|
||||
|
||||
export default LlmNode;
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Orchestration Node Component
|
||||
*
|
||||
* Node for executing skill orchestration graphs (DAGs).
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { OrchestrationNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
export const OrchestrationNode = memo(({ data, selected }: NodeProps<OrchestrationNodeData>) => {
|
||||
const hasGraphId = Boolean(data.graphId);
|
||||
const hasGraph = Boolean(data.graph);
|
||||
const inputCount = Object.keys(data.inputMappings).length;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[200px]
|
||||
bg-gradient-to-br from-indigo-50 to-purple-50
|
||||
border-indigo-300
|
||||
${selected ? 'border-indigo-500 shadow-lg shadow-indigo-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-indigo-400 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-indigo-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">🔀</span>
|
||||
<span className="font-medium text-indigo-800">{data.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Graph Reference */}
|
||||
<div className={`text-sm mb-2 ${hasGraphId || hasGraph ? 'text-indigo-600' : 'text-indigo-400 italic'}`}>
|
||||
{hasGraphId ? (
|
||||
<div className="flex items-center gap-1.5 bg-indigo-100 rounded px-2 py-1">
|
||||
<span className="text-xs">📋</span>
|
||||
<span className="font-mono text-xs">{data.graphId}</span>
|
||||
</div>
|
||||
) : hasGraph ? (
|
||||
<div className="flex items-center gap-1.5 bg-indigo-100 rounded px-2 py-1">
|
||||
<span className="text-xs">📊</span>
|
||||
<span className="text-xs">Inline graph</span>
|
||||
</div>
|
||||
) : (
|
||||
'No graph configured'
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Mappings */}
|
||||
{inputCount > 0 && (
|
||||
<div className="text-xs text-indigo-500 mt-2">
|
||||
{inputCount} input mapping(s)
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{data.description && (
|
||||
<div className="text-xs text-indigo-400 mt-2 line-clamp-2">
|
||||
{data.description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
OrchestrationNode.displayName = 'OrchestrationNode';
|
||||
|
||||
export default OrchestrationNode;
|
||||
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Parallel Node Component
|
||||
*
|
||||
* Node for parallel execution of steps.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { ParallelNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
export const ParallelNode = memo(({ data, selected }: NodeProps<ParallelNodeData>) => {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[180px]
|
||||
bg-cyan-50 border-cyan-300
|
||||
${selected ? 'border-cyan-500 shadow-lg shadow-cyan-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-cyan-400 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-cyan-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">⚡</span>
|
||||
<span className="font-medium text-cyan-800">{data.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Each Expression */}
|
||||
<div className="text-sm text-cyan-600 bg-cyan-100 rounded px-2 py-1 font-mono">
|
||||
each: {data.each || '${inputs.items}'}
|
||||
</div>
|
||||
|
||||
{/* Max Workers */}
|
||||
<div className="text-xs text-cyan-500 mt-2">
|
||||
Max workers: {data.maxWorkers}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
ParallelNode.displayName = 'ParallelNode';
|
||||
|
||||
export default ParallelNode;
|
||||
65
desktop/src/components/WorkflowBuilder/nodes/SkillNode.tsx
Normal file
65
desktop/src/components/WorkflowBuilder/nodes/SkillNode.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Skill Node Component
|
||||
*
|
||||
* Node for executing skills.
|
||||
*/
|
||||
|
||||
import React, { memo } from 'react';
|
||||
import { Handle, Position, NodeProps } from '@xyflow/react';
|
||||
import type { SkillNodeData } from '../../../lib/workflow-builder/types';
|
||||
|
||||
export const SkillNode = memo(({ data, selected }: NodeProps<SkillNodeData>) => {
|
||||
const hasSkill = Boolean(data.skillId);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
px-4 py-3 rounded-lg border-2 min-w-[180px]
|
||||
bg-amber-50 border-amber-300
|
||||
${selected ? 'border-amber-500 shadow-lg shadow-amber-200' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Input Handle */}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
className="w-3 h-3 bg-amber-400 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Output Handle */}
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-3 h-3 bg-amber-500 border-2 border-white"
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="text-lg">⚡</span>
|
||||
<span className="font-medium text-amber-800">{data.label}</span>
|
||||
</div>
|
||||
|
||||
{/* Skill ID */}
|
||||
<div className={`text-sm ${hasSkill ? 'text-amber-600' : 'text-amber-400 italic'}`}>
|
||||
{hasSkill ? (
|
||||
<span className="font-mono bg-amber-100 px-1.5 py-0.5 rounded">
|
||||
{data.skillName || data.skillId}
|
||||
</span>
|
||||
) : (
|
||||
'No skill selected'
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Mappings Count */}
|
||||
{Object.keys(data.inputMappings).length > 0 && (
|
||||
<div className="text-xs text-amber-500 mt-1">
|
||||
{Object.keys(data.inputMappings).length} input mapping(s)
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
SkillNode.displayName = 'SkillNode';
|
||||
|
||||
export default SkillNode;
|
||||
Reference in New Issue
Block a user