HomePilot / frontend /src /ui /mcp /AddServerDrawer.jsx
HomePilot Deploy Bot
chore(hf): sync HomePilot to HF Space
921d377
Raw
History Blame Contribute Delete
6.67 kB
import React, { useState } from 'react';
import { X, Plus, RefreshCw } from 'lucide-react';
const TRANSPORTS = ['SSE', 'STREAMABLEHTTP', 'HTTP', 'STDIO'];
export function AddServerDrawer({ backendUrl, apiKey, onClose, onRegistered }) {
const [name, setName] = useState('');
const [url, setUrl] = useState('');
const [transport, setTransport] = useState('SSE');
const [description, setDescription] = useState('');
const [autoRefresh, setAutoRefresh] = useState(true);
const [busy, setBusy] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
const canSubmit = name.trim() && url.trim() && !busy;
const handleSubmit = async () => {
if (!canSubmit)
return;
setBusy(true);
setError(null);
try {
const headers = { 'Content-Type': 'application/json' };
if (apiKey)
headers['x-api-key'] = apiKey;
const res = await fetch(`${backendUrl}/v1/agentic/register/gateway`, {
method: 'POST',
headers,
body: JSON.stringify({
name: name.trim(),
url: url.trim(),
transport,
description: description.trim(),
auto_refresh: autoRefresh,
}),
});
const data = await res.json();
if (!res.ok || !data.ok) {
throw new Error(data.detail || `HTTP ${res.status}`);
}
setSuccess(true);
setTimeout(() => {
onRegistered();
onClose();
}, 1000);
}
catch (e) {
setError(e?.message || 'Registration failed');
}
finally {
setBusy(false);
}
};
return (<div className="fixed inset-0 z-50 flex justify-end" onClick={onClose}>
<div className="absolute inset-0 bg-black/60 backdrop-blur-sm"/>
<div className="relative w-full max-w-md bg-[#0b0b12] border-l border-white/10 h-full overflow-y-auto animate-slide-in-right" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="sticky top-0 bg-[#0b0b12]/95 backdrop-blur border-b border-white/10 px-6 py-4 flex items-center justify-between z-10">
<h2 className="text-base font-semibold text-white">Add MCP Server</h2>
<button onClick={onClose} className="p-2 text-white/50 hover:text-white hover:bg-white/10 rounded-xl transition-colors">
<X size={18}/>
</button>
</div>
{/* Form */}
<div className="px-6 py-6 space-y-5">
{/* Name */}
<div>
<label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block">
Server Name *
</label>
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. my-mcp-server" className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors"/>
</div>
{/* URL */}
<div>
<label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block">
Endpoint URL *
</label>
<input type="url" value={url} onChange={(e) => setUrl(e.target.value)} placeholder="e.g. http://localhost:9101/rpc" className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors"/>
</div>
{/* Transport */}
<div>
<label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block">
Transport
</label>
<div className="flex gap-2">
{TRANSPORTS.map((t) => (<button key={t} onClick={() => setTransport(t)} className={`px-3 py-1.5 text-xs font-medium rounded-lg border transition-colors ${transport === t
? 'bg-purple-500/20 border-purple-500/50 text-purple-300'
: 'bg-white/5 border-white/10 text-white/40 hover:text-white/60'}`}>
{t}
</button>))}
</div>
</div>
{/* Description */}
<div>
<label className="text-xs font-semibold text-white/50 uppercase tracking-wide mb-1.5 block">
Description
</label>
<textarea value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Optional description..." rows={3} className="w-full bg-white/5 border border-white/10 rounded-xl px-4 py-2.5 text-sm text-white placeholder-white/30 focus:outline-none focus:border-purple-500/50 transition-colors resize-none"/>
</div>
{/* Auto-refresh toggle */}
<label className="flex items-center gap-3 cursor-pointer">
<div onClick={() => setAutoRefresh(!autoRefresh)} className={`w-10 h-5 rounded-full transition-colors relative ${autoRefresh ? 'bg-purple-500' : 'bg-white/10'}`}>
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${autoRefresh ? 'left-5' : 'left-0.5'}`}/>
</div>
<span className="text-sm text-white/70">Auto-discover tools after registration</span>
</label>
{/* Error / Success */}
{error && (<div className="text-sm text-red-400 bg-red-500/10 border border-red-500/20 rounded-xl px-4 py-3">
{error}
</div>)}
{success && (<div className="text-sm text-emerald-400 bg-emerald-500/10 border border-emerald-500/20 rounded-xl px-4 py-3">
Server registered successfully!
</div>)}
{/* Submit */}
<button onClick={handleSubmit} disabled={!canSubmit} className="w-full flex items-center justify-center gap-2 bg-purple-500 hover:bg-purple-600 disabled:bg-white/10 disabled:text-white/30 px-4 py-3 rounded-xl text-sm font-semibold transition-all">
{busy ? (<RefreshCw size={16} className="animate-spin"/>) : (<Plus size={16}/>)}
{busy ? 'Registering...' : 'Register Gateway'}
</button>
</div>
</div>
<style>{`
@keyframes slide-in-right {
from { transform: translateX(100%); }
to { transform: translateX(0); }
}
.animate-slide-in-right {
animation: slide-in-right 0.2s ease-out;
}
`}</style>
</div>);
}