Jake Shore 96e52666c5 MCPEngine full sync — studio scaffold, factory v2, server updates, state.json — 2026-02-12
=== 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
2026-02-12 17:58:33 -05:00

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