Revert "Generate connector form based on meta-data (#62)" (#65)

This reverts commit d7cfa7f0b2.
This commit is contained in:
Ettore Di Giacinto
2025-03-20 18:21:19 +01:00
committed by GitHub
parent 1e5b3f501f
commit d54abc3ed0
15 changed files with 77 additions and 629 deletions

2
.gitignore vendored
View File

@@ -2,7 +2,5 @@ models/
data/
pool
uploads/
volumes/
local-agent-framework
localagent
LocalAgent

View File

@@ -4,7 +4,6 @@ import (
"encoding/json"
"github.com/mudler/LocalAgent/core/agent"
"github.com/mudler/LocalAgent/pkg/metaform"
)
type ConnectorConfig struct {
@@ -63,6 +62,5 @@ type AgentConfig struct {
type Connector interface {
AgentResultCallback() func(state agent.ActionState)
AgentReasoningCallback() func(state agent.ActionCurrentState) bool
ConfigForm() metaform.Form
Start(a *agent.Agent)
}

View File

@@ -1,28 +0,0 @@
package metaform
// Option represents a selectable option for FieldOption type
type Option struct {
Value string `json:"value"`
Label string `json:"label"`
}
type FieldKind string
const (
FieldString FieldKind = "string"
FieldNumber FieldKind = "number"
FieldOptions FieldKind = "options"
)
type Field struct {
Kind FieldKind `json:"kind"`
Name string `json:"name"`
Label string `json:"label"`
Required bool `json:"required"`
Placeholder string `json:"placeholder,omitempty"`
Options []Option `json:"options,omitempty"`
}
type Form struct {
Fields []Field
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/bwmarrin/discordgo"
"github.com/mudler/LocalAgent/core/agent"
"github.com/mudler/LocalAgent/pkg/metaform"
"github.com/mudler/LocalAgent/pkg/xlog"
)
@@ -38,27 +37,6 @@ func (d *Discord) AgentReasoningCallback() func(state agent.ActionCurrentState)
}
}
func (d *Discord) ConfigForm() metaform.Form {
return metaform.Form{
Fields: []metaform.Field{
{
Kind: metaform.FieldString,
Name: "token",
Label: "Bot Token",
Required: true,
Placeholder: "Your Discord bot token",
},
{
Kind: metaform.FieldString,
Name: "defaultChannel",
Label: "Default Channel",
Required: true,
Placeholder: "The default channel for the bot to join",
},
},
}
}
func (d *Discord) Start(a *agent.Agent) {
Token := d.token

View File

@@ -7,7 +7,6 @@ import (
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAgent/core/agent"
"github.com/mudler/LocalAgent/pkg/metaform"
"github.com/mudler/LocalAgent/pkg/xlog"
"github.com/sashabaranov/go-openai"
@@ -64,51 +63,6 @@ func (g *GithubIssues) AgentReasoningCallback() func(state agent.ActionCurrentSt
}
}
func (g *GithubIssues) ConfigForm() metaform.Form {
return metaform.Form{
Fields: []metaform.Field{
{
Kind: metaform.FieldString,
Name: "token",
Label: "GitHub Token",
Required: true,
Placeholder: "Your GitHub personal access token",
},
{
Kind: metaform.FieldString,
Name: "owner",
Label: "Repository Owner",
Required: true,
Placeholder: "username or organization",
},
{
Kind: metaform.FieldString,
Name: "repository",
Label: "Repository Name",
Required: true,
Placeholder: "repo-name",
},
{
Kind: metaform.FieldOptions,
Name: "replyIfNoReplies",
Label: "Reply Behavior",
Required: true,
Options: []metaform.Option{
{Value: "true", Label: "Reply only to issues with no comments"},
{Value: "false", Label: "Reply to all issues"},
},
},
{
Kind: metaform.FieldString,
Name: "pollInterval",
Label: "Poll Interval",
Required: false,
Placeholder: "10m",
},
},
}
}
func (g *GithubIssues) Start(a *agent.Agent) {
// Start the connector
g.agent = a

View File

@@ -7,7 +7,6 @@ import (
"github.com/google/go-github/v69/github"
"github.com/mudler/LocalAgent/core/agent"
"github.com/mudler/LocalAgent/pkg/metaform"
"github.com/mudler/LocalAgent/pkg/xlog"
"github.com/sashabaranov/go-openai"
@@ -64,51 +63,6 @@ func (g *GithubPRs) AgentReasoningCallback() func(state agent.ActionCurrentState
}
}
func (g *GithubPRs) ConfigForm() metaform.Form {
return metaform.Form{
Fields: []metaform.Field{
{
Kind: metaform.FieldString,
Name: "token",
Label: "GitHub Token",
Required: true,
Placeholder: "Your GitHub personal access token",
},
{
Kind: metaform.FieldString,
Name: "owner",
Label: "Repository Owner",
Required: true,
Placeholder: "username or organization",
},
{
Kind: metaform.FieldString,
Name: "repository",
Label: "Repository Name",
Required: true,
Placeholder: "repo-name",
},
{
Kind: metaform.FieldOptions,
Name: "replyIfNoReplies",
Label: "Reply Behavior",
Required: true,
Options: []metaform.Option{
{Value: "true", Label: "Reply only to PRs with no comments"},
{Value: "false", Label: "Reply to all PRs"},
},
},
{
Kind: metaform.FieldString,
Name: "pollInterval",
Label: "Poll Interval",
Required: true,
Placeholder: "10m",
},
},
}
}
func (g *GithubPRs) Start(a *agent.Agent) {
// Start the connector
g.agent = a

View File

@@ -6,7 +6,6 @@ import (
"time"
"github.com/mudler/LocalAgent/core/agent"
"github.com/mudler/LocalAgent/pkg/metaform"
"github.com/mudler/LocalAgent/pkg/xlog"
"github.com/mudler/LocalAgent/services/actions"
irc "github.com/thoj/go-ircevent"
@@ -44,51 +43,6 @@ func (i *IRC) AgentReasoningCallback() func(state agent.ActionCurrentState) bool
}
}
func (i *IRC) ConfigForm() metaform.Form {
return metaform.Form{
Fields: []metaform.Field{
{
Kind: metaform.FieldString,
Name: "server",
Label: "Server",
Required: true,
Placeholder: "chat.freenode.net",
},
{
Kind: metaform.FieldString,
Name: "port",
Label: "Port",
Required: true,
Placeholder: "6667",
},
{
Kind: metaform.FieldString,
Name: "nickname",
Label: "Nickname",
Required: true,
Placeholder: "LocalAgentBot",
},
{
Kind: metaform.FieldString,
Name: "channel",
Label: "Channel",
Required: true,
Placeholder: "#general",
},
{
Kind: metaform.FieldOptions,
Name: "alwaysReply",
Label: "Always Reply",
Required: true,
Options: []metaform.Option{
{Value: "false", Label: "Only when mentioned"},
{Value: "true", Label: "Reply to all messages"},
},
},
},
}
}
// cleanUpUsernameFromMessage removes the bot's nickname from the message
func cleanUpMessage(message string, nickname string) string {
cleaned := strings.ReplaceAll(message, nickname+":", "")

View File

@@ -9,7 +9,6 @@ import (
"os"
"strings"
"github.com/mudler/LocalAgent/pkg/metaform"
"github.com/mudler/LocalAgent/pkg/xlog"
"github.com/mudler/LocalAgent/services/actions"
"github.com/sashabaranov/go-openai"
@@ -53,44 +52,6 @@ func (t *Slack) AgentReasoningCallback() func(state agent.ActionCurrentState) bo
}
}
func (t *Slack) ConfigForm() metaform.Form {
return metaform.Form{
Fields: []metaform.Field{
{
Kind: metaform.FieldString,
Name: "appToken",
Label: "App Token",
Required: true,
Placeholder: "xapp-...",
},
{
Kind: metaform.FieldString,
Name: "botToken",
Label: "Bot Token",
Required: true,
Placeholder: "xoxb-...",
},
{
Kind: metaform.FieldString,
Name: "channelID",
Label: "Channel ID",
Required: false,
Placeholder: "C12345678",
},
{
Kind: metaform.FieldOptions,
Name: "alwaysReply",
Label: "Always Reply",
Required: false,
Options: []metaform.Option{
{Value: "false", Label: "Only when mentioned"},
{Value: "true", Label: "Reply to all messages"},
},
},
},
}
}
func cleanUpUsernameFromMessage(message string, b *slack.AuthTestResponse) string {
cleaned := strings.ReplaceAll(message, "<@"+b.UserID+">", "")
cleaned = strings.ReplaceAll(cleaned, "<@"+b.BotID+">", "")

View File

@@ -9,7 +9,6 @@ import (
"github.com/go-telegram/bot"
"github.com/go-telegram/bot/models"
"github.com/mudler/LocalAgent/core/agent"
"github.com/mudler/LocalAgent/pkg/metaform"
)
type Telegram struct {
@@ -38,20 +37,6 @@ func (t *Telegram) AgentReasoningCallback() func(state agent.ActionCurrentState)
}
}
func (t *Telegram) ConfigForm() metaform.Form {
return metaform.Form{
Fields: []metaform.Field{
{
Kind: metaform.FieldString,
Name: "token",
Label: "Bot Token",
Required: true,
Placeholder: "Your Telegram bot token from @BotFather",
},
},
}
}
func (t *Telegram) Start(a *agent.Agent) {
ctx, cancel := signal.NotifyContext(a.Context(), os.Interrupt)
defer cancel()

View File

@@ -7,7 +7,6 @@ import (
"os/signal"
"github.com/mudler/LocalAgent/core/agent"
"github.com/mudler/LocalAgent/pkg/metaform"
"github.com/mudler/LocalAgent/pkg/xlog"
"github.com/mudler/LocalAgent/services/connectors/twitter"
"github.com/sashabaranov/go-openai"
@@ -33,37 +32,6 @@ func (t *Twitter) AgentReasoningCallback() func(state agent.ActionCurrentState)
}
}
func (t *Twitter) ConfigForm() metaform.Form {
return metaform.Form{
Fields: []metaform.Field{
{
Kind: metaform.FieldString,
Name: "token",
Label: "Token",
Required: true,
Placeholder: "elmo-...",
},
{
Kind: metaform.FieldString,
Name: "botUsername",
Label: "Bot Username",
Required: true,
Placeholder: "Chocolate Pineapple",
},
{
Kind: metaform.FieldOptions,
Name: "noCharacterLimit",
Label: "No Character Limit",
Required: false,
Options: []metaform.Option{
{Value: "false", Label: ""},
{Value: "true", Label: ""},
},
},
},
}
}
func NewTwitterConnector(config map[string]string) (*Twitter, error) {
return &Twitter{
token: config["token"],

View File

@@ -10,13 +10,10 @@ import (
"github.com/mudler/LocalAgent/pkg/xlog"
"github.com/mudler/LocalAgent/webui/types"
"github.com/mudler/LocalAgent/services"
"github.com/mudler/LocalAgent/core/agent"
"github.com/mudler/LocalAgent/core/sse"
"github.com/mudler/LocalAgent/core/state"
"github.com/mudler/LocalAgent/pkg/metaform"
"github.com/mudler/LocalAgent/services/connectors"
"github.com/donseba/go-htmx"
fiber "github.com/gofiber/fiber/v2"
@@ -353,41 +350,3 @@ func (a *App) Responses(pool *state.AgentPool) func(c *fiber.Ctx) error {
return c.JSON(response)
}
}
func (app *App) GetConnectorForm(pool *state.AgentPool) func(c *fiber.Ctx) error {
return func(c *fiber.Ctx) error {
connectorType := c.Params("type")
// Create a temporary connector to get its form metadata
var form metaform.Form
switch connectorType {
case services.ConnectorTelegram:
form = (&connectors.Telegram{}).ConfigForm()
case services.ConnectorSlack:
form = (&connectors.Slack{}).ConfigForm()
case services.ConnectorDiscord:
form = (&connectors.Discord{}).ConfigForm()
case services.ConnectorGithubIssues:
form = (&connectors.GithubIssues{}).ConfigForm()
case services.ConnectorGithubPRs:
form = (&connectors.GithubPRs{}).ConfigForm()
case services.ConnectorIRC:
form = (&connectors.IRC{}).ConfigForm()
case services.ConnectorTwitter:
form = (&connectors.Twitter{}).ConfigForm()
default:
return c.Status(404).JSON(fiber.Map{
"error": "Connector type not found",
})
}
// Create a data structure to pass both the form and the connector type
data := fiber.Map{
"Form": form,
"Type": connectorType,
}
return c.Render("views/partials/metaform", data)
}
}

View File

@@ -1,39 +1,15 @@
// Common utility functions for agent forms
const AgentFormUtils = {
// Add dynamic component based on template
addDynamicComponent: function(sectionId, templateFunction, options = {}) {
addDynamicComponent: function(sectionId, templateFunction, dataItems) {
const section = document.getElementById(sectionId);
if (!section) return;
const newIndex = section.getElementsByClassName(dataItems.className).length;
const index = section.getElementsByClassName(options.className || 'dynamic-component').length;
const templateData = { index, ...options };
// Generate HTML from template function
const newHtml = templateFunction(newIndex, dataItems);
// Create a new element from the template
const tempDiv = document.createElement('div');
tempDiv.innerHTML = templateFunction(index, templateData);
const newElement = tempDiv.firstElementChild;
// Add the new element to the section
section.appendChild(newElement);
// If it's a connector, add event listener for type change
if (options.className === 'connector') {
const newIndex = index;
const connectorType = document.getElementById(`connectorType${newIndex}`);
if (connectorType) {
// Add event listener for future changes
connectorType.addEventListener('change', function() {
loadConnectorForm(newIndex, this.value, null);
});
// If a connector type is already selected (default value), load its form immediately
if (connectorType.value) {
loadConnectorForm(newIndex, connectorType.value, null);
}
}
}
return newElement;
// Add to DOM
section.insertAdjacentHTML('beforeend', newHtml);
},
// Process form data into JSON structure
@@ -84,41 +60,23 @@ const AgentFormUtils = {
const connectorsElements = document.getElementsByClassName('connector');
for (let i = 0; i < connectorsElements.length; i++) {
const typeSelect = document.getElementById(`connectorType${i}`);
const typeField = document.getElementById(`connectorType${i}`);
const configField = document.getElementById(`connectorConfig${i}`);
if (typeSelect) {
const connectorType = typeSelect.value;
const configContainer = document.getElementById(`connectorConfigContainer${i}`);
// Only process if we have a metaform
if (configContainer && configContainer.querySelector('.metaform')) {
if (typeField && configField) {
try {
// Get all form fields
const fields = configContainer.querySelectorAll('.connector-field');
let configObj = {};
// Validate JSON but send as string
const configValue = configField.value.trim() || '{}';
// Parse to validate but don't use the parsed object
JSON.parse(configValue);
// Process each field based on its type
fields.forEach(field => {
const fieldName = field.dataset.fieldName;
const fieldType = field.dataset.fieldType;
// Convert value based on field type
let value = field.value;
if (fieldType === 'number' && value !== '') {
value = parseFloat(value);
}
configObj[fieldName] = value;
});
// Add the connector to the list
connectors.push({
type: connectorType,
config: JSON.stringify(configObj)
type: typeField.value,
config: configValue // Send the raw string, not parsed JSON
});
} catch (err) {
console.error(`Error processing connector ${i} form:`, err);
showToast(`Error in connector ${i+1} configuration`, 'error');
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) {
@@ -129,13 +87,6 @@ const AgentFormUtils = {
return null; // Indicate validation error
}
} else {
// If no form is loaded, create an empty config
connectors.push({
type: connectorType,
config: '{}'
});
}
}
}
@@ -248,16 +199,14 @@ const AgentFormUtils = {
const parsed = JSON.parse(configValue);
configElement.value = JSON.stringify(parsed, null, 2);
} catch (e) {
// If parsing fails, use the raw string
configElement.value = configValue;
console.warn("Failed to parse config JSON string:", e);
configElement.value = configValue; // Keep as is if parsing fails
}
}
// If it's already an object, stringify it
else if (typeof configValue === 'object' && configValue !== null) {
} else if (configValue !== undefined && configValue !== null) {
// Direct object, just stringify with formatting
configElement.value = JSON.stringify(configValue, null, 2);
}
} else {
// Default to empty object
else {
configElement.value = '{}';
}
},
@@ -265,90 +214,21 @@ const AgentFormUtils = {
// Helper function to set select value (with fallback if option doesn't exist)
setSelectValue: function(selectElement, 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;
}
}
const optionExists = Array.from(selectElement.options).some(option => option.value === value);
// Set the value if the option exists
if (optionExists) {
selectElement.value = value;
} else if (selectElement.options.length > 0) {
// Otherwise select the first option
selectElement.selectedIndex = 0;
} 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;
}
}
};
// Function to load connector form based on type
function loadConnectorForm(index, connectorType, configData) {
if (!connectorType) return;
const configContainer = document.getElementById(`connectorConfigContainer${index}`);
if (!configContainer) return;
// Show loading indicator
configContainer.innerHTML = '<div class="loading-spinner">Loading form...</div>';
// Fetch the form for the selected connector type
fetch(`/settings/connector/form/${connectorType}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to load connector form');
}
return response.text();
})
.then(html => {
// Replace the container content with the form
configContainer.innerHTML = html;
// Store the connector type as a data attribute on the form
const metaform = configContainer.querySelector('.metaform');
if (metaform) {
metaform.setAttribute('data-connector-type', connectorType);
// Add a hidden input to store the connector type
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'connector-type';
hiddenInput.value = connectorType;
metaform.appendChild(hiddenInput);
// If we have config data, populate the form fields
if (configData) {
try {
// Parse the config JSON
const parsedConfig = JSON.parse(configData);
// Find all form fields
const fields = metaform.querySelectorAll('.connector-field');
// Populate each field with the corresponding value from the config
fields.forEach(field => {
const fieldName = field.dataset.fieldName;
if (parsedConfig[fieldName] !== undefined) {
field.value = parsedConfig[fieldName];
}
});
} catch (error) {
console.warn(`Failed to populate connector form for ${connectorType}:`, error);
}
}
}
})
.catch(error => {
console.error('Error loading connector form:', error);
configContainer.innerHTML = `
<div class="error-message">
<p>Failed to load connector form: ${error.message}</p>
</div>
`;
});
}
// HTML Templates for dynamic elements
const AgentFormTemplates = {
// Connector template
@@ -362,10 +242,9 @@ const AgentFormTemplates = {
${data.options}
</select>
</div>
<div id="connectorConfigContainer${index}" class="mb-4">
<div class="text-center py-4">
<p>Select a connector type to load its configuration form.</p>
</div>
<div class="mb-4">
<label for="connectorConfig${index}">Connector Config (JSON)</label>
<textarea id="connectorConfig${index}" name="connectors[${index}].config" placeholder='{"token":"sk-mg3.."}'>{}</textarea>
</div>
</div>
`;
@@ -377,12 +256,12 @@ const AgentFormTemplates = {
<div class="mcp_server mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>MCP Server ${index + 1}</h2>
<div class="mb-4">
<label for="mcpURL${index}">Server URL</label>
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}].url" placeholder="https://example.com">
<label for="mcpURL${index}">MCP Server URL</label>
<input type="text" id="mcpURL${index}" name="mcp_servers[${index}].url" placeholder='https://...'>
</div>
<div class="mb-4">
<label for="mcpToken${index}">API Token (Optional)</label>
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}].token" placeholder="API token">
<label for="mcpToken${index}">Bearer Token</label>
<input type="text" id="mcpToken${index}" name="mcp_servers[${index}].token" placeholder='Bearer token'>
</div>
</div>
`;
@@ -394,14 +273,14 @@ const AgentFormTemplates = {
<div class="action mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Action ${index + 1}</h2>
<div class="mb-4">
<label for="actionsName${index}">Action Type</label>
<label for="actionsName${index}">Action</label>
<select name="actions[${index}].name" id="actionsName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="actionsConfig${index}">Action Config (JSON)</label>
<textarea id="actionsConfig${index}" name="actions[${index}].config" placeholder='{"param":"value"}'>{}</textarea>
<textarea id="actionsConfig${index}" name="actions[${index}].config" placeholder='{"results":"5"}'>{}</textarea>
</div>
</div>
`;
@@ -413,14 +292,14 @@ const AgentFormTemplates = {
<div class="promptBlock mb-4 section-box" style="margin-top: 15px; padding: 15px;">
<h2>Prompt Block ${index + 1}</h2>
<div class="mb-4">
<label for="promptName${index}">Prompt Block Type</label>
<label for="promptName${index}">Block Prompt</label>
<select name="promptblocks[${index}].name" id="promptName${index}">
${data.options}
</select>
</div>
<div class="mb-4">
<label for="promptConfig${index}">Prompt Block Config (JSON)</label>
<textarea id="promptConfig${index}" name="promptblocks[${index}].config" placeholder='{"param":"value"}'>{}</textarea>
<label for="promptConfig${index}">Prompt Config (JSON)</label>
<textarea id="promptConfig${index}" name="promptblocks[${index}].config" placeholder='{"results":"5"}'>{}</textarea>
</div>
</div>
`;
@@ -465,39 +344,25 @@ function initAgentFormCommon(options = {}) {
});
}
// Add additional CSS for loading spinner and error messages
// Add additional CSS for checkbox labels
const style = document.createElement('style');
style.textContent = `
.loading-spinner {
.checkbox-label {
display: flex;
justify-content: center;
align-items: center;
height: 100px;
color: #f0f0f0;
}
.loading-spinner::after {
content: '';
width: 20px;
height: 20px;
border: 2px solid #f0f0f0;
border-top-color: transparent;
border-radius: 50%;
animation: spinner 1s linear infinite;
margin-left: 10px;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}
.error-message {
color: #ff5555;
padding: 10px;
border: 1px solid #ff5555;
border-radius: 4px;
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);
}

View File

@@ -113,7 +113,6 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
})
})
webapp.Get("/settings/connector/form/:type", app.GetConnectorForm(pool))
webapp.Get("/settings/:name", func(c *fiber.Ctx) error {
status := false
for _, a := range pool.List() {

View File

@@ -1,94 +0,0 @@
{{/*
Metaform Partial Template
This template renders a form based on a Form struct from the metaform package.
Usage:
{{template "partials/metaform.html" .}}
Where . contains Form and Type fields
*/}}
<div class="metaform" data-connector-type="{{.Type}}">
{{range .Form.Fields}}
<div class="mb-4">
<label for="connector_{{.Name}}">{{.Label}}{{if .Required}} <span class="required">*</span>{{end}}</label>
{{if eq .Kind "string"}}
<input type="text"
name="connector_field_{{.Name}}"
id="connector_{{.Name}}"
class="connector-field"
data-field-name="{{.Name}}"
data-field-type="string"
{{if .Placeholder}}placeholder="{{.Placeholder}}"{{end}}
{{if .Required}}required{{end}}>
{{else if eq .Kind "number"}}
<input type="number"
name="connector_field_{{.Name}}"
id="connector_{{.Name}}"
class="connector-field"
data-field-name="{{.Name}}"
data-field-type="number"
{{if .Placeholder}}placeholder="{{.Placeholder}}"{{end}}
{{if .Required}}required{{end}}>
{{else if eq .Kind "options"}}
<select name="connector_field_{{.Name}}"
id="connector_{{.Name}}"
class="connector-field"
data-field-name="{{.Name}}"
data-field-type="options"
{{if .Required}}required{{end}}>
<option value="">Select an option</option>
{{range .Options}}
<option value="{{.Value}}">{{.Label}}</option>
{{end}}
</select>
{{end}}
</div>
{{end}}
</div>
<style>
.metaform .required {
color: #ff5555;
}
.metaform input[type="text"],
.metaform input[type="number"],
.metaform select {
width: 100%;
padding: 10px;
background-color: #1a1a1a;
border: 1px solid #333;
color: #f0f0f0;
border-radius: 4px;
margin-top: 5px;
transition: border-color 0.3s, box-shadow 0.3s;
}
.metaform input[type="text"]:focus,
.metaform input[type="number"]:focus,
.metaform select:focus {
border-color: #00b3ff;
box-shadow: 0 0 5px rgba(0, 179, 255, 0.5);
outline: none;
}
.metaform select {
appearance: none;
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f0f0f0' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right 10px center;
background-size: 16px;
padding-right: 30px;
}
.metaform label {
display: block;
margin-bottom: 5px;
color: #f0f0f0;
font-weight: 500;
}
</style>

View File

@@ -190,16 +190,10 @@
updateButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Updating...';
updateButton.disabled = true;
// Get the agent name from the hidden input field
const agentName = document.getElementById('name').value;
// Build a structured data object
const formData = new FormData(form);
const jsonData = AgentFormUtils.processFormData(formData);
// Ensure the name is set correctly
jsonData.name = agentName;
// Process special fields
jsonData.connectors = AgentFormUtils.processConnectors(updateButton);
if (jsonData.connectors === null) return; // Validation failed
@@ -215,7 +209,7 @@
console.log('Sending data:', jsonData);
// Send the structured data as JSON
fetch(`/api/agent/${agentName}/config`, {
fetch(`/api/agent/${jsonData.name}/config`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
@@ -366,13 +360,16 @@
// Find the added connector elements
const connectorType = document.getElementById(`connectorType${index}`);
const connectorConfig = document.getElementById(`connectorConfig${index}`);
// Set connector type value
// Set values
if (connectorType) {
AgentFormUtils.setSelectValue(connectorType, connector.type);
}
// Load the connector form with the existing config data
loadConnectorForm(index, connector.type, connector.config);
if (connectorConfig) {
// Format the config value
AgentFormUtils.formatConfigValue(connectorConfig, connector.config);
}
});
}