2026-02-06 23:01:30 -05:00

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>
);
});