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}
-
-
-
-
+
+
`;
},
@@ -256,13 +402,16 @@ const AgentFormTemplates = {
`;
},
@@ -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 @@
});
-