feat(ui): Add individual forms for each connector

This commit is contained in:
Richard Palethorpe
2025-03-21 14:20:27 +00:00
parent 5f2a2eaa24
commit 84836b8345
6 changed files with 852 additions and 171 deletions

View File

@@ -57,37 +57,68 @@ const AgentFormUtils = {
// Process connectors from form
processConnectors: function(button) {
const connectors = [];
const connectorsElements = document.getElementsByClassName('connector');
const connectorElements = document.querySelectorAll('.connector');
for (let i = 0; i < connectorsElements.length; i++) {
const typeField = document.getElementById(`connectorType${i}`);
const configField = document.getElementById(`connectorConfig${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;
}
return null; // Indicate validation error
}
for (let i = 0; i < connectorElements.length; i++) {
const typeSelect = document.getElementById(`connectorType${i}`);
if (!typeSelect) {
showToast(`Error: Could not find connector type select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const type = typeSelect.value;
if (!type) {
showToast(`Please select a connector type for connector ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
// Get all config fields for this connector
const connector = {
type: type,
config: {}
};
// Find all config inputs for this connector
const configInputs = document.querySelectorAll(`[name^="connectors[${i}][config]"]`);
// Check if we have a JSON textarea (fallback template)
const jsonTextarea = document.getElementById(`connectorConfig${i}`);
if (jsonTextarea && jsonTextarea.value) {
try {
// If it's a JSON textarea, parse it and use the result
const jsonConfig = JSON.parse(jsonTextarea.value);
// Convert the parsed JSON back to a string for the backend
connector.config = JSON.stringify(jsonConfig);
} catch (e) {
// If it's not valid JSON, use it as is
connector.config = jsonTextarea.value;
}
} else {
// Process individual form fields
configInputs.forEach(input => {
// Extract the key from the name attribute
// Format: connectors[0][config][key]
const keyMatch = input.name.match(/\[config\]\[([^\]]+)\]/);
if (keyMatch && keyMatch[1]) {
const key = keyMatch[1];
// For checkboxes, set true/false based on checked state
if (input.type === 'checkbox') {
connector.config[key] = input.checked ? 'true' : 'false';
} else {
connector.config[key] = input.value;
}
}
});
// Convert the config object to a JSON string for the backend
connector.config = JSON.stringify(connector.config);
}
connectors.push(connector);
}
return connectors;
@@ -96,17 +127,23 @@ const AgentFormUtils = {
// Process MCP servers from form
processMCPServers: function() {
const mcpServers = [];
const mcpServerElements = document.getElementsByClassName('mcp_server');
const mcpElements = document.querySelectorAll('.mcp_server');
for (let i = 0; i < mcpServerElements.length; i++) {
const urlField = document.getElementById(`mcpURL${i}`);
const tokenField = document.getElementById(`mcpToken${i}`);
for (let i = 0; i < mcpElements.length; i++) {
const urlInput = document.getElementById(`mcpURL${i}`);
const tokenInput = document.getElementById(`mcpToken${i}`);
if (urlField && urlField.value.trim()) {
mcpServers.push({
url: urlField.value.trim(),
token: tokenField ? tokenField.value.trim() : ''
});
if (urlInput && urlInput.value) {
const server = {
url: urlInput.value
};
// Add token if present
if (tokenInput && tokenInput.value) {
server.token = tokenInput.value;
}
mcpServers.push(server);
}
}
@@ -116,37 +153,43 @@ const AgentFormUtils = {
// Process actions from form
processActions: function(button) {
const actions = [];
const actionElements = document.getElementsByClassName('action');
const actionElements = document.querySelectorAll('.action');
for (let i = 0; i < actionElements.length; i++) {
const nameField = document.getElementById(`actionsName${i}`);
const configField = document.getElementById(`actionsConfig${i}`);
const nameSelect = document.getElementById(`actionsName${i}`);
const configTextarea = document.getElementById(`actionsConfig${i}`);
if (nameField && configField) {
if (!nameSelect) {
showToast(`Error: Could not find action name select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const name = nameSelect.value;
if (!name) {
showToast(`Please select an action type for action ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
let config = {};
if (configTextarea && configTextarea.value) {
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);
actions.push({
name: nameField.value,
config: configValue // Send the raw string, not parsed JSON
});
} catch (err) {
console.error(`Error parsing action ${i} config:`, err);
showToast(`Error in action ${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;
}
return null; // Indicate validation error
config = JSON.parse(configTextarea.value);
} catch (e) {
showToast(`Invalid JSON in action ${i+1} config: ${e.message}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
}
actions.push({
name: name,
config: JSON.stringify(config) // Convert to JSON string for backend
});
}
return actions;
@@ -155,37 +198,43 @@ const AgentFormUtils = {
// Process prompt blocks from form
processPromptBlocks: function(button) {
const promptBlocks = [];
const promptBlockElements = document.getElementsByClassName('promptBlock');
const promptElements = document.querySelectorAll('.prompt_block');
for (let i = 0; i < promptBlockElements.length; i++) {
const nameField = document.getElementById(`promptName${i}`);
const configField = document.getElementById(`promptConfig${i}`);
for (let i = 0; i < promptElements.length; i++) {
const nameSelect = document.getElementById(`promptName${i}`);
const configTextarea = document.getElementById(`promptConfig${i}`);
if (nameField && configField) {
if (!nameSelect) {
showToast(`Error: Could not find prompt block name select for index ${i}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
const name = nameSelect.value;
if (!name) {
showToast(`Please select a prompt block type for block ${i+1}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
let config = {};
if (configTextarea && configTextarea.value) {
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);
promptBlocks.push({
name: nameField.value,
config: configValue // Send the raw string, not parsed JSON
});
} catch (err) {
console.error(`Error parsing prompt block ${i} config:`, err);
showToast(`Error in prompt block ${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;
}
return null; // Indicate validation error
config = JSON.parse(configTextarea.value);
} catch (e) {
showToast(`Invalid JSON in prompt block ${i+1} config: ${e.message}`, 'error');
button.innerHTML = button.getAttribute('data-original-text');
button.disabled = false;
return null; // Validation failed
}
}
promptBlocks.push({
name: name,
config: JSON.stringify(config) // Convert to JSON string for backend
});
}
return promptBlocks;
@@ -193,38 +242,126 @@ const AgentFormUtils = {
// Helper function to format config values (for edit form)
formatConfigValue: function(configElement, configValue) {
// If it's a string (already stringified JSON), try to parse it first
if (typeof configValue === 'string') {
if (!configElement) return;
// If configValue is an object, stringify it
if (typeof configValue === 'object' && configValue !== null) {
try {
configElement.value = JSON.stringify(configValue, null, 2);
} catch (e) {
console.error('Error stringifying config value:', e);
configElement.value = '{}';
}
}
// If it's a string that looks like JSON, try to parse and pretty print it
else if (typeof configValue === 'string' && (configValue.startsWith('{') || configValue.startsWith('['))) {
try {
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 it's not valid JSON, just use the string as is
configElement.value = configValue;
}
} else if (configValue !== undefined && configValue !== null) {
// Direct object, just stringify with formatting
configElement.value = JSON.stringify(configValue, null, 2);
} else {
// Default to empty object
configElement.value = '{}';
}
// Otherwise, just use the value as is
else {
configElement.value = configValue || '';
}
},
// 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);
if (!selectElement) return;
// Check if the option exists
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;
}
},
// Render connector form based on type
renderConnectorForm: function(index, type, config = {}) {
const formContainer = document.getElementById(`connectorFormContainer${index}`);
if (!formContainer) return;
// Clear existing form
formContainer.innerHTML = '';
// Debug log to see what's happening
console.log(`Rendering connector form for type: ${type}`);
console.log(`Config for connector:`, config);
console.log(`Available templates:`, ConnectorTemplates ? Object.keys(ConnectorTemplates) : 'None');
// Ensure config is an object
let configObj = config;
if (typeof config === 'string') {
try {
configObj = JSON.parse(config);
} catch (e) {
console.error('Error parsing connector config string:', e);
configObj = {};
}
}
// If we have a template for this connector type in the global ConnectorTemplates object
if (ConnectorTemplates && type && ConnectorTemplates[type]) {
console.log(`Found template for ${type}`);
// Get the template result which contains HTML and setValues function
const templateResult = ConnectorTemplates[type](configObj, index);
// Set the HTML content
formContainer.innerHTML = templateResult.html;
// Call the setValues function to set input values safely
if (typeof templateResult.setValues === 'function') {
setTimeout(templateResult.setValues, 0);
}
} else {
console.log(`No template found for ${type}, using fallback`);
// Use the fallback template
if (ConnectorTemplates && ConnectorTemplates.fallback) {
const fallbackResult = ConnectorTemplates.fallback(configObj, index);
formContainer.innerHTML = fallbackResult.html;
if (typeof fallbackResult.setValues === 'function') {
setTimeout(fallbackResult.setValues, 0);
}
} else {
// Fallback to generic JSON textarea if no fallback template
formContainer.innerHTML = `
<div class="form-group">
<label for="connectorConfig${index}">Connector Config (JSON)</label>
<textarea id="connectorConfig${index}"
name="connectors[${index}][config]"
class="form-control"
placeholder='{"key":"value"}'></textarea>
</div>
`;
// Set the value safely after DOM is created
setTimeout(function() {
const configTextarea = document.getElementById(`connectorConfig${index}`);
if (configTextarea) {
if (typeof configObj === 'object' && configObj !== null) {
configTextarea.value = JSON.stringify(configObj, null, 2);
} else if (typeof config === 'string') {
configTextarea.value = config;
}
}
}, 0);
}
}
}
};
@@ -238,14 +375,23 @@ const AgentFormTemplates = {
<h2>Connector ${index + 1}</h2>
<div class="mb-4">
<label for="connectorType${index}">Connector Type</label>
<select name="connectors[${index}].type" id="connectorType${index}">
<select name="connectors[${index}][type]"
id="connectorType${index}"
class="form-control"
onchange="AgentFormUtils.renderConnectorForm(${index}, this.value)">
<option value="">Select Connector Type</option>
${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="connectorFormContainer${index}">
<!-- Connector form will be dynamically inserted here -->
<div class="form-group">
<div class="placeholder-text">Select a connector type to configure</div>
</div>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.connector').remove()">
<i class="fas fa-trash"></i> Remove Connector
</button>
</div>
`;
},
@@ -256,13 +402,16 @@ 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>
<button type="button" class="remove-btn" onclick="this.closest('.mcp_server').remove()">
<i class="fas fa-trash"></i> Remove Server
</button>
</div>
`;
},
@@ -273,15 +422,18 @@ 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>
<select name="actions[${index}].name" id="actionsName${index}">
<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='{"key":"value"}'>{}</textarea>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.action').remove()">
<i class="fas fa-trash"></i> Remove Action
</button>
</div>
`;
},
@@ -289,18 +441,21 @@ const AgentFormTemplates = {
// Prompt Block template
promptBlockTemplate: function(index, data) {
return `
<div class="promptBlock mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<div class="prompt_block 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>
<select name="promptblocks[${index}].name" id="promptName${index}">
<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='{"key":"value"}'>{}</textarea>
</div>
<button type="button" class="remove-btn" onclick="this.closest('.prompt_block').remove()">
<i class="fas fa-trash"></i> Remove Prompt Block
</button>
</div>
`;
}
@@ -308,61 +463,102 @@ const AgentFormTemplates = {
// Initialize form event listeners
function initAgentFormCommon(options = {}) {
// Setup event listeners for dynamic component buttons
if (options.enableConnectors !== false) {
document.getElementById('addConnectorButton').addEventListener('click', function() {
// Add connector button
const addConnectorButton = document.getElementById('addConnectorButton');
if (addConnectorButton) {
addConnectorButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.connectors) {
optionsHtml = options.connectors;
}
// Add new connector form
AgentFormUtils.addDynamicComponent('connectorsSection', AgentFormTemplates.connectorTemplate, {
className: 'connector',
options: options.connectors || ''
options: optionsHtml
});
});
}
if (options.enableMCP !== false) {
document.getElementById('addMCPButton').addEventListener('click', function() {
// Add MCP server button
const addMCPButton = document.getElementById('addMCPButton');
if (addMCPButton) {
addMCPButton.addEventListener('click', function() {
// Add new MCP server form
AgentFormUtils.addDynamicComponent('mcpSection', AgentFormTemplates.mcpServerTemplate, {
className: 'mcp_server'
});
});
}
if (options.enableActions !== false) {
document.getElementById('action_button').addEventListener('click', function() {
// Add action button
const actionButton = document.getElementById('action_button');
if (actionButton) {
actionButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.actions) {
optionsHtml = options.actions;
}
// Add new action form
AgentFormUtils.addDynamicComponent('action_box', AgentFormTemplates.actionTemplate, {
className: 'action',
options: options.actions || ''
options: optionsHtml
});
});
}
if (options.enablePromptBlocks !== false) {
document.getElementById('dynamic_button').addEventListener('click', function() {
// Add prompt block button
const dynamicButton = document.getElementById('dynamic_button');
if (dynamicButton) {
dynamicButton.addEventListener('click', function() {
// Create options string
let optionsHtml = '';
if (options.promptBlocks) {
optionsHtml = options.promptBlocks;
}
// Add new prompt block form
AgentFormUtils.addDynamicComponent('dynamic_box', AgentFormTemplates.promptBlockTemplate, {
className: 'promptBlock',
options: options.promptBlocks || ''
className: 'prompt_block',
options: optionsHtml
});
});
}
}
// Simple toast notification function
function showToast(message, type) {
// Check if toast container exists, if not create it
let toast = document.getElementById('toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'toast';
toast.className = 'toast';
const toastMessage = document.createElement('div');
toastMessage.id = 'toast-message';
toast.appendChild(toastMessage);
document.body.appendChild(toast);
}
// Add additional CSS for checkbox labels
const style = document.createElement('style');
style.textContent = `
.checkbox-label {
display: flex;
align-items: center;
cursor: pointer;
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);
}
const toastMessage = document.getElementById('toast-message');
// Set message
toastMessage.textContent = message;
// Set type class
toast.className = 'toast';
toast.classList.add(`toast-${type}`);
// Show toast
toast.classList.add('show');
// Hide after 3 seconds
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}