feat(ui): Add individual forms for each connector
This commit is contained in:
@@ -244,6 +244,7 @@ func createAgentAvatar(APIURL, APIKey, model, imageModel, avatarDir string, agen
|
|||||||
func (a *AgentPool) List() []string {
|
func (a *AgentPool) List() []string {
|
||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
|
|
||||||
var agents []string
|
var agents []string
|
||||||
for agent := range a.pool {
|
for agent := range a.pool {
|
||||||
agents = append(agents, agent)
|
agents = append(agents, agent)
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func (a *App) Create(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
|||||||
return errorJSONMessage(c, err.Error())
|
return errorJSONMessage(c, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Agent configuration: %+v\n", config)
|
xlog.Info("Agent configuration: %+v\n", config)
|
||||||
|
|
||||||
if config.Name == "" {
|
if config.Name == "" {
|
||||||
return errorJSONMessage(c, "Name is required")
|
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 {
|
if err := pool.CreateAgent(config.Name, &config); err != nil {
|
||||||
return errorJSONMessage(c, err.Error())
|
return errorJSONMessage(c, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusJSONMessage(c, "ok")
|
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
|
// UpdateAgentConfig handles updating an agent's configuration
|
||||||
func (a *App) UpdateAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
func (a *App) UpdateAgentConfig(pool *state.AgentPool) func(c *fiber.Ctx) error {
|
||||||
return 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
|
// First check if agent exists
|
||||||
oldConfig := pool.GetConfig(agentName)
|
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())
|
return errorJSONMessage(c, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the name doesn't change
|
|
||||||
newConfig.Name = agentName
|
|
||||||
|
|
||||||
// Remove the agent first
|
// Remove the agent first
|
||||||
if err := pool.Remove(agentName); err != nil {
|
if err := pool.Remove(agentName); err != nil {
|
||||||
return errorJSONMessage(c, "Error removing agent: "+err.Error())
|
return errorJSONMessage(c, "Error removing agent: "+err.Error())
|
||||||
|
|||||||
@@ -57,37 +57,68 @@ const AgentFormUtils = {
|
|||||||
// Process connectors from form
|
// Process connectors from form
|
||||||
processConnectors: function(button) {
|
processConnectors: function(button) {
|
||||||
const connectors = [];
|
const connectors = [];
|
||||||
const connectorsElements = document.getElementsByClassName('connector');
|
const connectorElements = document.querySelectorAll('.connector');
|
||||||
|
|
||||||
for (let i = 0; i < connectorsElements.length; i++) {
|
for (let i = 0; i < connectorElements.length; i++) {
|
||||||
const typeField = document.getElementById(`connectorType${i}`);
|
const typeSelect = document.getElementById(`connectorType${i}`);
|
||||||
const configField = document.getElementById(`connectorConfig${i}`);
|
if (!typeSelect) {
|
||||||
|
showToast(`Error: Could not find connector type select for index ${i}`, 'error');
|
||||||
if (typeField && configField) {
|
button.innerHTML = button.getAttribute('data-original-text');
|
||||||
try {
|
button.disabled = false;
|
||||||
// Validate JSON but send as string
|
return null; // Validation failed
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
return connectors;
|
||||||
@@ -96,17 +127,23 @@ const AgentFormUtils = {
|
|||||||
// Process MCP servers from form
|
// Process MCP servers from form
|
||||||
processMCPServers: function() {
|
processMCPServers: function() {
|
||||||
const mcpServers = [];
|
const mcpServers = [];
|
||||||
const mcpServerElements = document.getElementsByClassName('mcp_server');
|
const mcpElements = document.querySelectorAll('.mcp_server');
|
||||||
|
|
||||||
for (let i = 0; i < mcpServerElements.length; i++) {
|
for (let i = 0; i < mcpElements.length; i++) {
|
||||||
const urlField = document.getElementById(`mcpURL${i}`);
|
const urlInput = document.getElementById(`mcpURL${i}`);
|
||||||
const tokenField = document.getElementById(`mcpToken${i}`);
|
const tokenInput = document.getElementById(`mcpToken${i}`);
|
||||||
|
|
||||||
if (urlField && urlField.value.trim()) {
|
if (urlInput && urlInput.value) {
|
||||||
mcpServers.push({
|
const server = {
|
||||||
url: urlField.value.trim(),
|
url: urlInput.value
|
||||||
token: tokenField ? tokenField.value.trim() : ''
|
};
|
||||||
});
|
|
||||||
|
// 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
|
// Process actions from form
|
||||||
processActions: function(button) {
|
processActions: function(button) {
|
||||||
const actions = [];
|
const actions = [];
|
||||||
const actionElements = document.getElementsByClassName('action');
|
const actionElements = document.querySelectorAll('.action');
|
||||||
|
|
||||||
for (let i = 0; i < actionElements.length; i++) {
|
for (let i = 0; i < actionElements.length; i++) {
|
||||||
const nameField = document.getElementById(`actionsName${i}`);
|
const nameSelect = document.getElementById(`actionsName${i}`);
|
||||||
const configField = document.getElementById(`actionsConfig${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 {
|
try {
|
||||||
// Validate JSON but send as string
|
config = JSON.parse(configTextarea.value);
|
||||||
const configValue = configField.value.trim() || '{}';
|
} catch (e) {
|
||||||
// Parse to validate but don't use the parsed object
|
showToast(`Invalid JSON in action ${i+1} config: ${e.message}`, 'error');
|
||||||
JSON.parse(configValue);
|
button.innerHTML = button.getAttribute('data-original-text');
|
||||||
|
button.disabled = false;
|
||||||
actions.push({
|
return null; // Validation failed
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actions.push({
|
||||||
|
name: name,
|
||||||
|
config: JSON.stringify(config) // Convert to JSON string for backend
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return actions;
|
return actions;
|
||||||
@@ -155,37 +198,43 @@ const AgentFormUtils = {
|
|||||||
// Process prompt blocks from form
|
// Process prompt blocks from form
|
||||||
processPromptBlocks: function(button) {
|
processPromptBlocks: function(button) {
|
||||||
const promptBlocks = [];
|
const promptBlocks = [];
|
||||||
const promptBlockElements = document.getElementsByClassName('promptBlock');
|
const promptElements = document.querySelectorAll('.prompt_block');
|
||||||
|
|
||||||
for (let i = 0; i < promptBlockElements.length; i++) {
|
for (let i = 0; i < promptElements.length; i++) {
|
||||||
const nameField = document.getElementById(`promptName${i}`);
|
const nameSelect = document.getElementById(`promptName${i}`);
|
||||||
const configField = document.getElementById(`promptConfig${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 {
|
try {
|
||||||
// Validate JSON but send as string
|
config = JSON.parse(configTextarea.value);
|
||||||
const configValue = configField.value.trim() || '{}';
|
} catch (e) {
|
||||||
// Parse to validate but don't use the parsed object
|
showToast(`Invalid JSON in prompt block ${i+1} config: ${e.message}`, 'error');
|
||||||
JSON.parse(configValue);
|
button.innerHTML = button.getAttribute('data-original-text');
|
||||||
|
button.disabled = false;
|
||||||
promptBlocks.push({
|
return null; // Validation failed
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
promptBlocks.push({
|
||||||
|
name: name,
|
||||||
|
config: JSON.stringify(config) // Convert to JSON string for backend
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return promptBlocks;
|
return promptBlocks;
|
||||||
@@ -193,38 +242,126 @@ const AgentFormUtils = {
|
|||||||
|
|
||||||
// Helper function to format config values (for edit form)
|
// Helper function to format config values (for edit form)
|
||||||
formatConfigValue: function(configElement, configValue) {
|
formatConfigValue: function(configElement, configValue) {
|
||||||
// If it's a string (already stringified JSON), try to parse it first
|
if (!configElement) return;
|
||||||
if (typeof configValue === 'string') {
|
|
||||||
|
// 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 {
|
try {
|
||||||
const parsed = JSON.parse(configValue);
|
const parsed = JSON.parse(configValue);
|
||||||
configElement.value = JSON.stringify(parsed, null, 2);
|
configElement.value = JSON.stringify(parsed, null, 2);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to parse config JSON string:", e);
|
// If it's not valid JSON, just use the string as is
|
||||||
configElement.value = configValue; // Keep as is if parsing fails
|
configElement.value = configValue;
|
||||||
}
|
}
|
||||||
} else if (configValue !== undefined && configValue !== null) {
|
}
|
||||||
// Direct object, just stringify with formatting
|
// Otherwise, just use the value as is
|
||||||
configElement.value = JSON.stringify(configValue, null, 2);
|
else {
|
||||||
} else {
|
configElement.value = configValue || '';
|
||||||
// Default to empty object
|
|
||||||
configElement.value = '{}';
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Helper function to set select value (with fallback if option doesn't exist)
|
// Helper function to set select value (with fallback if option doesn't exist)
|
||||||
setSelectValue: function(selectElement, value) {
|
setSelectValue: function(selectElement, value) {
|
||||||
// Check if the option exists
|
if (!selectElement) return;
|
||||||
const optionExists = Array.from(selectElement.options).some(option => option.value === value);
|
|
||||||
|
|
||||||
|
// 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) {
|
if (optionExists) {
|
||||||
selectElement.value = value;
|
selectElement.value = value;
|
||||||
} else if (value) {
|
} else if (selectElement.options.length > 0) {
|
||||||
// If value is provided but option doesn't exist, create a new option
|
// Otherwise, select the first option
|
||||||
const newOption = document.createElement('option');
|
selectElement.selectedIndex = 0;
|
||||||
newOption.value = value;
|
}
|
||||||
newOption.text = value + ' (custom)';
|
},
|
||||||
selectElement.add(newOption);
|
|
||||||
selectElement.value = value;
|
// 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>
|
<h2>Connector ${index + 1}</h2>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="connectorType${index}">Connector Type</label>
|
<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}
|
${data.options}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div id="connectorFormContainer${index}">
|
||||||
<label for="connectorConfig${index}">Connector Config (JSON)</label>
|
<!-- Connector form will be dynamically inserted here -->
|
||||||
<textarea id="connectorConfig${index}" name="connectors[${index}].config" placeholder='{"token":"sk-mg3.."}'>{}</textarea>
|
<div class="form-group">
|
||||||
|
<div class="placeholder-text">Select a connector type to configure</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="remove-btn" onclick="this.closest('.connector').remove()">
|
||||||
|
<i class="fas fa-trash"></i> Remove Connector
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
@@ -256,13 +402,16 @@ const AgentFormTemplates = {
|
|||||||
<div class="mcp_server mb-4 section-box" style="margin-top: 15px; padding: 15px;">
|
<div class="mcp_server mb-4 section-box" style="margin-top: 15px; padding: 15px;">
|
||||||
<h2>MCP Server ${index + 1}</h2>
|
<h2>MCP Server ${index + 1}</h2>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="mcpURL${index}">MCP Server URL</label>
|
<label for="mcpURL${index}">Server URL</label>
|
||||||
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}].url" placeholder='https://...'>
|
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}][url]" placeholder="https://example.com">
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="mcpToken${index}">Bearer Token</label>
|
<label for="mcpToken${index}">API Token (Optional)</label>
|
||||||
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}].token" placeholder='Bearer token'>
|
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}][token]" placeholder="API token">
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="remove-btn" onclick="this.closest('.mcp_server').remove()">
|
||||||
|
<i class="fas fa-trash"></i> Remove Server
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
@@ -273,15 +422,18 @@ const AgentFormTemplates = {
|
|||||||
<div class="action mb-4 section-box" style="margin-top: 15px; padding: 15px;">
|
<div class="action mb-4 section-box" style="margin-top: 15px; padding: 15px;">
|
||||||
<h2>Action ${index + 1}</h2>
|
<h2>Action ${index + 1}</h2>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="actionsName${index}">Action</label>
|
<label for="actionsName${index}">Action Type</label>
|
||||||
<select name="actions[${index}].name" id="actionsName${index}">
|
<select name="actions[${index}][name]" id="actionsName${index}">
|
||||||
${data.options}
|
${data.options}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="actionsConfig${index}">Action Config (JSON)</label>
|
<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>
|
</div>
|
||||||
|
<button type="button" class="remove-btn" onclick="this.closest('.action').remove()">
|
||||||
|
<i class="fas fa-trash"></i> Remove Action
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
@@ -289,18 +441,21 @@ const AgentFormTemplates = {
|
|||||||
// Prompt Block template
|
// Prompt Block template
|
||||||
promptBlockTemplate: function(index, data) {
|
promptBlockTemplate: function(index, data) {
|
||||||
return `
|
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>
|
<h2>Prompt Block ${index + 1}</h2>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="promptName${index}">Block Prompt</label>
|
<label for="promptName${index}">Prompt Block Type</label>
|
||||||
<select name="promptblocks[${index}].name" id="promptName${index}">
|
<select name="promptblocks[${index}][name]" id="promptName${index}">
|
||||||
${data.options}
|
${data.options}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label for="promptConfig${index}">Prompt Config (JSON)</label>
|
<label for="promptConfig${index}">Prompt Block Config (JSON)</label>
|
||||||
<textarea id="promptConfig${index}" name="promptblocks[${index}].config" placeholder='{"results":"5"}'>{}</textarea>
|
<textarea id="promptConfig${index}" name="promptblocks[${index}][config]" placeholder='{"key":"value"}'>{}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
<button type="button" class="remove-btn" onclick="this.closest('.prompt_block').remove()">
|
||||||
|
<i class="fas fa-trash"></i> Remove Prompt Block
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -308,61 +463,102 @@ const AgentFormTemplates = {
|
|||||||
|
|
||||||
// Initialize form event listeners
|
// Initialize form event listeners
|
||||||
function initAgentFormCommon(options = {}) {
|
function initAgentFormCommon(options = {}) {
|
||||||
// Setup event listeners for dynamic component buttons
|
// Add connector button
|
||||||
if (options.enableConnectors !== false) {
|
const addConnectorButton = document.getElementById('addConnectorButton');
|
||||||
document.getElementById('addConnectorButton').addEventListener('click', function() {
|
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, {
|
AgentFormUtils.addDynamicComponent('connectorsSection', AgentFormTemplates.connectorTemplate, {
|
||||||
className: 'connector',
|
className: 'connector',
|
||||||
options: options.connectors || ''
|
options: optionsHtml
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.enableMCP !== false) {
|
// Add MCP server button
|
||||||
document.getElementById('addMCPButton').addEventListener('click', function() {
|
const addMCPButton = document.getElementById('addMCPButton');
|
||||||
|
if (addMCPButton) {
|
||||||
|
addMCPButton.addEventListener('click', function() {
|
||||||
|
// Add new MCP server form
|
||||||
AgentFormUtils.addDynamicComponent('mcpSection', AgentFormTemplates.mcpServerTemplate, {
|
AgentFormUtils.addDynamicComponent('mcpSection', AgentFormTemplates.mcpServerTemplate, {
|
||||||
className: 'mcp_server'
|
className: 'mcp_server'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.enableActions !== false) {
|
// Add action button
|
||||||
document.getElementById('action_button').addEventListener('click', function() {
|
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, {
|
AgentFormUtils.addDynamicComponent('action_box', AgentFormTemplates.actionTemplate, {
|
||||||
className: 'action',
|
className: 'action',
|
||||||
options: options.actions || ''
|
options: optionsHtml
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.enablePromptBlocks !== false) {
|
// Add prompt block button
|
||||||
document.getElementById('dynamic_button').addEventListener('click', function() {
|
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, {
|
AgentFormUtils.addDynamicComponent('dynamic_box', AgentFormTemplates.promptBlockTemplate, {
|
||||||
className: 'promptBlock',
|
className: 'prompt_block',
|
||||||
options: options.promptBlocks || ''
|
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 toastMessage = document.getElementById('toast-message');
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
// Set message
|
||||||
.checkbox-label {
|
toastMessage.textContent = message;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
// Set type class
|
||||||
cursor: pointer;
|
toast.className = 'toast';
|
||||||
margin-bottom: 10px;
|
toast.classList.add(`toast-${type}`);
|
||||||
}
|
|
||||||
|
// Show toast
|
||||||
.checkbox-label .checkbox-custom {
|
toast.classList.add('show');
|
||||||
margin-right: 10px;
|
|
||||||
}
|
// Hide after 3 seconds
|
||||||
|
setTimeout(() => {
|
||||||
@keyframes pulse {
|
toast.classList.remove('show');
|
||||||
0% { transform: scale(1); }
|
}, 3000);
|
||||||
50% { transform: scale(1.05); }
|
}
|
||||||
100% { transform: scale(1); }
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|||||||
475
webui/public/js/connector-templates.js
Normal file
475
webui/public/js/connector-templates.js
Normal file
@@ -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 = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="telegramToken${index}">Telegram Bot Token</label>
|
||||||
|
<input type="text"
|
||||||
|
id="telegramToken${index}"
|
||||||
|
name="connectors[${index}][config][token]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11">
|
||||||
|
<small class="form-text text-muted">Get this from @BotFather on Telegram</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="slackAppToken${index}">Slack App Token</label>
|
||||||
|
<input type="text"
|
||||||
|
id="slackAppToken${index}"
|
||||||
|
name="connectors[${index}][config][appToken]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="xapp-...">
|
||||||
|
<small class="form-text text-muted">App-level token starting with xapp-</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="slackBotToken${index}">Slack Bot Token</label>
|
||||||
|
<input type="text"
|
||||||
|
id="slackBotToken${index}"
|
||||||
|
name="connectors[${index}][config][botToken]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="xoxb-...">
|
||||||
|
<small class="form-text text-muted">Bot token starting with xoxb-</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="slackChannelID${index}">Slack Channel ID</label>
|
||||||
|
<input type="text"
|
||||||
|
id="slackChannelID${index}"
|
||||||
|
name="connectors[${index}][config][channelID]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="C012345678">
|
||||||
|
<small class="form-text text-muted">Channel ID where the bot will operate</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="slackAlwaysReply${index}"
|
||||||
|
name="connectors[${index}][config][alwaysReply]">
|
||||||
|
<label class="form-check-label" for="slackAlwaysReply${index}">Always Reply</label>
|
||||||
|
<small class="form-text text-muted">If checked, the bot will reply to all messages in the channel</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="discordToken${index}">Discord Bot Token</label>
|
||||||
|
<input type="text"
|
||||||
|
id="discordToken${index}"
|
||||||
|
name="connectors[${index}][config][token]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Bot token from Discord Developer Portal">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="discordChannelID${index}">Default Channel ID</label>
|
||||||
|
<input type="text"
|
||||||
|
id="discordChannelID${index}"
|
||||||
|
name="connectors[${index}][config][defaultChannel]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Channel ID where the bot will operate">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="githubIssuesToken${index}">GitHub Personal Access Token</label>
|
||||||
|
<input type="text"
|
||||||
|
id="githubIssuesToken${index}"
|
||||||
|
name="connectors[${index}][config][token]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="ghp_...">
|
||||||
|
<small class="form-text text-muted">Needs repo and read:org permissions</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="githubIssuesOwner${index}">Repository Owner</label>
|
||||||
|
<input type="text"
|
||||||
|
id="githubIssuesOwner${index}"
|
||||||
|
name="connectors[${index}][config][owner]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="username or organization">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="githubIssuesRepo${index}">Repository Name</label>
|
||||||
|
<input type="text"
|
||||||
|
id="githubIssuesRepo${index}"
|
||||||
|
name="connectors[${index}][config][repository]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="repository-name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="githubIssuesReplyIfNoReplies${index}"
|
||||||
|
name="connectors[${index}][config][replyIfNoReplies]"
|
||||||
|
value="true">
|
||||||
|
<label class="form-check-label" for="githubIssuesReplyIfNoReplies${index}">Reply to issues with no replies</label>
|
||||||
|
<small class="form-text text-muted">If checked, the bot will reply to issues that have no replies yet</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="githubIssuesPollInterval${index}">Poll Interval (seconds)</label>
|
||||||
|
<input type="number"
|
||||||
|
id="githubIssuesPollInterval${index}"
|
||||||
|
name="connectors[${index}][config][pollInterval]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="60">
|
||||||
|
<small class="form-text text-muted">How often to check for new issues (in seconds)</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="githubPRsToken${index}">GitHub Token</label>
|
||||||
|
<input type="text"
|
||||||
|
id="githubPRsToken${index}"
|
||||||
|
name="connectors[${index}][config][token]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="ghp_...">
|
||||||
|
<small class="form-text text-muted">Personal Access Token with repo permissions</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="githubPRsOwner${index}">Repository Owner</label>
|
||||||
|
<input type="text"
|
||||||
|
id="githubPRsOwner${index}"
|
||||||
|
name="connectors[${index}][config][owner]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="username or organization">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="githubPRsRepo${index}">Repository Name</label>
|
||||||
|
<input type="text"
|
||||||
|
id="githubPRsRepo${index}"
|
||||||
|
name="connectors[${index}][config][repository]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="repository-name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="githubPRsReplyIfNoReplies${index}"
|
||||||
|
name="connectors[${index}][config][replyIfNoReplies]"
|
||||||
|
value="true">
|
||||||
|
<label class="form-check-label" for="githubPRsReplyIfNoReplies${index}">Reply to PRs with no replies</label>
|
||||||
|
<small class="form-text text-muted">If checked, the bot will reply to pull requests that have no replies yet</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="githubPRsPollInterval${index}">Poll Interval (seconds)</label>
|
||||||
|
<input type="number"
|
||||||
|
id="githubPRsPollInterval${index}"
|
||||||
|
name="connectors[${index}][config][pollInterval]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="60">
|
||||||
|
<small class="form-text text-muted">How often to check for new pull requests (in seconds)</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ircServer${index}">IRC Server</label>
|
||||||
|
<input type="text"
|
||||||
|
id="ircServer${index}"
|
||||||
|
name="connectors[${index}][config][server]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="irc.libera.chat">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ircPort${index}">Port</label>
|
||||||
|
<input type="text"
|
||||||
|
id="ircPort${index}"
|
||||||
|
name="connectors[${index}][config][port]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="6667">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ircChannel${index}">Channel</label>
|
||||||
|
<input type="text"
|
||||||
|
id="ircChannel${index}"
|
||||||
|
name="connectors[${index}][config][channel]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="#channel">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ircNick${index}">Nickname</label>
|
||||||
|
<input type="text"
|
||||||
|
id="ircNick${index}"
|
||||||
|
name="connectors[${index}][config][nickname]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="MyBot">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="ircAlwaysReply${index}"
|
||||||
|
name="connectors[${index}][config][alwaysReply]"
|
||||||
|
value="true">
|
||||||
|
<label class="form-check-label" for="ircAlwaysReply${index}">Always reply to messages</label>
|
||||||
|
<small class="form-text text-muted">If checked, the bot will always reply to messages, even if they are not directed at it</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="twitterToken${index}">Twitter API Token</label>
|
||||||
|
<input type="text"
|
||||||
|
id="twitterToken${index}"
|
||||||
|
name="connectors[${index}][config][token]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="Your Twitter API token">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="twitterBotUsername${index}">Bot Username</label>
|
||||||
|
<input type="text"
|
||||||
|
id="twitterBotUsername${index}"
|
||||||
|
name="connectors[${index}][config][botUsername]"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="@YourBotUsername">
|
||||||
|
<small class="form-text text-muted">Username of your Twitter bot (with or without @)</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group form-check">
|
||||||
|
<input type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="twitterNoCharLimit${index}"
|
||||||
|
name="connectors[${index}][config][noCharacterLimit]"
|
||||||
|
value="true">
|
||||||
|
<label class="form-check-label" for="twitterNoCharLimit${index}">Disable character limit</label>
|
||||||
|
<small class="form-text text-muted">If checked, the bot will not enforce Twitter's character limit</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 = `
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="connectorConfig${index}">Connector Configuration (JSON)</label>
|
||||||
|
<textarea id="connectorConfig${index}"
|
||||||
|
name="connectors[${index}][config]"
|
||||||
|
class="form-control"
|
||||||
|
rows="10"
|
||||||
|
placeholder='{"key":"value"}'>${escapeHTML(configStr)}</textarea>
|
||||||
|
<small class="form-text text-muted">Enter the connector configuration as a JSON object</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
{{template "views/partials/header"}}
|
{{template "views/partials/header"}}
|
||||||
<script src="/public/js/wizard.js"></script>
|
<script src="/public/js/wizard.js"></script>
|
||||||
<link rel="stylesheet" href="/public/css/wizard.css">
|
<link rel="stylesheet" href="/public/css/wizard.css">
|
||||||
|
<script src="/public/js/connector-templates.js"></script>
|
||||||
<script src="/public/js/agent-form.js"></script>
|
<script src="/public/js/agent-form.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -150,4 +151,4 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
{{template "views/partials/header"}}
|
{{template "views/partials/header"}}
|
||||||
<script src="/public/js/wizard.js"></script>
|
<script src="/public/js/wizard.js"></script>
|
||||||
<link rel="stylesheet" href="/public/css/wizard.css">
|
<link rel="stylesheet" href="/public/css/wizard.css">
|
||||||
|
<script src="/public/js/connector-templates.js"></script>
|
||||||
<script src="/public/js/agent-form.js"></script>
|
<script src="/public/js/agent-form.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -361,16 +362,25 @@
|
|||||||
|
|
||||||
// Find the added connector elements
|
// Find the added connector elements
|
||||||
const connectorType = document.getElementById(`connectorType${index}`);
|
const connectorType = document.getElementById(`connectorType${index}`);
|
||||||
const connectorConfig = document.getElementById(`connectorConfig${index}`);
|
|
||||||
|
|
||||||
// Set values
|
// Set values
|
||||||
if (connectorType) {
|
if (connectorType) {
|
||||||
|
// First set the connector type
|
||||||
AgentFormUtils.setSelectValue(connectorType, connector.type);
|
AgentFormUtils.setSelectValue(connectorType, connector.type);
|
||||||
}
|
|
||||||
|
// Parse the config if it's a string (from backend)
|
||||||
if (connectorConfig) {
|
let configObj = connector.config;
|
||||||
// Format the config value
|
if (typeof connector.config === 'string') {
|
||||||
AgentFormUtils.formatConfigValue(connectorConfig, connector.config);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user