=== NEW === - studio/ — MCPEngine Studio scaffold (Next.js monorepo, build plan) - docs/FACTORY-V2.md — Factory v2 architecture doc - docs/CALENDLY_MCP_BUILD_SUMMARY.md — Calendly MCP build report === UPDATED SERVERS === - fieldedge: Added jobs-tools, UI build script, main entry update - lightspeed: Updated main + server entry points - squarespace: Added collection-browser + page-manager apps - toast: Added main + server entry points === INFRA === - infra/command-center/state.json — Updated pipeline state - infra/command-center/FACTORY-V2.md — Factory v2 operator playbook
208 lines
6.6 KiB
TypeScript
208 lines
6.6 KiB
TypeScript
'use client';
|
|
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
import { ReactFlowProvider } from '@xyflow/react';
|
|
import {
|
|
Sparkles,
|
|
FlaskConical,
|
|
Rocket,
|
|
Plus,
|
|
} from 'lucide-react';
|
|
|
|
import { ToolCanvas } from '../../../../components/canvas/ToolCanvas';
|
|
import { ToolInspector } from '../../../../components/inspector/ToolInspector';
|
|
import { useCanvasState } from '../../../../hooks/useCanvasState';
|
|
|
|
import type { ToolDefinition } from '@mcpengine/ai-pipeline/types';
|
|
import type { ToolNodeData } from '../../../../components/canvas/ToolNode';
|
|
|
|
// Mock data for development — replaced by API calls in production
|
|
const MOCK_TOOLS: ToolDefinition[] = [
|
|
{
|
|
name: 'list_contacts',
|
|
description: 'Retrieve a paginated list of contacts with optional filtering by tags, dates, and custom fields.',
|
|
method: 'GET',
|
|
endpoint: '/contacts',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
limit: { type: 'number', description: 'Max results to return' },
|
|
offset: { type: 'number', description: 'Pagination offset' },
|
|
tag: { type: 'string', description: 'Filter by tag' },
|
|
},
|
|
required: [],
|
|
},
|
|
annotations: { readOnlyHint: true, idempotentHint: true },
|
|
},
|
|
{
|
|
name: 'create_contact',
|
|
description: 'Create a new contact with the provided information.',
|
|
method: 'POST',
|
|
endpoint: '/contacts',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
email: { type: 'string', description: 'Contact email address' },
|
|
name: { type: 'string', description: 'Full name' },
|
|
phone: { type: 'string', description: 'Phone number' },
|
|
},
|
|
required: ['email', 'name'],
|
|
},
|
|
},
|
|
{
|
|
name: 'delete_contact',
|
|
description: 'Permanently delete a contact by their ID. This action cannot be undone.',
|
|
method: 'DELETE',
|
|
endpoint: '/contacts/{id}',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string', description: 'Contact ID' },
|
|
},
|
|
required: ['id'],
|
|
},
|
|
annotations: { destructiveHint: true },
|
|
},
|
|
{
|
|
name: 'update_contact',
|
|
description: 'Update an existing contact\'s information.',
|
|
method: 'PUT',
|
|
endpoint: '/contacts/{id}',
|
|
inputSchema: {
|
|
type: 'object',
|
|
properties: {
|
|
id: { type: 'string', description: 'Contact ID' },
|
|
email: { type: 'string', description: 'Updated email' },
|
|
name: { type: 'string', description: 'Updated name' },
|
|
},
|
|
required: ['id'],
|
|
},
|
|
annotations: { idempotentHint: true },
|
|
},
|
|
];
|
|
|
|
export default function ProjectEditorPage() {
|
|
const [tools, setTools] = useState<ToolDefinition[]>(MOCK_TOOLS);
|
|
const {
|
|
selectedNodeId,
|
|
inspectorOpen,
|
|
nodes,
|
|
initializeFromTools,
|
|
selectNode,
|
|
updateTool,
|
|
removeTool,
|
|
} = useCanvasState();
|
|
|
|
// Initialize canvas from tools
|
|
useEffect(() => {
|
|
initializeFromTools(tools);
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
// Get selected tool
|
|
const selectedTool = selectedNodeId
|
|
? (nodes.find((n) => n.id === selectedNodeId)?.data as ToolNodeData | undefined)?.tool
|
|
: null;
|
|
|
|
const handleToolSelect = useCallback(
|
|
(toolName: string | null) => {
|
|
selectNode(toolName);
|
|
},
|
|
[selectNode]
|
|
);
|
|
|
|
const handleToolsChange = useCallback((updated: ToolDefinition[]) => {
|
|
setTools(updated);
|
|
}, []);
|
|
|
|
const handleToolChange = useCallback(
|
|
(updatedTool: ToolDefinition) => {
|
|
if (!selectedNodeId) return;
|
|
updateTool(selectedNodeId, updatedTool);
|
|
setTools((prev) =>
|
|
prev.map((t) => (t.name === selectedNodeId ? updatedTool : t))
|
|
);
|
|
},
|
|
[selectedNodeId, updateTool]
|
|
);
|
|
|
|
const handleToolDelete = useCallback(
|
|
(toolName: string) => {
|
|
removeTool(toolName);
|
|
setTools((prev) => prev.filter((t) => t.name !== toolName));
|
|
},
|
|
[removeTool]
|
|
);
|
|
|
|
const handleCloseInspector = useCallback(() => {
|
|
selectNode(null);
|
|
}, [selectNode]);
|
|
|
|
return (
|
|
<div className="flex flex-col h-screen bg-[#0A0F1E]">
|
|
{/* Top Bar */}
|
|
<header className="flex items-center justify-between px-6 py-3 border-b border-gray-800 bg-gray-900/80 backdrop-blur-sm z-10">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse" />
|
|
<h1 className="text-sm font-semibold text-gray-100">
|
|
Project Editor
|
|
</h1>
|
|
<span className="text-xs text-gray-500">
|
|
{tools.length} tools
|
|
</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded-lg text-xs text-gray-300 hover:border-gray-600 transition-colors">
|
|
<Plus className="w-3.5 h-3.5" />
|
|
Add Tool
|
|
</button>
|
|
<div className="w-px h-6 bg-gray-800" />
|
|
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded-lg text-xs text-gray-300 hover:border-gray-600 transition-colors">
|
|
<Sparkles className="w-3.5 h-3.5 text-amber-400" />
|
|
Analyze
|
|
</button>
|
|
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-gray-800 border border-gray-700 rounded-lg text-xs text-gray-300 hover:border-gray-600 transition-colors">
|
|
<FlaskConical className="w-3.5 h-3.5 text-blue-400" />
|
|
Test
|
|
</button>
|
|
<button className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-500 rounded-lg text-xs text-white transition-colors">
|
|
<Rocket className="w-3.5 h-3.5" />
|
|
Deploy
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex flex-1 overflow-hidden">
|
|
{/* Canvas */}
|
|
<ReactFlowProvider>
|
|
<div className="flex-1 relative">
|
|
<ToolCanvas
|
|
tools={tools}
|
|
onToolSelect={handleToolSelect}
|
|
onToolsChange={handleToolsChange}
|
|
/>
|
|
</div>
|
|
</ReactFlowProvider>
|
|
|
|
{/* Inspector Panel — slides in from right */}
|
|
<div
|
|
className={`
|
|
transition-all duration-300 ease-in-out overflow-hidden
|
|
${inspectorOpen && selectedTool ? 'w-[380px]' : 'w-0'}
|
|
`}
|
|
>
|
|
{selectedTool && (
|
|
<ToolInspector
|
|
tool={selectedTool}
|
|
onChange={handleToolChange}
|
|
onDelete={handleToolDelete}
|
|
onClose={handleCloseInspector}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|