Download frontend/src/ui/mcp/AddServerDrawer.jsx from ruslanmv/HomePilot: direct link, hf CLI and curl.
- Browser
- Download file 6.67 kB
-
https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/mcp/AddServerDrawer.jsx
- Command line
-
hf download hf://spaces/ruslanmv/HomePilot/frontend/src/ui/mcp/AddServerDrawer.jsx
-
curl -L -o AddServerDrawer.jsx https://huggingface.co/spaces/ruslanmv/HomePilot/resolve/main/frontend/src/ui/mcp/AddServerDrawer.jsx
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>); | |
| } | |