From d7cfa7f0b2c9fe1eb21d91a78a06a9cd88b52f9a Mon Sep 17 00:00:00 2001 From: Richard Palethorpe Date: Thu, 20 Mar 2025 15:00:37 +0000 Subject: [PATCH] Generate connector form based on meta-data (#62) * Ignore volumes and exe * Export form meta-data * use dynamic metaform for connectors * fix populating form --- .gitignore | 4 +- core/state/config.go | 2 + pkg/metaform/metaform.go | 28 +++ services/connectors/discord.go | 22 +++ services/connectors/githubissue.go | 46 +++++ services/connectors/githubpr.go | 46 +++++ services/connectors/irc.go | 46 +++++ services/connectors/slack.go | 39 +++++ services/connectors/telegram.go | 15 ++ services/connectors/twitter.go | 32 ++++ webui/app.go | 41 +++++ webui/public/js/agent-form.js | 273 +++++++++++++++++++++-------- webui/routes.go | 1 + webui/views/partials/metaform.html | 94 ++++++++++ webui/views/settings.html | 19 +- 15 files changed, 630 insertions(+), 78 deletions(-) create mode 100644 pkg/metaform/metaform.go create mode 100644 webui/views/partials/metaform.html diff --git a/.gitignore b/.gitignore index 659f466..1574d82 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ models/ data/ pool uploads/ +volumes/ local-agent-framework -localagent \ No newline at end of file +localagent +LocalAgent diff --git a/core/state/config.go b/core/state/config.go index e1692c3..64aebcf 100644 --- a/core/state/config.go +++ b/core/state/config.go @@ -4,6 +4,7 @@ import ( "encoding/json" "github.com/mudler/LocalAgent/core/agent" + "github.com/mudler/LocalAgent/pkg/metaform" ) type ConnectorConfig struct { @@ -62,5 +63,6 @@ type AgentConfig struct { type Connector interface { AgentResultCallback() func(state agent.ActionState) AgentReasoningCallback() func(state agent.ActionCurrentState) bool + ConfigForm() metaform.Form Start(a *agent.Agent) } diff --git a/pkg/metaform/metaform.go b/pkg/metaform/metaform.go new file mode 100644 index 0000000..1f9a66e --- /dev/null +++ b/pkg/metaform/metaform.go @@ -0,0 +1,28 @@ +package metaform + +// Option represents a selectable option for FieldOption type +type Option struct { + Value string `json:"value"` + Label string `json:"label"` +} + +type FieldKind string + +const ( + FieldString FieldKind = "string" + FieldNumber FieldKind = "number" + FieldOptions FieldKind = "options" +) + +type Field struct { + Kind FieldKind `json:"kind"` + Name string `json:"name"` + Label string `json:"label"` + Required bool `json:"required"` + Placeholder string `json:"placeholder,omitempty"` + Options []Option `json:"options,omitempty"` +} + +type Form struct { + Fields []Field +} diff --git a/services/connectors/discord.go b/services/connectors/discord.go index 8365058..c833c25 100644 --- a/services/connectors/discord.go +++ b/services/connectors/discord.go @@ -5,6 +5,7 @@ import ( "github.com/bwmarrin/discordgo" "github.com/mudler/LocalAgent/core/agent" + "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" ) @@ -37,6 +38,27 @@ func (d *Discord) AgentReasoningCallback() func(state agent.ActionCurrentState) } } +func (d *Discord) ConfigForm() metaform.Form { + return metaform.Form{ + Fields: []metaform.Field{ + { + Kind: metaform.FieldString, + Name: "token", + Label: "Bot Token", + Required: true, + Placeholder: "Your Discord bot token", + }, + { + Kind: metaform.FieldString, + Name: "defaultChannel", + Label: "Default Channel", + Required: true, + Placeholder: "The default channel for the bot to join", + }, + }, + } +} + func (d *Discord) Start(a *agent.Agent) { Token := d.token diff --git a/services/connectors/githubissue.go b/services/connectors/githubissue.go index bd6506e..750cbce 100644 --- a/services/connectors/githubissue.go +++ b/services/connectors/githubissue.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-github/v69/github" "github.com/mudler/LocalAgent/core/agent" + "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/sashabaranov/go-openai" @@ -63,6 +64,51 @@ func (g *GithubIssues) AgentReasoningCallback() func(state agent.ActionCurrentSt } } +func (g *GithubIssues) ConfigForm() metaform.Form { + return metaform.Form{ + Fields: []metaform.Field{ + { + Kind: metaform.FieldString, + Name: "token", + Label: "GitHub Token", + Required: true, + Placeholder: "Your GitHub personal access token", + }, + { + Kind: metaform.FieldString, + Name: "owner", + Label: "Repository Owner", + Required: true, + Placeholder: "username or organization", + }, + { + Kind: metaform.FieldString, + Name: "repository", + Label: "Repository Name", + Required: true, + Placeholder: "repo-name", + }, + { + Kind: metaform.FieldOptions, + Name: "replyIfNoReplies", + Label: "Reply Behavior", + Required: true, + Options: []metaform.Option{ + {Value: "true", Label: "Reply only to issues with no comments"}, + {Value: "false", Label: "Reply to all issues"}, + }, + }, + { + Kind: metaform.FieldString, + Name: "pollInterval", + Label: "Poll Interval", + Required: false, + Placeholder: "10m", + }, + }, + } +} + func (g *GithubIssues) Start(a *agent.Agent) { // Start the connector g.agent = a diff --git a/services/connectors/githubpr.go b/services/connectors/githubpr.go index 3114e01..97a4c03 100644 --- a/services/connectors/githubpr.go +++ b/services/connectors/githubpr.go @@ -7,6 +7,7 @@ import ( "github.com/google/go-github/v69/github" "github.com/mudler/LocalAgent/core/agent" + "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/sashabaranov/go-openai" @@ -63,6 +64,51 @@ func (g *GithubPRs) AgentReasoningCallback() func(state agent.ActionCurrentState } } +func (g *GithubPRs) ConfigForm() metaform.Form { + return metaform.Form{ + Fields: []metaform.Field{ + { + Kind: metaform.FieldString, + Name: "token", + Label: "GitHub Token", + Required: true, + Placeholder: "Your GitHub personal access token", + }, + { + Kind: metaform.FieldString, + Name: "owner", + Label: "Repository Owner", + Required: true, + Placeholder: "username or organization", + }, + { + Kind: metaform.FieldString, + Name: "repository", + Label: "Repository Name", + Required: true, + Placeholder: "repo-name", + }, + { + Kind: metaform.FieldOptions, + Name: "replyIfNoReplies", + Label: "Reply Behavior", + Required: true, + Options: []metaform.Option{ + {Value: "true", Label: "Reply only to PRs with no comments"}, + {Value: "false", Label: "Reply to all PRs"}, + }, + }, + { + Kind: metaform.FieldString, + Name: "pollInterval", + Label: "Poll Interval", + Required: true, + Placeholder: "10m", + }, + }, + } +} + func (g *GithubPRs) Start(a *agent.Agent) { // Start the connector g.agent = a diff --git a/services/connectors/irc.go b/services/connectors/irc.go index b3c1a06..f9dd225 100644 --- a/services/connectors/irc.go +++ b/services/connectors/irc.go @@ -6,6 +6,7 @@ import ( "time" "github.com/mudler/LocalAgent/core/agent" + "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/services/actions" irc "github.com/thoj/go-ircevent" @@ -43,6 +44,51 @@ func (i *IRC) AgentReasoningCallback() func(state agent.ActionCurrentState) bool } } +func (i *IRC) ConfigForm() metaform.Form { + return metaform.Form{ + Fields: []metaform.Field{ + { + Kind: metaform.FieldString, + Name: "server", + Label: "Server", + Required: true, + Placeholder: "chat.freenode.net", + }, + { + Kind: metaform.FieldString, + Name: "port", + Label: "Port", + Required: true, + Placeholder: "6667", + }, + { + Kind: metaform.FieldString, + Name: "nickname", + Label: "Nickname", + Required: true, + Placeholder: "LocalAgentBot", + }, + { + Kind: metaform.FieldString, + Name: "channel", + Label: "Channel", + Required: true, + Placeholder: "#general", + }, + { + Kind: metaform.FieldOptions, + Name: "alwaysReply", + Label: "Always Reply", + Required: true, + Options: []metaform.Option{ + {Value: "false", Label: "Only when mentioned"}, + {Value: "true", Label: "Reply to all messages"}, + }, + }, + }, + } +} + // cleanUpUsernameFromMessage removes the bot's nickname from the message func cleanUpMessage(message string, nickname string) string { cleaned := strings.ReplaceAll(message, nickname+":", "") diff --git a/services/connectors/slack.go b/services/connectors/slack.go index 30af50e..d049b5d 100644 --- a/services/connectors/slack.go +++ b/services/connectors/slack.go @@ -9,6 +9,7 @@ import ( "os" "strings" + "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/services/actions" "github.com/sashabaranov/go-openai" @@ -52,6 +53,44 @@ func (t *Slack) AgentReasoningCallback() func(state agent.ActionCurrentState) bo } } +func (t *Slack) ConfigForm() metaform.Form { + return metaform.Form{ + Fields: []metaform.Field{ + { + Kind: metaform.FieldString, + Name: "appToken", + Label: "App Token", + Required: true, + Placeholder: "xapp-...", + }, + { + Kind: metaform.FieldString, + Name: "botToken", + Label: "Bot Token", + Required: true, + Placeholder: "xoxb-...", + }, + { + Kind: metaform.FieldString, + Name: "channelID", + Label: "Channel ID", + Required: false, + Placeholder: "C12345678", + }, + { + Kind: metaform.FieldOptions, + Name: "alwaysReply", + Label: "Always Reply", + Required: false, + Options: []metaform.Option{ + {Value: "false", Label: "Only when mentioned"}, + {Value: "true", Label: "Reply to all messages"}, + }, + }, + }, + } +} + func cleanUpUsernameFromMessage(message string, b *slack.AuthTestResponse) string { cleaned := strings.ReplaceAll(message, "<@"+b.UserID+">", "") cleaned = strings.ReplaceAll(cleaned, "<@"+b.BotID+">", "") diff --git a/services/connectors/telegram.go b/services/connectors/telegram.go index 37de7dd..03d484f 100644 --- a/services/connectors/telegram.go +++ b/services/connectors/telegram.go @@ -9,6 +9,7 @@ import ( "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" "github.com/mudler/LocalAgent/core/agent" + "github.com/mudler/LocalAgent/pkg/metaform" ) type Telegram struct { @@ -37,6 +38,20 @@ func (t *Telegram) AgentReasoningCallback() func(state agent.ActionCurrentState) } } +func (t *Telegram) ConfigForm() metaform.Form { + return metaform.Form{ + Fields: []metaform.Field{ + { + Kind: metaform.FieldString, + Name: "token", + Label: "Bot Token", + Required: true, + Placeholder: "Your Telegram bot token from @BotFather", + }, + }, + } +} + func (t *Telegram) Start(a *agent.Agent) { ctx, cancel := signal.NotifyContext(a.Context(), os.Interrupt) defer cancel() diff --git a/services/connectors/twitter.go b/services/connectors/twitter.go index 32b9850..422aeaf 100644 --- a/services/connectors/twitter.go +++ b/services/connectors/twitter.go @@ -7,6 +7,7 @@ import ( "os/signal" "github.com/mudler/LocalAgent/core/agent" + "github.com/mudler/LocalAgent/pkg/metaform" "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/services/connectors/twitter" "github.com/sashabaranov/go-openai" @@ -32,6 +33,37 @@ func (t *Twitter) AgentReasoningCallback() func(state agent.ActionCurrentState) } } +func (t *Twitter) ConfigForm() metaform.Form { + return metaform.Form{ + Fields: []metaform.Field{ + { + Kind: metaform.FieldString, + Name: "token", + Label: "Token", + Required: true, + Placeholder: "elmo-...", + }, + { + Kind: metaform.FieldString, + Name: "botUsername", + Label: "Bot Username", + Required: true, + Placeholder: "Chocolate Pineapple", + }, + { + Kind: metaform.FieldOptions, + Name: "noCharacterLimit", + Label: "No Character Limit", + Required: false, + Options: []metaform.Option{ + {Value: "false", Label: ""}, + {Value: "true", Label: ""}, + }, + }, + }, + } +} + func NewTwitterConnector(config map[string]string) (*Twitter, error) { return &Twitter{ token: config["token"], diff --git a/webui/app.go b/webui/app.go index 8f7c47a..1feda91 100644 --- a/webui/app.go +++ b/webui/app.go @@ -10,10 +10,13 @@ import ( "github.com/mudler/LocalAgent/pkg/xlog" "github.com/mudler/LocalAgent/webui/types" + "github.com/mudler/LocalAgent/services" "github.com/mudler/LocalAgent/core/agent" "github.com/mudler/LocalAgent/core/sse" "github.com/mudler/LocalAgent/core/state" + "github.com/mudler/LocalAgent/pkg/metaform" + "github.com/mudler/LocalAgent/services/connectors" "github.com/donseba/go-htmx" fiber "github.com/gofiber/fiber/v2" @@ -350,3 +353,41 @@ func (a *App) Responses(pool *state.AgentPool) func(c *fiber.Ctx) error { return c.JSON(response) } } + +func (app *App) GetConnectorForm(pool *state.AgentPool) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + connectorType := c.Params("type") + + // Create a temporary connector to get its form metadata + var form metaform.Form + + switch connectorType { + case services.ConnectorTelegram: + form = (&connectors.Telegram{}).ConfigForm() + case services.ConnectorSlack: + form = (&connectors.Slack{}).ConfigForm() + case services.ConnectorDiscord: + form = (&connectors.Discord{}).ConfigForm() + case services.ConnectorGithubIssues: + form = (&connectors.GithubIssues{}).ConfigForm() + case services.ConnectorGithubPRs: + form = (&connectors.GithubPRs{}).ConfigForm() + case services.ConnectorIRC: + form = (&connectors.IRC{}).ConfigForm() + case services.ConnectorTwitter: + form = (&connectors.Twitter{}).ConfigForm() + default: + return c.Status(404).JSON(fiber.Map{ + "error": "Connector type not found", + }) + } + + // Create a data structure to pass both the form and the connector type + data := fiber.Map{ + "Form": form, + "Type": connectorType, + } + + return c.Render("views/partials/metaform", data) + } +} diff --git a/webui/public/js/agent-form.js b/webui/public/js/agent-form.js index 8f1c9b8..e7a3bb1 100644 --- a/webui/public/js/agent-form.js +++ b/webui/public/js/agent-form.js @@ -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 = '
Loading form...
'; + + // 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 = ` +
+

Failed to load connector form: ${error.message}

+
+ `; + }); +} + // HTML Templates for dynamic elements const AgentFormTemplates = { // Connector template @@ -242,9 +362,10 @@ const AgentFormTemplates = { ${data.options} -
- - +
+
+

Select a connector type to load its configuration form.

+
`; @@ -256,12 +377,12 @@ const AgentFormTemplates = {

MCP Server ${index + 1}

- - + +
- - + +
`; @@ -273,14 +394,14 @@ const AgentFormTemplates = {

Action ${index + 1}

- +
- +
`; @@ -292,14 +413,14 @@ const AgentFormTemplates = {

Prompt Block ${index + 1}

- +
- - + +
`; @@ -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); } \ No newline at end of file diff --git a/webui/routes.go b/webui/routes.go index c823fae..7943722 100644 --- a/webui/routes.go +++ b/webui/routes.go @@ -113,6 +113,7 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) { }) }) + webapp.Get("/settings/connector/form/:type", app.GetConnectorForm(pool)) webapp.Get("/settings/:name", func(c *fiber.Ctx) error { status := false for _, a := range pool.List() { diff --git a/webui/views/partials/metaform.html b/webui/views/partials/metaform.html new file mode 100644 index 0000000..f120666 --- /dev/null +++ b/webui/views/partials/metaform.html @@ -0,0 +1,94 @@ +{{/* + Metaform Partial Template + This template renders a form based on a Form struct from the metaform package. + + Usage: + {{template "partials/metaform.html" .}} + + Where . contains Form and Type fields +*/}} + +
+ {{range .Form.Fields}} +
+ + + {{if eq .Kind "string"}} + + + {{else if eq .Kind "number"}} + + + {{else if eq .Kind "options"}} + + {{end}} +
+ {{end}} +
+ + diff --git a/webui/views/settings.html b/webui/views/settings.html index 64a6faf..a14dbe2 100644 --- a/webui/views/settings.html +++ b/webui/views/settings.html @@ -190,10 +190,16 @@ updateButton.innerHTML = ' Updating...'; updateButton.disabled = true; + // Get the agent name from the hidden input field + const agentName = document.getElementById('name').value; + // Build a structured data object const formData = new FormData(form); const jsonData = AgentFormUtils.processFormData(formData); + // Ensure the name is set correctly + jsonData.name = agentName; + // Process special fields jsonData.connectors = AgentFormUtils.processConnectors(updateButton); if (jsonData.connectors === null) return; // Validation failed @@ -209,7 +215,7 @@ console.log('Sending data:', jsonData); // Send the structured data as JSON - fetch(`/api/agent/${jsonData.name}/config`, { + fetch(`/api/agent/${agentName}/config`, { method: 'PUT', headers: { 'Content-Type': 'application/json' @@ -360,16 +366,13 @@ // Find the added connector elements const connectorType = document.getElementById(`connectorType${index}`); - const connectorConfig = document.getElementById(`connectorConfig${index}`); - // Set values + // Set connector type value if (connectorType) { AgentFormUtils.setSelectValue(connectorType, connector.type); - } - - if (connectorConfig) { - // Format the config value - AgentFormUtils.formatConfigValue(connectorConfig, connector.config); + + // Load the connector form with the existing config data + loadConnectorForm(index, connector.type, connector.config); } }); }