Skip to main content
Glama
form-builder.html25.4 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Dynamic Form</title> <style> :root { --bg-primary: #0d1117; --bg-secondary: #161b22; --bg-tertiary: #21262d; --text-primary: #e6edf3; --text-secondary: #8b949e; --text-muted: #6e7681; --border-color: #30363d; --accent-blue: #58a6ff; --accent-green: #3fb950; --accent-red: #f85149; --accent-orange: #d29922; --input-bg: #0d1117; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--bg-primary); color: var(--text-primary); padding: 20px; line-height: 1.5; } .form-container { max-width: 600px; margin: 0 auto; } .form-header { margin-bottom: 24px; padding-bottom: 16px; border-bottom: 1px solid var(--border-color); } .form-title { font-size: 24px; font-weight: 600; margin-bottom: 8px; display: flex; align-items: center; gap: 10px; } .form-description { color: var(--text-secondary); font-size: 14px; } .form-section { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 12px; padding: 20px; margin-bottom: 16px; } .section-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; color: var(--text-primary); display: flex; align-items: center; gap: 8px; } .form-group { margin-bottom: 20px; } .form-group:last-child { margin-bottom: 0; } label { display: block; font-size: 14px; font-weight: 500; margin-bottom: 6px; color: var(--text-primary); } label .required { color: var(--accent-red); margin-left: 2px; } .label-hint { font-weight: 400; color: var(--text-muted); font-size: 12px; margin-left: 8px; } input[type="text"], input[type="email"], input[type="number"], input[type="tel"], input[type="date"], input[type="url"], textarea, select { width: 100%; padding: 10px 14px; font-size: 14px; background: var(--input-bg); border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-primary); transition: border-color 0.2s, box-shadow 0.2s; } input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent-blue); box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15); } input::placeholder, textarea::placeholder { color: var(--text-muted); } input.error, textarea.error, select.error { border-color: var(--accent-red); } .error-message { color: var(--accent-red); font-size: 12px; margin-top: 6px; display: none; } .error-message.visible { display: block; } textarea { min-height: 100px; resize: vertical; } select { cursor: pointer; } .radio-group, .checkbox-group { display: flex; flex-direction: column; gap: 10px; } .radio-option, .checkbox-option { display: flex; align-items: center; gap: 10px; cursor: pointer; } input[type="radio"], input[type="checkbox"] { width: 18px; height: 18px; accent-color: var(--accent-blue); cursor: pointer; } .radio-option span, .checkbox-option span { font-size: 14px; color: var(--text-secondary); } .slider-container { display: flex; align-items: center; gap: 16px; } input[type="range"] { flex: 1; height: 6px; -webkit-appearance: none; background: var(--bg-tertiary); border-radius: 3px; cursor: pointer; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: var(--accent-blue); border-radius: 50%; cursor: pointer; } .slider-value { min-width: 50px; text-align: center; font-weight: 600; color: var(--accent-blue); } .file-upload { border: 2px dashed var(--border-color); border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: border-color 0.2s, background 0.2s; } .file-upload:hover { border-color: var(--accent-blue); background: rgba(88, 166, 255, 0.05); } .file-upload-icon { font-size: 32px; margin-bottom: 8px; } .file-upload-text { color: var(--text-secondary); font-size: 14px; } .file-upload-hint { color: var(--text-muted); font-size: 12px; margin-top: 4px; } .form-actions { display: flex; gap: 12px; margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--border-color); } .btn { padding: 12px 24px; font-size: 14px; font-weight: 500; border: 1px solid var(--border-color); border-radius: 8px; cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 8px; } .btn-secondary { background: var(--bg-tertiary); color: var(--text-primary); } .btn-secondary:hover { background: var(--bg-secondary); border-color: var(--text-muted); } .btn-primary { background: var(--accent-blue); border-color: var(--accent-blue); color: #fff; flex: 1; justify-content: center; } .btn-primary:hover { background: #4c9aed; } .btn-primary:disabled { background: var(--bg-tertiary); border-color: var(--border-color); color: var(--text-muted); cursor: not-allowed; } .progress-bar { height: 4px; background: var(--bg-tertiary); border-radius: 2px; margin-bottom: 24px; overflow: hidden; } .progress-fill { height: 100%; background: var(--accent-blue); border-radius: 2px; transition: width 0.3s ease; } .progress-text { font-size: 12px; color: var(--text-muted); margin-bottom: 8px; } .success-message { background: rgba(63, 185, 80, 0.1); border: 1px solid var(--accent-green); border-radius: 8px; padding: 16px; display: none; align-items: center; gap: 12px; } .success-message.visible { display: flex; } .success-icon { font-size: 24px; } .success-text { color: var(--accent-green); font-weight: 500; } .loading { display: flex; align-items: center; justify-content: center; padding: 60px; color: var(--text-secondary); } @keyframes spin { to { transform: rotate(360deg); } } .spinner { width: 24px; height: 24px; border: 2px solid var(--border-color); border-top-color: var(--accent-blue); border-radius: 50%; animation: spin 0.8s linear infinite; margin-right: 12px; } /* Tags input */ .tags-container { display: flex; flex-wrap: wrap; gap: 8px; padding: 8px; background: var(--input-bg); border: 1px solid var(--border-color); border-radius: 8px; min-height: 44px; } .tag { display: inline-flex; align-items: center; gap: 6px; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 4px; padding: 4px 8px; font-size: 13px; } .tag-remove { cursor: pointer; color: var(--text-muted); font-size: 16px; line-height: 1; } .tag-remove:hover { color: var(--accent-red); } .tags-input { flex: 1; min-width: 100px; border: none; background: transparent; color: var(--text-primary); font-size: 14px; padding: 4px; } .tags-input:focus { outline: none; } </style> </head> <body> <div class="form-container"> <div id="loading" class="loading"> <div class="spinner"></div> Loading form... </div> <div id="form-content" style="display: none;"> <div class="form-header"> <h1 class="form-title"> <span id="form-icon">📝</span> <span id="form-title-text">Dynamic Form</span> </h1> <p class="form-description" id="form-description">Fill out the form below.</p> </div> <div class="progress-text"><span id="progress-count">0</span> of <span id="total-required">0</span> required fields completed</div> <div class="progress-bar"> <div class="progress-fill" id="progress-fill" style="width: 0%"></div> </div> <form id="dynamic-form"> <!-- Form sections will be generated here --> </form> <div class="success-message" id="success-message"> <span class="success-icon">✅</span> <span class="success-text">Form submitted successfully!</span> </div> <div class="form-actions"> <button type="button" class="btn btn-secondary" id="clear-btn"> 🗑️ Clear </button> <button type="submit" class="btn btn-primary" id="submit-btn" form="dynamic-form"> Submit Form </button> </div> </div> </div> <script type="module"> import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps"; // State let formConfig = null; let formData = {}; // Sample form to show immediately const sampleFormConfig = { icon: "📋", title: "Feedback Form", description: "Share your thoughts with us", sections: [ { title: "About You", icon: "👤", fields: [ { id: "name", type: "text", label: "Your Name", placeholder: "John Doe", required: true }, { id: "email", type: "email", label: "Email Address", placeholder: "john@example.com", required: true }, { id: "role", type: "select", label: "Your Role", required: true, options: [ { value: "developer", label: "Developer" }, { value: "designer", label: "Designer" }, { value: "manager", label: "Manager" }, { value: "other", label: "Other" } ]} ] }, { title: "Your Feedback", icon: "💬", fields: [ { id: "rating", type: "range", label: "Overall Rating (1-10)", min: 1, max: 10, default: 7 }, { id: "features", type: "checkbox", label: "What features do you use?", options: [ { value: "charts", label: "Data Visualization" }, { value: "forms", label: "Form Builder" }, { value: "monitor", label: "System Monitor" }, { value: "playground", label: "Code Playground" } ]}, { id: "feedback", type: "textarea", label: "Additional Comments", placeholder: "Tell us what you think...", rows: 4 } ] } ] }; // Initialize MCP App const app = new App({ appInfo: { name: "Dynamic Form", version: "1.0.0" }, appCapabilities: {} }); app.ontoolinput = (input) => { console.log("[Form] Tool input received:", input); }; app.ontoolresult = (result) => { console.log("[Form] Tool result received:", result); try { const content = result.content?.[0]; if (content?.type === 'text') { formConfig = JSON.parse(content.text); renderForm(); } } catch (e) { console.error("[Form] Failed to parse result:", e); } }; // Connect to host await app.connect(new PostMessageTransport(window.parent)); console.log("[Form] Connected to host"); // Render sample form immediately so users see something formConfig = sampleFormConfig; renderForm(); // Render the form based on config function renderForm() { if (!formConfig) return; document.getElementById('loading').style.display = 'none'; document.getElementById('form-content').style.display = 'block'; // Set header if (formConfig.icon) document.getElementById('form-icon').textContent = formConfig.icon; if (formConfig.title) document.getElementById('form-title-text').textContent = formConfig.title; if (formConfig.description) document.getElementById('form-description').textContent = formConfig.description; // Generate form fields const form = document.getElementById('dynamic-form'); form.innerHTML = ''; const sections = formConfig.sections || [{ fields: formConfig.fields || [] }]; sections.forEach((section, sectionIndex) => { const sectionEl = document.createElement('div'); sectionEl.className = 'form-section'; if (section.title) { sectionEl.innerHTML = ` <div class="section-title"> ${section.icon ? `<span>${section.icon}</span>` : ''} ${section.title} </div> `; } section.fields.forEach(field => { const fieldEl = createField(field); sectionEl.appendChild(fieldEl); }); form.appendChild(sectionEl); }); updateProgress(); notifySize(); } function createField(field) { const group = document.createElement('div'); group.className = 'form-group'; group.dataset.fieldId = field.id; const required = field.required ? '<span class="required">*</span>' : ''; const hint = field.hint ? `<span class="label-hint">${field.hint}</span>` : ''; let inputHtml = ''; switch (field.type) { case 'text': case 'email': case 'tel': case 'url': case 'number': case 'date': inputHtml = `<input type="${field.type}" id="${field.id}" name="${field.id}" placeholder="${field.placeholder || ''}" ${field.required ? 'required' : ''} ${field.min !== undefined ? `min="${field.min}"` : ''} ${field.max !== undefined ? `max="${field.max}"` : ''}>`; break; case 'textarea': inputHtml = `<textarea id="${field.id}" name="${field.id}" placeholder="${field.placeholder || ''}" ${field.required ? 'required' : ''} rows="${field.rows || 4}"></textarea>`; break; case 'select': const options = (field.options || []).map(opt => `<option value="${opt.value}">${opt.label}</option>` ).join(''); inputHtml = `<select id="${field.id}" name="${field.id}" ${field.required ? 'required' : ''}> <option value="">${field.placeholder || 'Select an option...'}</option> ${options} </select>`; break; case 'radio': const radios = (field.options || []).map(opt => ` <label class="radio-option"> <input type="radio" name="${field.id}" value="${opt.value}" ${field.required ? 'required' : ''}> <span>${opt.label}</span> </label> `).join(''); inputHtml = `<div class="radio-group">${radios}</div>`; break; case 'checkbox': if (field.options) { const checkboxes = field.options.map(opt => ` <label class="checkbox-option"> <input type="checkbox" name="${field.id}" value="${opt.value}"> <span>${opt.label}</span> </label> `).join(''); inputHtml = `<div class="checkbox-group">${checkboxes}</div>`; } else { inputHtml = ` <label class="checkbox-option"> <input type="checkbox" id="${field.id}" name="${field.id}"> <span>${field.checkboxLabel || 'Yes'}</span> </label> `; } break; case 'range': inputHtml = ` <div class="slider-container"> <input type="range" id="${field.id}" name="${field.id}" min="${field.min || 0}" max="${field.max || 100}" value="${field.default || 50}"> <span class="slider-value" id="${field.id}-value">${field.default || 50}</span> </div> `; break; case 'tags': inputHtml = ` <div class="tags-container" id="${field.id}-container"> <input type="text" class="tags-input" id="${field.id}" placeholder="${field.placeholder || 'Type and press Enter'}"> </div> `; break; default: inputHtml = `<input type="text" id="${field.id}" name="${field.id}">`; } group.innerHTML = ` <label for="${field.id}">${field.label}${required}${hint}</label> ${inputHtml} <div class="error-message" id="${field.id}-error">${field.errorMessage || 'This field is required'}</div> `; return group; } // Event handlers document.getElementById('dynamic-form').addEventListener('input', (e) => { const target = e.target; const name = target.name || target.id; if (target.type === 'range') { document.getElementById(`${name}-value`).textContent = target.value; } updateProgress(); clearError(name); }); document.getElementById('dynamic-form').addEventListener('change', (e) => { updateProgress(); }); // Tags input handling document.addEventListener('keydown', (e) => { if (e.target.classList.contains('tags-input') && e.key === 'Enter') { e.preventDefault(); const input = e.target; const container = input.parentElement; const value = input.value.trim(); if (value) { const tag = document.createElement('span'); tag.className = 'tag'; tag.innerHTML = `${value}<span class="tag-remove">×</span>`; tag.dataset.value = value; container.insertBefore(tag, input); input.value = ''; tag.querySelector('.tag-remove').addEventListener('click', () => { tag.remove(); updateProgress(); }); updateProgress(); } } }); document.getElementById('dynamic-form').addEventListener('submit', async (e) => { e.preventDefault(); if (!validateForm()) return; const data = collectFormData(); console.log("[Form] Submitting data:", data); // Send to chat app.sendMessage('user', [ { type: 'text', text: formatSubmission(data) } ]); // Show success document.getElementById('success-message').classList.add('visible'); document.getElementById('submit-btn').disabled = true; setTimeout(() => { document.getElementById('success-message').classList.remove('visible'); document.getElementById('submit-btn').disabled = false; }, 3000); }); document.getElementById('clear-btn').addEventListener('click', () => { document.getElementById('dynamic-form').reset(); // Clear tags document.querySelectorAll('.tag').forEach(tag => tag.remove()); // Reset sliders document.querySelectorAll('input[type="range"]').forEach(slider => { const defaultVal = slider.getAttribute('value') || 50; slider.value = defaultVal; document.getElementById(`${slider.id}-value`).textContent = defaultVal; }); updateProgress(); }); function validateForm() { let isValid = true; const form = document.getElementById('dynamic-form'); if (!formConfig) return false; const allFields = (formConfig.sections || [{ fields: formConfig.fields || [] }]) .flatMap(s => s.fields); allFields.forEach(field => { if (field.required) { const input = form.querySelector(`[name="${field.id}"]`) || form.querySelector(`#${field.id}`); if (!input) return; let hasValue = false; if (field.type === 'checkbox' && field.options) { hasValue = form.querySelectorAll(`[name="${field.id}"]:checked`).length > 0; } else if (field.type === 'radio') { hasValue = form.querySelector(`[name="${field.id}"]:checked`) !== null; } else if (field.type === 'tags') { hasValue = form.querySelectorAll(`#${field.id}-container .tag`).length > 0; } else { hasValue = input.value.trim() !== ''; } if (!hasValue) { isValid = false; showError(field.id); } } }); return isValid; } function showError(fieldId) { const input = document.querySelector(`[name="${fieldId}"]`) || document.querySelector(`#${fieldId}`); const error = document.getElementById(`${fieldId}-error`); if (input) input.classList.add('error'); if (error) error.classList.add('visible'); } function clearError(fieldId) { const input = document.querySelector(`[name="${fieldId}"]`) || document.querySelector(`#${fieldId}`); const error = document.getElementById(`${fieldId}-error`); if (input) input.classList.remove('error'); if (error) error.classList.remove('visible'); } function collectFormData() { const form = document.getElementById('dynamic-form'); const data = {}; if (!formConfig) return data; const allFields = (formConfig.sections || [{ fields: formConfig.fields || [] }]) .flatMap(s => s.fields); allFields.forEach(field => { if (field.type === 'checkbox' && field.options) { const checked = form.querySelectorAll(`[name="${field.id}"]:checked`); data[field.id] = Array.from(checked).map(c => c.value); } else if (field.type === 'radio') { const checked = form.querySelector(`[name="${field.id}"]:checked`); data[field.id] = checked ? checked.value : null; } else if (field.type === 'tags') { const tags = form.querySelectorAll(`#${field.id}-container .tag`); data[field.id] = Array.from(tags).map(t => t.dataset.value); } else if (field.type === 'checkbox') { const input = form.querySelector(`#${field.id}`); data[field.id] = input ? input.checked : false; } else { const input = form.querySelector(`#${field.id}`); data[field.id] = input ? input.value : ''; } }); return data; } function formatSubmission(data) { let text = `📋 **Form Submission: ${formConfig?.title || 'Form'}**\n\n`; const allFields = (formConfig.sections || [{ fields: formConfig.fields || [] }]) .flatMap(s => s.fields); allFields.forEach(field => { const value = data[field.id]; let displayValue = value; if (Array.isArray(value)) { displayValue = value.length > 0 ? value.join(', ') : '(none)'; } else if (typeof value === 'boolean') { displayValue = value ? 'Yes' : 'No'; } else if (!value && value !== 0) { displayValue = '(not provided)'; } text += `**${field.label}:** ${displayValue}\n`; }); return text; } function updateProgress() { if (!formConfig) return; const allFields = (formConfig.sections || [{ fields: formConfig.fields || [] }]) .flatMap(s => s.fields); const requiredFields = allFields.filter(f => f.required); const form = document.getElementById('dynamic-form'); let completed = 0; requiredFields.forEach(field => { const input = form.querySelector(`[name="${field.id}"]`) || form.querySelector(`#${field.id}`); if (!input) return; let hasValue = false; if (field.type === 'checkbox' && field.options) { hasValue = form.querySelectorAll(`[name="${field.id}"]:checked`).length > 0; } else if (field.type === 'radio') { hasValue = form.querySelector(`[name="${field.id}"]:checked`) !== null; } else if (field.type === 'tags') { hasValue = form.querySelectorAll(`#${field.id}-container .tag`).length > 0; } else { hasValue = input.value.trim() !== ''; } if (hasValue) completed++; }); const total = requiredFields.length; const percent = total > 0 ? (completed / total) * 100 : 100; document.getElementById('progress-count').textContent = completed; document.getElementById('total-required').textContent = total; document.getElementById('progress-fill').style.width = `${percent}%`; } function notifySize() { const height = document.body.scrollHeight; app.sendSizeChanged(undefined, height); } // Observe size changes const observer = new ResizeObserver(notifySize); observer.observe(document.body); </script> </body> </html>

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jamesdowzard/mcp-apps-poc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server