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

236 lines
7.5 KiB
TypeScript

'use client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { CheckCircle2, Circle, ArrowRight, Loader2 } from 'lucide-react';
interface DiscoveredTool {
name: string;
method: string;
description: string;
paramCount: number;
enabled: boolean;
}
interface AnalysisStreamProps {
/** The analysis ID or spec input to stream */
analysisInput: { type: 'url' | 'raw'; value: string };
/** Called when analysis completes with selected tools */
onComplete: (tools: DiscoveredTool[]) => void;
/** Called if user clicks Continue */
onContinue: (tools: DiscoveredTool[]) => void;
}
const METHOD_COLORS: Record<string, string> = {
GET: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
POST: 'bg-blue-500/20 text-blue-400 border-blue-500/30',
PUT: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
PATCH: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
DELETE: 'bg-red-500/20 text-red-400 border-red-500/30',
};
export function AnalysisStream({
analysisInput,
onComplete,
onContinue,
}: AnalysisStreamProps) {
const [tools, setTools] = useState<DiscoveredTool[]>([]);
const [progress, setProgress] = useState(0);
const [step, setStep] = useState('Initializing...');
const [done, setDone] = useState(false);
const [error, setError] = useState<string | null>(null);
const eventSourceRef = useRef<EventSource | null>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const params = new URLSearchParams();
params.set('type', analysisInput.type);
params.set('value', analysisInput.value);
const es = new EventSource(`/api/analyze?${params.toString()}`);
eventSourceRef.current = es;
es.addEventListener('progress', (e) => {
try {
const data = JSON.parse(e.data);
setProgress(data.percent || 0);
setStep(data.step || '');
} catch {}
});
es.addEventListener('tool_found', (e) => {
try {
const data = JSON.parse(e.data);
const tool: DiscoveredTool = {
name: data.name || 'Unknown',
method: data.method || 'GET',
description: data.description || '',
paramCount: data.paramCount || Object.keys(data.inputSchema?.properties || {}).length || 0,
enabled: true,
};
setTools((prev) => [...prev, tool]);
} catch {}
});
es.addEventListener('complete', (e) => {
try {
const data = JSON.parse(e.data);
setProgress(100);
setStep('Analysis complete');
setDone(true);
// If complete event includes all tools, sync them
if (data.tools && Array.isArray(data.tools)) {
const allTools: DiscoveredTool[] = data.tools.map((t: any) => ({
name: t.name || 'Unknown',
method: t.method || 'GET',
description: t.description || '',
paramCount: t.paramCount || Object.keys(t.inputSchema?.properties || {}).length || 0,
enabled: true,
}));
setTools(allTools);
onComplete(allTools);
}
} catch {}
es.close();
});
es.addEventListener('error', (e) => {
try {
const data = JSON.parse((e as any).data || '{}');
setError(data.message || 'Analysis failed');
} catch {
setError('Connection lost. Analysis may have failed.');
}
es.close();
});
es.onerror = () => {
if (!done) {
setError('Connection lost');
es.close();
}
};
return () => {
es.close();
};
}, [analysisInput, done, onComplete]);
// Auto-scroll as tools appear
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight;
}
}, [tools]);
const toggleTool = useCallback((index: number) => {
setTools((prev) =>
prev.map((t, i) => (i === index ? { ...t, enabled: !t.enabled } : t)),
);
}, []);
const enabledCount = tools.filter((t) => t.enabled).length;
return (
<div className="w-full space-y-6">
{/* Progress bar */}
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span className="text-gray-400">{step}</span>
<span className="text-gray-500 font-mono">{Math.round(progress)}%</span>
</div>
<div className="w-full h-2 bg-gray-800 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-indigo-600 to-indigo-400 rounded-full
transition-all duration-500 ease-out"
style={{ width: `${progress}%` }}
/>
</div>
</div>
{/* Error state */}
{error && (
<div className="p-4 rounded-xl bg-red-500/10 border border-red-500/20 text-red-400 text-sm">
{error}
</div>
)}
{/* Tool list */}
<div
ref={containerRef}
className="space-y-2 max-h-[400px] overflow-y-auto pr-1"
style={{ scrollbarWidth: 'thin', scrollbarColor: '#374151 transparent' }}
>
{tools.map((tool, i) => (
<div
key={`${tool.name}-${i}`}
className="flex items-center gap-3 p-3 rounded-lg bg-gray-800/50 border border-gray-800
animate-in slide-in-from-bottom-2 duration-300"
style={{ animationDelay: `${i * 50}ms`, animationFillMode: 'both' }}
>
{/* Checkbox */}
<button
onClick={() => toggleTool(i)}
className="flex-shrink-0"
>
{tool.enabled ? (
<CheckCircle2 className="h-5 w-5 text-indigo-400" />
) : (
<Circle className="h-5 w-5 text-gray-600" />
)}
</button>
{/* Method badge */}
<span
className={`
flex-shrink-0 px-2 py-0.5 rounded text-xs font-mono font-bold border
${METHOD_COLORS[tool.method] || METHOD_COLORS.GET}
`}
>
{tool.method}
</span>
{/* Tool info */}
<div className="flex-1 min-w-0">
<span className="text-sm font-medium text-gray-200">{tool.name}</span>
<p className="text-xs text-gray-500 truncate">{tool.description}</p>
</div>
{/* Param count */}
<span className="flex-shrink-0 text-xs text-gray-600 font-mono">
{tool.paramCount}p
</span>
</div>
))}
{/* Loading indicator while streaming */}
{!done && !error && (
<div className="flex items-center gap-2 p-3 text-gray-500 text-sm">
<Loader2 className="h-4 w-4 animate-spin" />
Discovering tools...
</div>
)}
</div>
{/* Summary + Continue */}
{done && tools.length > 0 && (
<div className="flex items-center justify-between pt-4 border-t border-gray-800">
<span className="text-sm text-gray-400">
{enabledCount} of {tools.length} tools selected
</span>
<button
onClick={() => onContinue(tools.filter((t) => t.enabled))}
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 transition-colors
shadow-lg shadow-indigo-600/20"
>
Continue to Editor
<ArrowRight className="h-4 w-4" />
</button>
</div>
)}
</div>
);
}