126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
'use client';
|
|
|
|
import React, { memo, useCallback } from 'react';
|
|
import { Handle, Position, type NodeProps } from '@xyflow/react';
|
|
import { Lock, Unlock } from 'lucide-react';
|
|
|
|
import type { ToolDefinition, AuthConfig } from '@mcpengine/ai-pipeline/types';
|
|
|
|
export interface ToolNodeData extends Record<string, unknown> {
|
|
tool: ToolDefinition;
|
|
selected: boolean;
|
|
enabled: boolean;
|
|
onToggleEnabled?: (name: string) => void;
|
|
}
|
|
|
|
const methodColors: Record<string, { bg: string; text: string }> = {
|
|
GET: { bg: 'bg-emerald-500/20', text: 'text-emerald-400' },
|
|
POST: { bg: 'bg-blue-500/20', text: 'text-blue-400' },
|
|
PUT: { bg: 'bg-amber-500/20', text: 'text-amber-400' },
|
|
PATCH: { bg: 'bg-orange-500/20', text: 'text-orange-400' },
|
|
DELETE: { bg: 'bg-red-500/20', text: 'text-red-400' },
|
|
};
|
|
|
|
function getParamCount(tool: ToolDefinition): number {
|
|
return Object.keys(tool.inputSchema?.properties ?? {}).length;
|
|
}
|
|
|
|
function hasAuth(tool: ToolDefinition): boolean {
|
|
// Infer auth from endpoint or annotations — the tool itself doesn't carry auth,
|
|
// but we check if the method implies auth-required patterns
|
|
return tool.endpoint?.includes('auth') || false;
|
|
}
|
|
|
|
export const ToolNode = memo(function ToolNode({ data, selected }: NodeProps) {
|
|
const { tool, enabled = true, onToggleEnabled } = data as ToolNodeData;
|
|
const method = (tool.method ?? 'GET').toUpperCase();
|
|
const colors = methodColors[method] ?? methodColors.GET;
|
|
const paramCount = getParamCount(tool);
|
|
|
|
const handleToggle = useCallback(
|
|
(e: React.MouseEvent) => {
|
|
e.stopPropagation();
|
|
onToggleEnabled?.(tool.name);
|
|
},
|
|
[onToggleEnabled, tool.name]
|
|
);
|
|
|
|
return (
|
|
<div
|
|
className={`
|
|
bg-gray-800 border rounded-xl p-4 w-[280px] transition-all duration-200
|
|
${selected
|
|
? 'border-indigo-500 shadow-[0_0_20px_rgba(99,102,241,0.15)]'
|
|
: 'border-gray-600 hover:border-gray-500'
|
|
}
|
|
${!enabled ? 'opacity-50' : ''}
|
|
`}
|
|
>
|
|
{/* Target handle (top) */}
|
|
<Handle
|
|
type="target"
|
|
position={Position.Top}
|
|
className="!w-3 !h-3 !bg-gray-500 !border-gray-400 hover:!bg-indigo-500 transition-colors"
|
|
/>
|
|
|
|
{/* Method badge + name */}
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<span
|
|
className={`${colors.bg} ${colors.text} text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded`}
|
|
>
|
|
{method}
|
|
</span>
|
|
<span className="font-semibold text-gray-100 text-sm truncate flex-1">
|
|
{tool.name}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Description */}
|
|
<p className="text-sm text-gray-400 line-clamp-2 mb-3 leading-relaxed">
|
|
{tool.description || 'No description'}
|
|
</p>
|
|
|
|
{/* Bottom row */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
{/* Param count badge */}
|
|
<span className="text-[10px] bg-gray-700 text-gray-300 px-2 py-0.5 rounded-full">
|
|
{paramCount} param{paramCount !== 1 ? 's' : ''}
|
|
</span>
|
|
|
|
{/* Auth indicator */}
|
|
{hasAuth(tool) ? (
|
|
<Lock className="w-3.5 h-3.5 text-amber-400" />
|
|
) : (
|
|
<Unlock className="w-3.5 h-3.5 text-gray-500" />
|
|
)}
|
|
</div>
|
|
|
|
{/* Enabled/Disabled toggle */}
|
|
<button
|
|
onClick={handleToggle}
|
|
className={`
|
|
relative w-9 h-5 rounded-full transition-colors duration-200
|
|
${enabled ? 'bg-indigo-500' : 'bg-gray-600'}
|
|
`}
|
|
>
|
|
<span
|
|
className={`
|
|
absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full
|
|
transition-transform duration-200
|
|
${enabled ? 'translate-x-4' : 'translate-x-0'}
|
|
`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Source handle (bottom) */}
|
|
<Handle
|
|
type="source"
|
|
position={Position.Bottom}
|
|
className="!w-3 !h-3 !bg-gray-500 !border-gray-400 hover:!bg-indigo-500 transition-colors"
|
|
/>
|
|
</div>
|
|
);
|
|
});
|