=== 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
237 lines
8.3 KiB
TypeScript
237 lines
8.3 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useCallback } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { ArrowLeft, ArrowRight, Check, Loader2 } from 'lucide-react';
|
|
import { SpecUploader } from '@/components/spec-upload/SpecUploader';
|
|
import { AnalysisStream } from '@/components/spec-upload/AnalysisStream';
|
|
|
|
type Step = 1 | 2 | 3;
|
|
|
|
interface DiscoveredTool {
|
|
name: string;
|
|
method: string;
|
|
description: string;
|
|
paramCount: number;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export default function NewProjectPage() {
|
|
const router = useRouter();
|
|
|
|
const [step, setStep] = useState<Step>(1);
|
|
const [name, setName] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [analysisInput, setAnalysisInput] = useState<{ type: 'url' | 'raw'; value: string } | null>(null);
|
|
const [analyzing, setAnalyzing] = useState(false);
|
|
const [creating, setCreating] = useState(false);
|
|
|
|
const steps = [
|
|
{ num: 1, label: 'Project Info' },
|
|
{ num: 2, label: 'API Spec' },
|
|
{ num: 3, label: 'Discover Tools' },
|
|
];
|
|
|
|
const canProceedStep1 = name.trim().length >= 2;
|
|
|
|
const handleAnalyze = useCallback((input: { type: 'url' | 'raw'; value: string }) => {
|
|
setAnalysisInput(input);
|
|
setAnalyzing(true);
|
|
setStep(3);
|
|
}, []);
|
|
|
|
const handleAnalysisComplete = useCallback((_tools: DiscoveredTool[]) => {
|
|
setAnalyzing(false);
|
|
}, []);
|
|
|
|
const handleContinue = useCallback(
|
|
async (selectedTools: DiscoveredTool[]) => {
|
|
setCreating(true);
|
|
try {
|
|
// Create the project
|
|
const res = await fetch('/api/projects', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: name.trim(),
|
|
description: description.trim(),
|
|
specUrl: analysisInput?.type === 'url' ? analysisInput.value : undefined,
|
|
specRaw: analysisInput?.type === 'raw' ? analysisInput.value : undefined,
|
|
tools: selectedTools,
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) throw new Error('Failed to create project');
|
|
|
|
const { data } = await res.json();
|
|
router.push(`/projects/${data.id}`);
|
|
} catch (err) {
|
|
console.error('[NewProject]', err);
|
|
setCreating(false);
|
|
}
|
|
},
|
|
[name, description, analysisInput, router],
|
|
);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-950 py-10 px-6">
|
|
<div className="max-w-2xl mx-auto">
|
|
{/* Back to projects */}
|
|
<button
|
|
onClick={() => router.push('/projects')}
|
|
className="flex items-center gap-2 text-sm text-gray-500 hover:text-gray-300
|
|
transition-colors mb-8"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Back to Projects
|
|
</button>
|
|
|
|
{/* Title */}
|
|
<h1 className="text-2xl font-bold text-white mb-2">Create New Project</h1>
|
|
<p className="text-gray-400 text-sm mb-8">
|
|
Build an MCP server from any API specification.
|
|
</p>
|
|
|
|
{/* Step indicator */}
|
|
<div className="flex items-center gap-3 mb-10">
|
|
{steps.map((s, i) => (
|
|
<div key={s.num} className="flex items-center gap-3">
|
|
<div className="flex items-center gap-2">
|
|
<div
|
|
className={`
|
|
w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium
|
|
transition-all duration-300
|
|
${
|
|
step > s.num
|
|
? 'bg-indigo-600 text-white'
|
|
: step === s.num
|
|
? 'bg-indigo-600 text-white ring-4 ring-indigo-600/20'
|
|
: 'bg-gray-800 text-gray-500'
|
|
}
|
|
`}
|
|
>
|
|
{step > s.num ? <Check className="h-4 w-4" /> : s.num}
|
|
</div>
|
|
<span
|
|
className={`text-sm font-medium hidden sm:block ${
|
|
step >= s.num ? 'text-gray-200' : 'text-gray-600'
|
|
}`}
|
|
>
|
|
{s.label}
|
|
</span>
|
|
</div>
|
|
{i < steps.length - 1 && (
|
|
<div
|
|
className={`w-12 h-px ${
|
|
step > s.num ? 'bg-indigo-600' : 'bg-gray-800'
|
|
}`}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Step 1: Name + Description */}
|
|
{step === 1 && (
|
|
<div className="space-y-6 animate-in fade-in duration-300">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Project Name *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="My API Server"
|
|
className="w-full px-4 py-3 rounded-xl bg-gray-800 border border-gray-700
|
|
text-gray-100 placeholder-gray-500 text-sm
|
|
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent
|
|
transition-all"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-300 mb-2">
|
|
Description
|
|
</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
placeholder="What does this MCP server do?"
|
|
rows={3}
|
|
className="w-full px-4 py-3 rounded-xl bg-gray-800 border border-gray-700
|
|
text-gray-100 placeholder-gray-500 text-sm
|
|
focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent
|
|
resize-none transition-all"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<button
|
|
onClick={() => setStep(2)}
|
|
disabled={!canProceedStep1}
|
|
className="inline-flex items-center gap-2 px-6 py-2.5 rounded-lg
|
|
font-medium text-sm bg-indigo-600 text-white
|
|
hover:bg-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed
|
|
transition-colors"
|
|
>
|
|
Next
|
|
<ArrowRight className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 2: Spec Upload */}
|
|
{step === 2 && (
|
|
<div className="space-y-6 animate-in fade-in duration-300">
|
|
<SpecUploader onAnalyze={handleAnalyze} loading={analyzing} />
|
|
|
|
<div className="flex justify-between">
|
|
<button
|
|
onClick={() => setStep(1)}
|
|
className="inline-flex items-center gap-2 px-5 py-2 rounded-lg
|
|
text-sm text-gray-400 bg-gray-800 border border-gray-700
|
|
hover:bg-gray-700 hover:text-gray-200 transition-colors"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Step 3: Analysis Stream */}
|
|
{step === 3 && analysisInput && (
|
|
<div className="space-y-6 animate-in fade-in duration-300">
|
|
<AnalysisStream
|
|
analysisInput={analysisInput}
|
|
onComplete={handleAnalysisComplete}
|
|
onContinue={handleContinue}
|
|
/>
|
|
|
|
{creating && (
|
|
<div className="flex items-center justify-center gap-2 text-gray-400 text-sm py-4">
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
Creating project...
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex justify-start">
|
|
<button
|
|
onClick={() => { setStep(2); setAnalysisInput(null); setAnalyzing(false); }}
|
|
className="inline-flex items-center gap-2 px-5 py-2 rounded-lg
|
|
text-sm text-gray-400 bg-gray-800 border border-gray-700
|
|
hover:bg-gray-700 hover:text-gray-200 transition-colors"
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Back
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|