Generate connector form based on meta-data (#62)

* Ignore volumes and exe

* Export form meta-data

* use dynamic metaform for connectors

* fix populating form
This commit is contained in:
Richard Palethorpe
2025-03-20 15:00:37 +00:00
committed by GitHub
parent 43a46ad1fb
commit d7cfa7f0b2
15 changed files with 630 additions and 78 deletions

View File

@@ -1,15 +1,39 @@
// Common utility functions for agent forms
const AgentFormUtils = {
// Add dynamic component based on template
addDynamicComponent: function(sectionId, templateFunction, dataItems) {
addDynamicComponent: function(sectionId, templateFunction, options = {}) {
const section = document.getElementById(sectionId);
const newIndex = section.getElementsByClassName(dataItems.className).length;
if (!section) return;
// Generate HTML from template function
const newHtml = templateFunction(newIndex, dataItems);
const index = section.getElementsByClassName(options.className || 'dynamic-component').length;
const templateData = { index, ...options };
// Add to DOM
section.insertAdjacentHTML('beforeend', newHtml);
// Create a new element from the template
const tempDiv = document.createElement('div');
tempDiv.innerHTML = templateFunction(index, templateData);
const newElement = tempDiv.firstElementChild;
// Add the new element to the section
section.appendChild(newElement);
// If it's a connector, add event listener for type change
if (options.className === 'connector') {
const newIndex = index;
const connectorType = document.getElementById(`connectorType${newIndex}`);
if (connectorType) {
// Add event listener for future changes
connectorType.addEventListener('change', function() {
loadConnectorForm(newIndex, this.value, null);
});
// If a connector type is already selected (default value), load its form immediately
if (connectorType.value) {
loadConnectorForm(newIndex, connectorType.value, null);
}
}
}
return newElement;
},
// Process form data into JSON structure
@@ -60,32 +84,57 @@ const AgentFormUtils = {
const connectorsElements = document.getElementsByClassName('connector');
for (let i = 0; i < connectorsElements.length; i++) {
const typeField = document.getElementById(`connectorType${i}`);
const configField = document.getElementById(`connectorConfig${i}`);
const typeSelect = document.getElementById(`connectorType${i}`);
if (typeField && configField) {
try {
// Validate JSON but send as string
const configValue = configField.value.trim() || '{}';
// Parse to validate but don't use the parsed object
JSON.parse(configValue);
connectors.push({
type: typeField.value,
config: configValue // Send the raw string, not parsed JSON
});
} catch (err) {
console.error(`Error parsing connector ${i} config:`, err);
showToast(`Error in connector ${i+1} configuration: Invalid JSON`, 'error');
// If button is provided, restore its state
if (button) {
const originalButtonText = button.getAttribute('data-original-text');
button.innerHTML = originalButtonText;
button.disabled = false;
if (typeSelect) {
const connectorType = typeSelect.value;
const configContainer = document.getElementById(`connectorConfigContainer${i}`);
// Only process if we have a metaform
if (configContainer && configContainer.querySelector('.metaform')) {
try {
// Get all form fields
const fields = configContainer.querySelectorAll('.connector-field');
let configObj = {};
// Process each field based on its type
fields.forEach(field => {
const fieldName = field.dataset.fieldName;
const fieldType = field.dataset.fieldType;
// Convert value based on field type
let value = field.value;
if (fieldType === 'number' && value !== '') {
value = parseFloat(value);
}
configObj[fieldName] = value;
});
// Add the connector to the list
connectors.push({
type: connectorType,
config: JSON.stringify(configObj)
});
} catch (err) {
console.error(`Error processing connector ${i} form:`, err);
showToast(`Error in connector ${i+1} configuration`, 'error');
// If button is provided, restore its state
if (button) {
const originalButtonText = button.getAttribute('data-original-text');
button.innerHTML = originalButtonText;
button.disabled = false;
}
return null; // Indicate validation error
}
return null; // Indicate validation error
} else {
// If no form is loaded, create an empty config
connectors.push({
type: connectorType,
config: '{}'
});
}
}
}
@@ -199,14 +248,16 @@ const AgentFormUtils = {
const parsed = JSON.parse(configValue);
configElement.value = JSON.stringify(parsed, null, 2);
} catch (e) {
console.warn("Failed to parse config JSON string:", e);
configElement.value = configValue; // Keep as is if parsing fails
// If parsing fails, use the raw string
configElement.value = configValue;
}
} else if (configValue !== undefined && configValue !== null) {
// Direct object, just stringify with formatting
}
// If it's already an object, stringify it
else if (typeof configValue === 'object' && configValue !== null) {
configElement.value = JSON.stringify(configValue, null, 2);
} else {
// Default to empty object
}
// Default to empty object
else {
configElement.value = '{}';
}
},
@@ -214,21 +265,90 @@ const AgentFormUtils = {
// Helper function to set select value (with fallback if option doesn't exist)
setSelectValue: function(selectElement, value) {
// Check if the option exists
const optionExists = Array.from(selectElement.options).some(option => option.value === value);
let optionExists = false;
for (let i = 0; i < selectElement.options.length; i++) {
if (selectElement.options[i].value === value) {
optionExists = true;
break;
}
}
// Set the value if the option exists
if (optionExists) {
selectElement.value = value;
} else if (value) {
// If value is provided but option doesn't exist, create a new option
const newOption = document.createElement('option');
newOption.value = value;
newOption.text = value + ' (custom)';
selectElement.add(newOption);
selectElement.value = value;
} else if (selectElement.options.length > 0) {
// Otherwise select the first option
selectElement.selectedIndex = 0;
}
}
};
// Function to load connector form based on type
function loadConnectorForm(index, connectorType, configData) {
if (!connectorType) return;
const configContainer = document.getElementById(`connectorConfigContainer${index}`);
if (!configContainer) return;
// Show loading indicator
configContainer.innerHTML = '<div class="loading-spinner">Loading form...</div>';
// Fetch the form for the selected connector type
fetch(`/settings/connector/form/${connectorType}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to load connector form');
}
return response.text();
})
.then(html => {
// Replace the container content with the form
configContainer.innerHTML = html;
// Store the connector type as a data attribute on the form
const metaform = configContainer.querySelector('.metaform');
if (metaform) {
metaform.setAttribute('data-connector-type', connectorType);
// Add a hidden input to store the connector type
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'connector-type';
hiddenInput.value = connectorType;
metaform.appendChild(hiddenInput);
// If we have config data, populate the form fields
if (configData) {
try {
// Parse the config JSON
const parsedConfig = JSON.parse(configData);
// Find all form fields
const fields = metaform.querySelectorAll('.connector-field');
// Populate each field with the corresponding value from the config
fields.forEach(field => {
const fieldName = field.dataset.fieldName;
if (parsedConfig[fieldName] !== undefined) {
field.value = parsedConfig[fieldName];
}
});
} catch (error) {
console.warn(`Failed to populate connector form for ${connectorType}:`, error);
}
}
}
})
.catch(error => {
console.error('Error loading connector form:', error);
configContainer.innerHTML = `
<div class="error-message">
<p>Failed to load connector form: ${error.message}</p>
</div>
`;
});
}
// HTML Templates for dynamic elements
const AgentFormTemplates = {
// Connector template
@@ -242,9 +362,10 @@ const AgentFormTemplates = {
${data.options}
</select>
</div>
<div class="mb-4">
<label for="connectorConfig${index}">Connector Config (JSON)</label>
<textarea id="connectorConfig${index}" name="connectors[${index}].config" placeholder='{"token":"sk-mg3.."}'>{}</textarea>
<div id="connectorConfigContainer${index}" class="mb-4">
<div class="text-center py-4">
<p>Select a connector type to load its configuration form.</p>
</div>
</div>
</div>
`;
@@ -256,12 +377,12 @@ const AgentFormTemplates = {
<div class="mcp_server mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>MCP Server ${index + 1}</h2>
<div class="mb-4">
<label for="mcpURL${index}">MCP Server URL</label>
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}].url" placeholder='https://...'>
<label for="mcpURL${index}">Server URL</label>
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}].url" placeholder="https://example.com">
</div>
<div class="mb-4">
<label for="mcpToken${index}">Bearer Token</label>
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}].token" placeholder='Bearer token'>
<label for="mcpToken${index}">API Token (Optional)</label>
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}].token" placeholder="API token">
</div>
</div>
`;
@@ -273,14 +394,14 @@ const AgentFormTemplates = {
<div class="action mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Action ${index + 1}</h2>
<div class="mb-4">
<label for="actionsName${index}">Action</label>
<label for="actionsName${index}">Action Type</label>
<select name="actions[${index}].name" id="actionsName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="actionsConfig${index}">Action Config (JSON)</label>
<textarea id="actionsConfig${index}" name="actions[${index}].config" placeholder='{"results":"5"}'>{}</textarea>
<textarea id="actionsConfig${index}" name="actions[${index}].config" placeholder='{"param":"value"}'>{}</textarea>
</div>
</div>
`;
@@ -292,14 +413,14 @@ const AgentFormTemplates = {
<div class="promptBlock mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Prompt Block ${index + 1}</h2>
<div class="mb-4">
<label for="promptName${index}">Block Prompt</label>
<label for="promptName${index}">Prompt Block Type</label>
<select name="promptblocks[${index}].name" id="promptName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="promptConfig${index}">Prompt Config (JSON)</label>
<textarea id="promptConfig${index}" name="promptblocks[${index}].config" placeholder='{"results":"5"}'>{}</textarea>
<label for="promptConfig${index}">Prompt Block Config (JSON)</label>
<textarea id="promptConfig${index}" name="promptblocks[${index}].config" placeholder='{"param":"value"}'>{}</textarea>
</div>
</div>
`;
@@ -344,25 +465,39 @@ function initAgentFormCommon(options = {}) {
});
}
// Add additional CSS for checkbox labels
// Add additional CSS for loading spinner and error messages
const style = document.createElement('style');
style.textContent = `
.checkbox-label {
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
height: 100px;
color: #f0f0f0;
}
.loading-spinner::after {
content: '';
width: 20px;
height: 20px;
border: 2px solid #f0f0f0;
border-top-color: transparent;
border-radius: 50%;
animation: spinner 1s linear infinite;
margin-left: 10px;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
.error-message {
color: #ff5555;
padding: 10px;
border: 1px solid #ff5555;
border-radius: 4px;
margin-bottom: 10px;
}
.checkbox-label .checkbox-custom {
margin-right: 10px;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
`;
document.head.appendChild(style);
}