diff --git a/ui/install-link-generator/index.html b/ui/install-link-generator/index.html new file mode 100644 index 00000000..9a73ce0e --- /dev/null +++ b/ui/install-link-generator/index.html @@ -0,0 +1,72 @@ + + + + + + Goose Install Link Generator + + + +
+

Install Link Generator

+ +
+ + +
+ +
+
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ + +
+
+ +
+
+ + +
+ +
+ +
+

Generated Link

+ +
+
+ + + \ No newline at end of file diff --git a/ui/install-link-generator/script.js b/ui/install-link-generator/script.js new file mode 100644 index 00000000..6f4147b0 --- /dev/null +++ b/ui/install-link-generator/script.js @@ -0,0 +1,152 @@ +document.addEventListener('DOMContentLoaded', () => { + // Tab switching + const tabs = document.querySelectorAll('.tab-btn'); + tabs.forEach(tab => { + tab.addEventListener('click', () => { + tabs.forEach(t => t.classList.remove('active')); + tab.classList.add('active'); + + document.querySelectorAll('.tab-content').forEach(content => { + content.classList.remove('active'); + }); + document.getElementById(tab.dataset.tab).classList.add('active'); + }); + }); + + // Handle built-in checkbox + const isBuiltinCheckbox = document.getElementById('isBuiltin'); + const nonBuiltinFields = document.querySelector('.non-builtin'); + + isBuiltinCheckbox.addEventListener('change', () => { + nonBuiltinFields.style.display = isBuiltinCheckbox.checked ? 'none' : 'block'; + }); + + // Environment variables handling + const envVarsContainer = document.getElementById('envVars'); + const addEnvVarBtn = document.getElementById('addEnvVar'); + + function createEnvVarInputs() { + const envVarDiv = document.createElement('div'); + envVarDiv.className = 'env-var'; + + const nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.placeholder = 'Variable Name'; + nameInput.className = 'env-name'; + + const descInput = document.createElement('input'); + descInput.type = 'text'; + descInput.placeholder = 'Description'; + descInput.className = 'env-desc'; + + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.textContent = 'Remove'; + removeBtn.onclick = () => envVarDiv.remove(); + + envVarDiv.appendChild(nameInput); + envVarDiv.appendChild(descInput); + envVarDiv.appendChild(removeBtn); + + return envVarDiv; + } + + addEnvVarBtn.addEventListener('click', () => { + envVarsContainer.appendChild(createEnvVarInputs()); + }); + + // Generate link from form + document.getElementById('installForm').addEventListener('submit', (e) => { + e.preventDefault(); + const formData = new FormData(e.target); + const server = { + is_builtin: formData.get('is_builtin') === 'on', + id: formData.get('id'), + name: formData.get('name'), + description: formData.get('description'), + command: formData.get('command'), + environmentVariables: [] + }; + + // Collect environment variables + document.querySelectorAll('.env-var').forEach(envVar => { + const name = envVar.querySelector('.env-name').value; + const description = envVar.querySelector('.env-desc').value; + if (name && description) { + server.environmentVariables.push({ + name, + description, + required: true + }); + } + }); + + const link = generateInstallLink(server); + displayGeneratedLink(link); + }); + + // Generate link from JSON + document.getElementById('generateFromJson').addEventListener('click', () => { + try { + const jsonInput = document.getElementById('jsonInput').value; + const server = JSON.parse(jsonInput); + const link = generateInstallLink(server); + displayGeneratedLink(link); + } catch (error) { + alert('Invalid JSON: ' + error.message); + } + }); + + // Link generation logic + function generateInstallLink(server) { + if (server.is_builtin) { + const queryParams = [ + 'cmd=goosed', + 'arg=mcp', + `arg=${encodeURIComponent(server.id)}`, + `description=${encodeURIComponent(server.id)}` + ].join('&'); + return `goose://extension?${queryParams}`; + } + + const parts = server.command.split(" "); + const baseCmd = parts[0]; + const args = parts.slice(1); + const queryParams = [ + `cmd=${encodeURIComponent(baseCmd)}`, + ...args.map((arg) => `arg=${encodeURIComponent(arg)}`), + `id=${encodeURIComponent(server.id)}`, + `name=${encodeURIComponent(server.name)}`, + `description=${encodeURIComponent(server.description)}`, + ...server.environmentVariables + .filter((env) => env.required) + .map( + (env) => `env=${encodeURIComponent(`${env.name}=${env.description}`)}` + ), + ].join("&"); + + return `goose://extension?${queryParams}`; + } + + function displayGeneratedLink(link) { + const linkElement = document.getElementById('generatedLink'); + linkElement.textContent = link; + } + + // Add sample JSON to the textarea + const sampleJson = { + is_builtin: false, + id: "example-extension", + name: "Example Extension", + description: "An example Goose extension", + command: "npx @gooseai/example-extension", + environmentVariables: [ + { + name: "API_KEY", + description: "Your API key", + required: true + } + ] + }; + document.getElementById('jsonInput').value = JSON.stringify(sampleJson, null, 2); +}); \ No newline at end of file diff --git a/ui/install-link-generator/styles.css b/ui/install-link-generator/styles.css new file mode 100644 index 00000000..b9638c49 --- /dev/null +++ b/ui/install-link-generator/styles.css @@ -0,0 +1,258 @@ +:root { + --primary: #ffffff; + --primary-dark: #e0e0e0; + --secondary: #333333; + --text: #ffffff; + --background: #000000; + --card-bg: #111111; + --success: #ffffff; + --error: #ff4b4b; + --border-color: rgba(255, 255, 255, 0.1); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; + transition: all 0.2s ease; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + line-height: 1.5; + padding: 16px; + background: var(--background); + color: var(--text); + min-height: 100vh; +} + +.container { + max-width: 600px; + margin: 0 auto; + background: var(--card-bg); + padding: 1.5rem; + border-radius: 12px; + border: 1px solid var(--border-color); +} + +h1 { + text-align: center; + margin-bottom: 1.5rem; + color: var(--text); + font-size: 1.75rem; + font-weight: 600; + letter-spacing: -0.5px; +} + +.tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; + padding: 0.25rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 8px; +} + +.tab-btn { + flex: 1; + padding: 0.75rem; + border: none; + background: transparent; + color: var(--text); + cursor: pointer; + border-radius: 6px; + font-weight: 500; + font-size: 0.875rem; +} + +.tab-btn.active { + background: var(--secondary); +} + +.tab-btn:hover:not(.active) { + background: rgba(255, 255, 255, 0.05); +} + +.tab-content { + display: none; + animation: fadeIn 0.2s ease; +} + +.tab-content.active { + display: block; +} + +.form-group { + margin-bottom: 1rem; +} + +label { + display: block; + margin-bottom: 0.25rem; + font-weight: 500; + color: var(--text); + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +input[type="text"], +textarea { + width: 100%; + padding: 0.75rem; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border-color); + border-radius: 6px; + color: var(--text); + font-size: 0.875rem; +} + +input[type="text"]:focus, +textarea:focus { + outline: none; + border-color: var(--primary); +} + +input[type="checkbox"] { + appearance: none; + width: 16px; + height: 16px; + border: 2px solid var(--border-color); + border-radius: 4px; + cursor: pointer; + position: relative; +} + +input[type="checkbox"]:checked { + background-color: var(--primary); + border-color: var(--primary); +} + +input[type="checkbox"]:checked::after { + content: "✓"; + position: absolute; + color: var(--background); + font-size: 12px; + left: 2px; + top: -2px; +} + +textarea { + resize: vertical; + min-height: 100px; +} + +button { + background: var(--secondary); + color: white; + padding: 0.75rem 1.5rem; + border: 1px solid var(--border-color); + border-radius: 6px; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + letter-spacing: 0.5px; +} + +button:hover { + background: #444444; +} + +button:active { + transform: translateY(1px); +} + +.result { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--border-color); +} + +#generatedLink { + padding: 1rem; + background: rgba(255, 255, 255, 0.05); + border-radius: 6px; + word-break: break-all; + font-family: 'SF Mono', 'Fira Code', monospace; + font-size: 0.8125rem; + border: 1px solid var(--border-color); +} + +.env-var { + display: flex; + gap: 0.5rem; + margin-bottom: 0.75rem; + align-items: center; +} + +.env-var input { + flex: 1; +} + +.env-var button { + padding: 0.75rem; + min-width: 40px; + background: #333333; +} + +#addEnvVar { + width: 100%; + margin-top: 0.75rem; + background: #333333; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb { + background: #444444; + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: #555555; +} + +/* Focus styles */ +*:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Responsive adjustments */ +@media (max-width: 640px) { + .container { + padding: 1rem; + } + + h1 { + font-size: 1.5rem; + } + + .tabs { + flex-direction: column; + } + + button { + width: 100%; + } +} \ No newline at end of file