diff --git a/core/state/pool.go b/core/state/pool.go index 4aac9cc..40d3694 100644 --- a/core/state/pool.go +++ b/core/state/pool.go @@ -244,6 +244,7 @@ func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agen func (a *AgentPool) List() []string { a.Lock() defer a.Unlock() + var agents []string for agent := range a.pool { agents = append(agents, agent) diff --git a/webui/app.go b/webui/app.go index c64d15f..364f71f 100644 --- a/webui/app.go +++ b/webui/app.go @@ -130,7 +130,7 @@ func (a *App) Create(pool *state.AgentPool) func(c *fiber.Ctx) error { return errorJSONMessage(c, err.Error()) } - fmt.Printf("Agent configuration: %+v\n", config) + xlog.Info("Agent configuration: %+v\n", config) if config.Name == "" { return errorJSONMessage(c, "Name is required") @@ -138,6 +138,7 @@ func (a *App) Create(pool *state.AgentPool) func(c *fiber.Ctx) error { if err := pool.CreateAgent(config.Name, &config); err != nil { return errorJSONMessage(c, err.Error()) } + return statusJSONMessage(c, "ok") } } @@ -156,7 +157,7 @@ func (a *App) GetAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error { // UpdateAgentConfig handles updating an agent's configuration func (a *App) UpdateAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error { - agentName := c.Params("name") + agentName := strings.Clone(c.Params("name")) // First check if agent exists oldConfig := pool.GetConfig(agentName) @@ -171,9 +172,6 @@ func (a *App) UpdateAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error return errorJSONMessage(c, err.Error()) } - // Ensure the name doesn't change - newConfig.Name = agentName - // Remove the agent first if err := pool.Remove(agentName); err != nil { return errorJSONMessage(c, "Error removing agent: "+err.Error()) diff --git a/webui/public/js/agent-form.js b/webui/public/js/agent-form.js index 8f1c9b8..5c73698 100644 --- a/webui/public/js/agent-form.js +++ b/webui/public/js/agent-form.js @@ -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 = ` +
+ + +
+ `; + + // 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 = {

Connector ${index + 1}

- + ${data.options}
-
- - +
+ +
+
Select a connector type to configure
+
+
`; }, @@ -256,13 +402,16 @@ const AgentFormTemplates = {

MCP Server ${index + 1}

- - + +
- - + +
+
`; }, @@ -273,15 +422,18 @@ const AgentFormTemplates = {

Action ${index + 1}

- - ${data.options}
- +
+
`; }, @@ -289,18 +441,21 @@ const AgentFormTemplates = { // Prompt Block template promptBlockTemplate: function(index, data) { return ` -
+

Prompt Block ${index + 1}

- - ${data.options}
- - + +
+
`; } @@ -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); -} \ No newline at end of file + 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); +} diff --git a/webui/public/js/connector-templates.js b/webui/public/js/connector-templates.js new file mode 100644 index 0000000..f093989 --- /dev/null +++ b/webui/public/js/connector-templates.js @@ -0,0 +1,475 @@ +/** + * Connector Templates + * + * This file contains templates for all connector types supported by LocalAgent. + * Each template is a function that returns an HTML string for the connector's form. + * + * Note: We don't need to escape HTML in the value attributes because browsers + * handle these values safely when setting them via DOM properties after rendering. + */ + +/** + * Connector Templates + * Each function takes a config object and returns an HTML string + */ +const ConnectorTemplates = { + /** + * Telegram Connector Template + * @param {Object} config - Existing configuration values + * @param {Number} index - Connector index + * @returns {Object} HTML template and setValues function + */ + telegram: function(config = {}, index) { + // Return HTML without values in the template string + const html = ` +
+ + + Get this from @BotFather on Telegram +
+ `; + + // Function to set values after HTML is added to DOM to avoid XSS + const setValues = function() { + const input = document.getElementById(`telegramToken${index}`); + if (input) input.value = config.token || ''; + }; + + return { html, setValues }; + }, + + /** + * Slack Connector Template + * @param {Object} config - Existing configuration values + * @param {Number} index - Connector index + * @returns {Object} HTML template and setValues function + */ + slack: function(config = {}, index) { + // Return HTML without values in the template string + const html = ` +
+ + + App-level token starting with xapp- +
+ +
+ + + Bot token starting with xoxb- +
+ +
+ + + Channel ID where the bot will operate +
+ +
+ + + If checked, the bot will reply to all messages in the channel +
+ `; + + // Function to set values after HTML is added to DOM to avoid XSS + const setValues = function() { + const appTokenInput = document.getElementById(`slackAppToken${index}`); + const botTokenInput = document.getElementById(`slackBotToken${index}`); + const channelIDInput = document.getElementById(`slackChannelID${index}`); + const alwaysReplyInput = document.getElementById(`slackAlwaysReply${index}`); + + if (appTokenInput) appTokenInput.value = config.appToken || ''; + if (botTokenInput) botTokenInput.value = config.botToken || ''; + if (channelIDInput) channelIDInput.value = config.channelID || ''; + if (alwaysReplyInput) alwaysReplyInput.checked = config.alwaysReply === 'true'; + }; + + return { html, setValues }; + }, + + /** + * Discord Connector Template + * @param {Object} config - Existing configuration values + * @param {Number} index - Connector index + * @returns {Object} HTML template and setValues function + */ + discord: function(config = {}, index) { + // Return HTML without values in the template string + const html = ` +
+ + +
+ +
+ + +
+ `; + + // Function to set values after HTML is added to DOM + const setValues = function() { + const tokenInput = document.getElementById(`discordToken${index}`); + const channelIDInput = document.getElementById(`discordChannelID${index}`); + + if (tokenInput) tokenInput.value = config.token || ''; + if (channelIDInput) channelIDInput.value = config.defaultChannel || ''; + }; + + return { html, setValues }; + }, + + /** + * GitHub Issues Connector Template + * @param {Object} config - Existing configuration values + * @param {Number} index - Connector index + * @returns {Object} HTML template and setValues function + */ + 'github-issues': function(config = {}, index) { + // Return HTML without values in the template string + const html = ` +
+ + + Needs repo and read:org permissions +
+ +
+ + +
+ +
+ + +
+ +
+ + + If checked, the bot will reply to issues that have no replies yet +
+ +
+ + + How often to check for new issues (in seconds) +
+ `; + + // Function to set values after HTML is added to DOM to avoid XSS + const setValues = function() { + const tokenInput = document.getElementById(`githubIssuesToken${index}`); + const ownerInput = document.getElementById(`githubIssuesOwner${index}`); + const repoInput = document.getElementById(`githubIssuesRepo${index}`); + const replyIfNoRepliesInput = document.getElementById(`githubIssuesReplyIfNoReplies${index}`); + const pollIntervalInput = document.getElementById(`githubIssuesPollInterval${index}`); + + if (tokenInput) tokenInput.value = config.token || ''; + if (ownerInput) ownerInput.value = config.owner || ''; + if (repoInput) repoInput.value = config.repository || ''; + if (replyIfNoRepliesInput) replyIfNoRepliesInput.checked = config.replyIfNoReplies === 'true'; + if (pollIntervalInput) pollIntervalInput.value = config.pollInterval || '60'; + }; + + return { html, setValues }; + }, + + /** + * GitHub PRs Connector Template + * @param {Object} config - Existing configuration values + * @param {Number} index - Connector index + * @returns {Object} HTML template and setValues function + */ + 'github-prs': function(config = {}, index) { + // Return HTML without values in the template string + const html = ` +
+ + + Personal Access Token with repo permissions +
+ +
+ + +
+ +
+ + +
+ +
+ + + If checked, the bot will reply to pull requests that have no replies yet +
+ +
+ + + How often to check for new pull requests (in seconds) +
+ `; + + // Function to set values after HTML is added to DOM to avoid XSS + const setValues = function() { + const tokenInput = document.getElementById(`githubPRsToken${index}`); + const ownerInput = document.getElementById(`githubPRsOwner${index}`); + const repoInput = document.getElementById(`githubPRsRepo${index}`); + const replyIfNoRepliesInput = document.getElementById(`githubPRsReplyIfNoReplies${index}`); + const pollIntervalInput = document.getElementById(`githubPRsPollInterval${index}`); + + if (tokenInput) tokenInput.value = config.token || ''; + if (ownerInput) ownerInput.value = config.owner || ''; + if (repoInput) repoInput.value = config.repository || ''; + if (replyIfNoRepliesInput) replyIfNoRepliesInput.checked = config.replyIfNoReplies === 'true'; + if (pollIntervalInput) pollIntervalInput.value = config.pollInterval || '60'; + }; + + return { html, setValues }; + }, + + /** + * IRC Connector Template + * @param {Object} config - Existing configuration values + * @param {Number} index - Connector index + * @returns {Object} HTML template and setValues function + */ + irc: function(config = {}, index) { + // Return HTML without values in the template string + const html = ` +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + If checked, the bot will always reply to messages, even if they are not directed at it +
+ `; + + // Function to set values after HTML is added to DOM + const setValues = function() { + const serverInput = document.getElementById(`ircServer${index}`); + const portInput = document.getElementById(`ircPort${index}`); + const channelInput = document.getElementById(`ircChannel${index}`); + const nickInput = document.getElementById(`ircNick${index}`); + const alwaysReplyInput = document.getElementById(`ircAlwaysReply${index}`); + + if (serverInput) serverInput.value = config.server || ''; + if (portInput) portInput.value = config.port || '6667'; + if (channelInput) channelInput.value = config.channel || ''; + if (nickInput) nickInput.value = config.nickname || ''; + if (alwaysReplyInput) alwaysReplyInput.checked = config.alwaysReply === 'true'; + }; + + return { html, setValues }; + }, + + /** + * Twitter Connector Template + * @param {Object} config - Existing configuration values + * @param {Number} index - Connector index + * @returns {Object} HTML template and setValues function + */ + twitter: function(config = {}, index) { + // Return HTML without values in the template string + const html = ` +
+ + +
+ +
+ + + Username of your Twitter bot (with or without @) +
+ +
+ + + If checked, the bot will not enforce Twitter's character limit +
+ `; + + // Function to set values after HTML is added to DOM + const setValues = function() { + const tokenInput = document.getElementById(`twitterToken${index}`); + const botUsernameInput = document.getElementById(`twitterBotUsername${index}`); + const noCharLimitInput = document.getElementById(`twitterNoCharLimit${index}`); + + if (tokenInput) tokenInput.value = config.token || ''; + if (botUsernameInput) botUsernameInput.value = config.botUsername || ''; + if (noCharLimitInput) noCharLimitInput.checked = config.noCharacterLimit === 'true'; + }; + + return { html, setValues }; + }, + + /** + * Fallback template for any connector without a specific template + * @param {Object} config - Existing configuration values + * @param {Number} index - Connector index + * @returns {Object} HTML template and setValues function + */ + fallback: function(config = {}, index) { + // Convert config to a pretty-printed JSON string + let configStr = '{}'; + try { + if (typeof config === 'string') { + // If it's already a string, try to parse it first to pretty-print + configStr = JSON.stringify(JSON.parse(config), null, 2); + } else if (typeof config === 'object' && config !== null) { + configStr = JSON.stringify(config, null, 2); + } + } catch (e) { + console.error('Error formatting config:', e); + // If it's a string but not valid JSON, just use it as is + if (typeof config === 'string') { + configStr = config; + } + } + + // Return HTML without values in the template string + const html = ` +
+ + + Enter the connector configuration as a JSON object +
+ `; + + // Function to set values after HTML is added to DOM + const setValues = function() { + const configInput = document.getElementById(`connectorConfig${index}`); + + if (configInput) configInput.value = configStr; + }; + + return { html, setValues }; + } +}; diff --git a/webui/views/create.html b/webui/views/create.html index b2796ab..95413dc 100644 --- a/webui/views/create.html +++ b/webui/views/create.html @@ -5,6 +5,7 @@ {{template "views/partials/header"}} + @@ -150,4 +151,4 @@ }); - \ No newline at end of file + diff --git a/webui/views/settings.html b/webui/views/settings.html index c92077a..e3f2a11 100644 --- a/webui/views/settings.html +++ b/webui/views/settings.html @@ -5,6 +5,7 @@ {{template "views/partials/header"}} + @@ -361,16 +362,25 @@ // Find the added connector elements const connectorType = document.getElementById(`connectorType${index}`); - const connectorConfig = document.getElementById(`connectorConfig${index}`); // Set values if (connectorType) { + // First set the connector type AgentFormUtils.setSelectValue(connectorType, connector.type); - } - - if (connectorConfig) { - // Format the config value - AgentFormUtils.formatConfigValue(connectorConfig, connector.config); + + // Parse the config if it's a string (from backend) + let configObj = connector.config; + if (typeof connector.config === 'string') { + try { + configObj = JSON.parse(connector.config); + } catch (e) { + console.error('Error parsing connector config:', e); + configObj = {}; // Fallback to empty object if parsing fails + } + } + + // Now render the appropriate form for this connector type with the config values + AgentFormUtils.renderConnectorForm(index, connector.type, configObj); } }); }