feat(ui): Add React based UI for the vibes at /app

This adds a completely separate frontend based on React because I
found that code gen works better with React once the application gets
bigger. In particular it was getting very hard to move past add
connectors and actions.

The idea is to replace the standard UI with this once it has been
tested. But for now it is available at /app in addition to the
original at /

Signed-off-by: Richard Palethorpe <io@richiejp.com>
This commit is contained in:
Richard Palethorpe
2025-03-24 14:36:18 +00:00
parent 438a65caf6
commit 71e66c651c
61 changed files with 6452 additions and 2 deletions

View File

@@ -0,0 +1,208 @@
import React from 'react';
import FallbackAction from './actions/FallbackAction';
import GithubIssueLabelerAction from './actions/GithubIssueLabelerAction';
import GithubIssueOpenerAction from './actions/GithubIssueOpenerAction';
import GithubIssueCloserAction from './actions/GithubIssueCloserAction';
import GithubIssueCommenterAction from './actions/GithubIssueCommenterAction';
import GithubRepositoryAction from './actions/GithubRepositoryAction';
import TwitterPostAction from './actions/TwitterPostAction';
import SendMailAction from './actions/SendMailAction';
/**
* ActionForm component for configuring an action
*/
const ActionForm = ({ actions = [], onChange, onRemove, onAdd }) => {
// Available action types
const actionTypes = [
{ value: '', label: 'Select an action type' },
{ value: 'github-issue-labeler', label: 'GitHub Issue Labeler' },
{ value: 'github-issue-opener', label: 'GitHub Issue Opener' },
{ value: 'github-issue-closer', label: 'GitHub Issue Closer' },
{ value: 'github-issue-commenter', label: 'GitHub Issue Commenter' },
{ value: 'github-repository-get-content', label: 'GitHub Repository Get Content' },
{ value: 'github-repository-create-or-update-content', label: 'GitHub Repository Create/Update Content' },
{ value: 'github-readme', label: 'GitHub Readme' },
{ value: 'twitter-post', label: 'Twitter Post' },
{ value: 'send-mail', label: 'Send Email' },
{ value: 'search', label: 'Search' },
{ value: 'github-issue-searcher', label: 'GitHub Issue Searcher' },
{ value: 'github-issue-reader', label: 'GitHub Issue Reader' },
{ value: 'scraper', label: 'Web Scraper' },
{ value: 'wikipedia', label: 'Wikipedia' },
{ value: 'browse', label: 'Browse' },
{ value: 'generate_image', label: 'Generate Image' },
{ value: 'counter', label: 'Counter' },
{ value: 'call_agents', label: 'Call Agents' },
{ value: 'shell-command', label: 'Shell Command' },
{ value: 'custom', label: 'Custom' }
];
// Parse the config JSON string to an object
const parseConfig = (action) => {
if (!action || !action.config) return {};
try {
return JSON.parse(action.config || '{}');
} catch (error) {
console.error('Error parsing action config:', error);
return {};
}
};
// Get a value from the config object
const getConfigValue = (action, key, defaultValue = '') => {
const config = parseConfig(action);
return config[key] !== undefined ? config[key] : defaultValue;
};
// Update a value in the config object
const onActionConfigChange = (index, key, value) => {
const action = actions[index];
const config = parseConfig(action);
config[key] = value;
onChange(index, {
...action,
config: JSON.stringify(config)
});
};
// Handle action type change
const handleActionTypeChange = (index, value) => {
const action = actions[index];
onChange(index, {
...action,
name: value
});
};
// Render the appropriate action component based on the action type
const renderActionComponent = (action, index) => {
// Common props for all action components
const actionProps = {
index,
onActionConfigChange: (key, value) => onActionConfigChange(index, key, value),
getConfigValue: (key, defaultValue) => getConfigValue(action, key, defaultValue)
};
switch (action.name) {
case 'github-issue-labeler':
return <GithubIssueLabelerAction {...actionProps} />;
case 'github-issue-opener':
return <GithubIssueOpenerAction {...actionProps} />;
case 'github-issue-closer':
return <GithubIssueCloserAction {...actionProps} />;
case 'github-issue-commenter':
return <GithubIssueCommenterAction {...actionProps} />;
case 'github-repository-get-content':
case 'github-repository-create-or-update-content':
case 'github-readme':
return <GithubRepositoryAction {...actionProps} />;
case 'twitter-post':
return <TwitterPostAction {...actionProps} />;
case 'send-mail':
return <SendMailAction {...actionProps} />;
case 'generate_image':
return (
<div className="generate-image-action">
<div className="form-group mb-3">
<label htmlFor={`apiKey${index}`}>OpenAI API Key</label>
<input
type="text"
id={`apiKey${index}`}
value={getConfigValue(action, 'apiKey', '')}
onChange={(e) => onActionConfigChange(index, 'apiKey', e.target.value)}
className="form-control"
placeholder="sk-..."
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`apiURL${index}`}>API URL (Optional)</label>
<input
type="text"
id={`apiURL${index}`}
value={getConfigValue(action, 'apiURL', 'https://api.openai.com/v1')}
onChange={(e) => onActionConfigChange(index, 'apiURL', e.target.value)}
className="form-control"
placeholder="https://api.openai.com/v1"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`model${index}`}>Model</label>
<input
type="text"
id={`model${index}`}
value={getConfigValue(action, 'model', 'dall-e-3')}
onChange={(e) => onActionConfigChange(index, 'model', e.target.value)}
className="form-control"
placeholder="dall-e-3"
/>
<small className="form-text text-muted">Image generation model (e.g., dall-e-3)</small>
</div>
</div>
);
default:
return <FallbackAction {...actionProps} />;
}
};
// Render a specific action form
const renderActionForm = (action, index) => {
// Ensure action is an object with expected properties
const safeAction = action || {};
return (
<div key={index} className="connector-item mb-4">
<div className="connector-header">
<h4>Action #{index + 1}</h4>
<button
type="button"
className="remove-btn"
onClick={() => onRemove(index)}
>
<i className="fas fa-times"></i>
</button>
</div>
<div className="connector-type mb-3">
<label htmlFor={`actionType${index}`}>Action Type</label>
<select
id={`actionType${index}`}
value={safeAction.name || ''}
onChange={(e) => handleActionTypeChange(index, e.target.value)}
className="form-control"
>
{actionTypes.map((type) => (
<option key={type.value} value={type.value}>
{type.label}
</option>
))}
</select>
</div>
{/* Render specific action template based on type */}
{renderActionComponent(safeAction, index)}
</div>
);
};
return (
<div className="connectors-container">
{actions && actions.map((action, index) => (
renderActionForm(action, index)
))}
<button
type="button"
className="add-btn"
onClick={onAdd}
>
<i className="fas fa-plus"></i> Add Action
</button>
</div>
);
};
export default ActionForm;

View File

@@ -0,0 +1,321 @@
import React, { useState } from 'react';
import { useNavigate, useOutletContext } from 'react-router-dom';
// Import form sections
import BasicInfoSection from './agent-form-sections/BasicInfoSection';
import ConnectorsSection from './agent-form-sections/ConnectorsSection';
import ActionsSection from './agent-form-sections/ActionsSection';
import MCPServersSection from './agent-form-sections/MCPServersSection';
import MemorySettingsSection from './agent-form-sections/MemorySettingsSection';
import ModelSettingsSection from './agent-form-sections/ModelSettingsSection';
import PromptsGoalsSection from './agent-form-sections/PromptsGoalsSection';
import AdvancedSettingsSection from './agent-form-sections/AdvancedSettingsSection';
const AgentForm = ({
isEdit = false,
formData,
setFormData,
onSubmit,
loading = false,
submitButtonText,
isGroupForm = false,
noFormWrapper = false
}) => {
const navigate = useNavigate();
const { showToast } = useOutletContext();
const [activeSection, setActiveSection] = useState(isGroupForm ? 'model-section' : 'basic-section');
// Handle input changes
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
if (name.includes('.')) {
const [parent, child] = name.split('.');
setFormData({
...formData,
[parent]: {
...formData[parent],
[child]: type === 'checkbox' ? checked : value
}
});
} else {
setFormData({
...formData,
[name]: type === 'checkbox' ? checked : value
});
}
};
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
if (onSubmit) {
onSubmit(e);
}
};
// Handle navigation between sections
const handleSectionChange = (section) => {
setActiveSection(section);
};
// Handle adding a connector
const handleAddConnector = () => {
setFormData({
...formData,
connectors: [
...(formData.connectors || []),
{ name: '', config: '{}' }
]
});
};
// Handle removing a connector
const handleRemoveConnector = (index) => {
const updatedConnectors = [...formData.connectors];
updatedConnectors.splice(index, 1);
setFormData({
...formData,
connectors: updatedConnectors
});
};
// Handle connector name change
const handleConnectorNameChange = (index, value) => {
const updatedConnectors = [...formData.connectors];
updatedConnectors[index] = {
...updatedConnectors[index],
type: value
};
setFormData({
...formData,
connectors: updatedConnectors
});
};
// Handle connector config change
const handleConnectorConfigChange = (index, key, value) => {
const updatedConnectors = [...formData.connectors];
const currentConnector = updatedConnectors[index];
// Parse the current config if it's a string
let currentConfig = {};
if (typeof currentConnector.config === 'string') {
try {
currentConfig = JSON.parse(currentConnector.config);
} catch (err) {
console.error('Error parsing config:', err);
currentConfig = {};
}
} else if (currentConnector.config) {
currentConfig = currentConnector.config;
}
// Update the config with the new key-value pair
currentConfig = {
...currentConfig,
[key]: value
};
// Update the connector with the stringified config
updatedConnectors[index] = {
...currentConnector,
config: JSON.stringify(currentConfig)
};
setFormData({
...formData,
connectors: updatedConnectors
});
};
// Handle adding an MCP server
const handleAddMCPServer = () => {
setFormData({
...formData,
mcp_servers: [
...(formData.mcp_servers || []),
{ url: '' }
]
});
};
// Handle removing an MCP server
const handleRemoveMCPServer = (index) => {
const updatedMCPServers = [...formData.mcp_servers];
updatedMCPServers.splice(index, 1);
setFormData({
...formData,
mcp_servers: updatedMCPServers
});
};
// Handle MCP server change
const handleMCPServerChange = (index, value) => {
const updatedMCPServers = [...formData.mcp_servers];
updatedMCPServers[index] = { url: value };
setFormData({
...formData,
mcp_servers: updatedMCPServers
});
};
if (loading) {
return <div className="loading">Loading...</div>;
}
return (
<div className="agent-form-container">
{/* Wizard Sidebar */}
<div className="wizard-sidebar">
<ul className="wizard-nav">
{!isGroupForm && (
<li
className={`wizard-nav-item ${activeSection === 'basic-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('basic-section')}
>
<i className="fas fa-info-circle"></i>
Basic Information
</li>
)}
<li
className={`wizard-nav-item ${activeSection === 'model-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('model-section')}
>
<i className="fas fa-brain"></i>
Model Settings
</li>
<li
className={`wizard-nav-item ${activeSection === 'connectors-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('connectors-section')}
>
<i className="fas fa-plug"></i>
Connectors
</li>
<li
className={`wizard-nav-item ${activeSection === 'actions-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('actions-section')}
>
<i className="fas fa-bolt"></i>
Actions
</li>
<li
className={`wizard-nav-item ${activeSection === 'mcp-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('mcp-section')}
>
<i className="fas fa-server"></i>
MCP Servers
</li>
<li
className={`wizard-nav-item ${activeSection === 'memory-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('memory-section')}
>
<i className="fas fa-memory"></i>
Memory Settings
</li>
<li
className={`wizard-nav-item ${activeSection === 'prompts-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('prompts-section')}
>
<i className="fas fa-comment-alt"></i>
Prompts & Goals
</li>
<li
className={`wizard-nav-item ${activeSection === 'advanced-section' ? 'active' : ''}`}
onClick={() => handleSectionChange('advanced-section')}
>
<i className="fas fa-cogs"></i>
Advanced Settings
</li>
</ul>
</div>
{/* Form Content */}
<div className="form-content-area">
{noFormWrapper ? (
<div className='agent-form'>
{/* Form Sections */}
<div style={{ display: activeSection === 'basic-section' ? 'block' : 'none' }}>
<BasicInfoSection formData={formData} handleInputChange={handleInputChange} isEdit={isEdit} isGroupForm={isGroupForm} />
</div>
<div style={{ display: activeSection === 'model-section' ? 'block' : 'none' }}>
<ModelSettingsSection formData={formData} handleInputChange={handleInputChange} />
</div>
<div style={{ display: activeSection === 'connectors-section' ? 'block' : 'none' }}>
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorNameChange={handleConnectorNameChange} handleConnectorConfigChange={handleConnectorConfigChange} />
</div>
<div style={{ display: activeSection === 'actions-section' ? 'block' : 'none' }}>
<ActionsSection formData={formData} setFormData={setFormData} />
</div>
<div style={{ display: activeSection === 'mcp-section' ? 'block' : 'none' }}>
<MCPServersSection formData={formData} handleAddMCPServer={handleAddMCPServer} handleRemoveMCPServer={handleRemoveMCPServer} handleMCPServerChange={handleMCPServerChange} />
</div>
<div style={{ display: activeSection === 'memory-section' ? 'block' : 'none' }}>
<MemorySettingsSection formData={formData} handleInputChange={handleInputChange} />
</div>
<div style={{ display: activeSection === 'prompts-section' ? 'block' : 'none' }}>
<PromptsGoalsSection formData={formData} handleInputChange={handleInputChange} isGroupForm={isGroupForm} />
</div>
<div style={{ display: activeSection === 'advanced-section' ? 'block' : 'none' }}>
<AdvancedSettingsSection formData={formData} handleInputChange={handleInputChange} />
</div>
</div>
) : (
<form className="agent-form" onSubmit={handleSubmit}>
{/* Form Sections */}
<div style={{ display: activeSection === 'basic-section' ? 'block' : 'none' }}>
<BasicInfoSection formData={formData} handleInputChange={handleInputChange} isEdit={isEdit} isGroupForm={isGroupForm} />
</div>
<div style={{ display: activeSection === 'model-section' ? 'block' : 'none' }}>
<ModelSettingsSection formData={formData} handleInputChange={handleInputChange} />
</div>
<div style={{ display: activeSection === 'connectors-section' ? 'block' : 'none' }}>
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorNameChange={handleConnectorNameChange} handleConnectorConfigChange={handleConnectorConfigChange} />
</div>
<div style={{ display: activeSection === 'actions-section' ? 'block' : 'none' }}>
<ActionsSection formData={formData} setFormData={setFormData} />
</div>
<div style={{ display: activeSection === 'mcp-section' ? 'block' : 'none' }}>
<MCPServersSection formData={formData} handleAddMCPServer={handleAddMCPServer} handleRemoveMCPServer={handleRemoveMCPServer} handleMCPServerChange={handleMCPServerChange} />
</div>
<div style={{ display: activeSection === 'memory-section' ? 'block' : 'none' }}>
<MemorySettingsSection formData={formData} handleInputChange={handleInputChange} />
</div>
<div style={{ display: activeSection === 'prompts-section' ? 'block' : 'none' }}>
<PromptsGoalsSection formData={formData} handleInputChange={handleInputChange} isGroupForm={isGroupForm} />
</div>
<div style={{ display: activeSection === 'advanced-section' ? 'block' : 'none' }}>
<AdvancedSettingsSection formData={formData} handleInputChange={handleInputChange} />
</div>
{/* Form Controls */}
<div className="form-actions">
<button type="button" className="btn btn-secondary" onClick={() => navigate('/agents')}>
Cancel
</button>
<button type="submit" className="btn btn-primary" disabled={loading}>
{submitButtonText || (isEdit ? 'Update Agent' : 'Create Agent')}
</button>
</div>
</form>
)}
</div>
</div>
);
};
export default AgentForm;

View File

@@ -0,0 +1,138 @@
import { useState } from 'react';
// Import connector components
import TelegramConnector from './connectors/TelegramConnector';
import SlackConnector from './connectors/SlackConnector';
import DiscordConnector from './connectors/DiscordConnector';
import GithubIssuesConnector from './connectors/GithubIssuesConnector';
import GithubPRsConnector from './connectors/GithubPRsConnector';
import IRCConnector from './connectors/IRCConnector';
import TwitterConnector from './connectors/TwitterConnector';
import FallbackConnector from './connectors/FallbackConnector';
/**
* ConnectorForm component
* Provides specific form templates for different connector types
*/
function ConnectorForm({
connectors = [],
onAddConnector,
onRemoveConnector,
onConnectorNameChange,
onConnectorConfigChange
}) {
const [newConfigKey, setNewConfigKey] = useState('');
// Render a specific connector form based on its type
const renderConnectorForm = (connector, index) => {
// Ensure connector is an object with expected properties
const safeConnector = connector || {};
return (
<div key={index} className="connector-item mb-4">
<div className="connector-header">
<h4>Connector #{index + 1}</h4>
<button
type="button"
className="remove-btn"
onClick={() => onRemoveConnector(index)}
>
<i className="fas fa-times"></i>
</button>
</div>
<div className="connector-type mb-3">
<label htmlFor={`connectorName${index}`}>Connector Type</label>
<select
id={`connectorName${index}`}
value={safeConnector.type || ''}
onChange={(e) => onConnectorNameChange(index, e.target.value)}
className="form-control"
>
<option value="">Select a connector type</option>
<option value="telegram">Telegram</option>
<option value="slack">Slack</option>
<option value="discord">Discord</option>
<option value="github-issues">GitHub Issues</option>
<option value="github-prs">GitHub PRs</option>
<option value="irc">IRC</option>
<option value="twitter">Twitter</option>
<option value="custom">Custom</option>
</select>
</div>
{/* Render specific connector template based on type */}
{renderConnectorTemplate(safeConnector, index)}
</div>
);
};
// Get the appropriate form template based on connector type
const renderConnectorTemplate = (connector, index) => {
// Check if connector.type exists, if not use empty string to avoid errors
const connectorType = (connector.type || '').toLowerCase();
// Common props for all connector components
const connectorProps = {
connector,
index,
onConnectorConfigChange,
getConfigValue
};
switch (connectorType) {
case 'telegram':
return <TelegramConnector {...connectorProps} />;
case 'slack':
return <SlackConnector {...connectorProps} />;
case 'discord':
return <DiscordConnector {...connectorProps} />;
case 'github-issues':
return <GithubIssuesConnector {...connectorProps} />;
case 'github-prs':
return <GithubPRsConnector {...connectorProps} />;
case 'irc':
return <IRCConnector {...connectorProps} />;
case 'twitter':
return <TwitterConnector {...connectorProps} />;
default:
return <FallbackConnector {...connectorProps} />;
}
};
// Helper function to safely get config values
const getConfigValue = (connector, key, defaultValue = '') => {
if (!connector || !connector.config) return defaultValue;
// If config is a string (JSON), try to parse it
let config = connector.config;
if (typeof config === 'string') {
try {
config = JSON.parse(config);
} catch (err) {
console.error('Error parsing config:', err);
return defaultValue;
}
}
return config[key] !== undefined ? config[key] : defaultValue;
};
return (
<div className="connectors-container">
{connectors && connectors.map((connector, index) => (
renderConnectorForm(connector, index)
))}
<button
type="button"
className="add-btn"
onClick={onAddConnector}
>
<i className="fas fa-plus"></i> Add Connector
</button>
</div>
);
}
export default ConnectorForm;

View File

@@ -0,0 +1,16 @@
import React from 'react';
/**
* FallbackAction component for actions without specific configuration
*/
const FallbackAction = ({ index, onActionConfigChange, getConfigValue }) => {
return (
<div className="fallback-action">
<p className="text-muted">
This action doesn't require any additional configuration.
</p>
</div>
);
};
export default FallbackAction;

View File

@@ -0,0 +1,62 @@
import React from 'react';
/**
* GitHub Issue Closer action component
*/
const GithubIssueCloserAction = ({ index, onActionConfigChange, getConfigValue }) => {
return (
<div className="github-issue-closer-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="close_github_issue"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
);
};
export default GithubIssueCloserAction;

View File

@@ -0,0 +1,62 @@
import React from 'react';
/**
* GitHub Issue Commenter action component
*/
const GithubIssueCommenterAction = ({ index, onActionConfigChange, getConfigValue }) => {
return (
<div className="github-issue-commenter-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="comment_on_github_issue"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
);
};
export default GithubIssueCommenterAction;

View File

@@ -0,0 +1,75 @@
import React from 'react';
/**
* GitHub Issue Labeler action component
*/
const GithubIssueLabelerAction = ({ index, onActionConfigChange, getConfigValue }) => {
return (
<div className="github-issue-labeler-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`availableLabels${index}`}>Available Labels</label>
<input
type="text"
id={`availableLabels${index}`}
value={getConfigValue('availableLabels', 'bug,enhancement')}
onChange={(e) => onActionConfigChange('availableLabels', e.target.value)}
className="form-control"
placeholder="bug,enhancement,documentation"
/>
<small className="form-text text-muted">Comma-separated list of available labels</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="add_label_to_issue"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
);
};
export default GithubIssueLabelerAction;

View File

@@ -0,0 +1,62 @@
import React from 'react';
/**
* GitHub Issue Opener action component
*/
const GithubIssueOpenerAction = ({ index, onActionConfigChange, getConfigValue }) => {
return (
<div className="github-issue-opener-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="open_github_issue"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
);
};
export default GithubIssueOpenerAction;

View File

@@ -0,0 +1,66 @@
import React from 'react';
/**
* GitHub Repository action component for repository-related actions
* Used for:
* - github-repository-get-content
* - github-repository-create-or-update-content
* - github-readme
*/
const GithubRepositoryAction = ({ index, onActionConfigChange, getConfigValue }) => {
return (
<div className="github-repository-action">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue('owner', '')}
onChange={(e) => onActionConfigChange('owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue('repository', '')}
onChange={(e) => onActionConfigChange('repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
<input
type="text"
id={`customActionName${index}`}
value={getConfigValue('customActionName', '')}
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
className="form-control"
placeholder="github_repo_action"
/>
<small className="form-text text-muted">Custom name for this action (optional)</small>
</div>
</div>
);
};
export default GithubRepositoryAction;

View File

@@ -0,0 +1,75 @@
import React from 'react';
/**
* SendMail action component
*/
const SendMailAction = ({ index, onActionConfigChange, getConfigValue }) => {
return (
<div className="send-mail-action">
<div className="form-group mb-3">
<label htmlFor={`email${index}`}>Email</label>
<input
type="email"
id={`email${index}`}
value={getConfigValue('email', '')}
onChange={(e) => onActionConfigChange('email', e.target.value)}
className="form-control"
placeholder="your-email@example.com"
/>
<small className="form-text text-muted">Email address to send from</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`username${index}`}>Username</label>
<input
type="text"
id={`username${index}`}
value={getConfigValue('username', '')}
onChange={(e) => onActionConfigChange('username', e.target.value)}
className="form-control"
placeholder="SMTP username (often same as email)"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`password${index}`}>Password</label>
<input
type="password"
id={`password${index}`}
value={getConfigValue('password', '')}
onChange={(e) => onActionConfigChange('password', e.target.value)}
className="form-control"
placeholder="SMTP password or app password"
/>
<small className="form-text text-muted">For Gmail, use an app password</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`smtpHost${index}`}>SMTP Host</label>
<input
type="text"
id={`smtpHost${index}`}
value={getConfigValue('smtpHost', '')}
onChange={(e) => onActionConfigChange('smtpHost', e.target.value)}
className="form-control"
placeholder="smtp.gmail.com"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`smtpPort${index}`}>SMTP Port</label>
<input
type="text"
id={`smtpPort${index}`}
value={getConfigValue('smtpPort', '587')}
onChange={(e) => onActionConfigChange('smtpPort', e.target.value)}
className="form-control"
placeholder="587"
/>
<small className="form-text text-muted">Common ports: 587 (TLS), 465 (SSL)</small>
</div>
</div>
);
};
export default SendMailAction;

View File

@@ -0,0 +1,41 @@
import React from 'react';
/**
* Twitter Post action component
*/
const TwitterPostAction = ({ index, onActionConfigChange, getConfigValue }) => {
return (
<div className="twitter-post-action">
<div className="form-group mb-3">
<label htmlFor={`twitterToken${index}`}>Twitter API Token</label>
<input
type="text"
id={`twitterToken${index}`}
value={getConfigValue('token', '')}
onChange={(e) => onActionConfigChange('token', e.target.value)}
className="form-control"
placeholder="Twitter API token"
/>
<small className="form-text text-muted">Twitter API token with posting permissions</small>
</div>
<div className="form-group mb-3">
<div className="form-check">
<input
type="checkbox"
id={`noCharacterLimits${index}`}
checked={getConfigValue('noCharacterLimits', '') === 'true'}
onChange={(e) => onActionConfigChange('noCharacterLimits', e.target.checked ? 'true' : 'false')}
className="form-check-input"
/>
<label className="form-check-label" htmlFor={`noCharacterLimits${index}`}>
Disable character limit (280 characters)
</label>
<small className="form-text text-muted d-block">Enable to bypass the 280 character limit check</small>
</div>
</div>
</div>
);
};
export default TwitterPostAction;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import ActionForm from '../ActionForm';
/**
* ActionsSection component for the agent form
*/
const ActionsSection = ({ formData, setFormData }) => {
// Handle action change
const handleActionChange = (index, updatedAction) => {
const updatedActions = [...(formData.actions || [])];
updatedActions[index] = updatedAction;
setFormData({
...formData,
actions: updatedActions
});
};
// Handle action removal
const handleActionRemove = (index) => {
const updatedActions = [...(formData.actions || [])].filter((_, i) => i !== index);
setFormData({
...formData,
actions: updatedActions
});
};
// Handle adding an action
const handleAddAction = () => {
setFormData({
...formData,
actions: [
...(formData.actions || []),
{ name: '', config: '{}' }
]
});
};
return (
<div className="actions-section">
<h3>Actions</h3>
<p className="text-muted">
Configure actions that the agent can perform.
</p>
<ActionForm
actions={formData.actions || []}
onChange={handleActionChange}
onRemove={handleActionRemove}
onAdd={handleAddAction}
/>
</div>
);
};
export default ActionsSection;

View File

@@ -0,0 +1,84 @@
import React from 'react';
/**
* Advanced Settings section of the agent form
*/
const AdvancedSettingsSection = ({ formData, handleInputChange }) => {
return (
<div id="advanced-section">
<h3 className="section-title">Advanced Settings</h3>
<div className="mb-4">
<label htmlFor="max_steps">Max Steps</label>
<input
type="number"
name="max_steps"
id="max_steps"
min="1"
value={formData.max_steps || 10}
onChange={handleInputChange}
className="form-control"
/>
<small className="form-text text-muted">Maximum number of steps the agent can take</small>
</div>
<div className="mb-4">
<label htmlFor="max_iterations">Max Iterations</label>
<input
type="number"
name="max_iterations"
id="max_iterations"
min="1"
value={formData.max_iterations || 5}
onChange={handleInputChange}
className="form-control"
/>
<small className="form-text text-muted">Maximum number of iterations for each step</small>
</div>
<div className="mb-4">
<label htmlFor="autonomous" className="checkbox-label">
<input
type="checkbox"
name="autonomous"
id="autonomous"
checked={formData.autonomous || false}
onChange={handleInputChange}
/>
Autonomous Mode
</label>
<small className="form-text text-muted">Allow the agent to operate autonomously</small>
</div>
<div className="mb-4">
<label htmlFor="verbose" className="checkbox-label">
<input
type="checkbox"
name="verbose"
id="verbose"
checked={formData.verbose || false}
onChange={handleInputChange}
/>
Verbose Mode
</label>
<small className="form-text text-muted">Enable detailed logging</small>
</div>
<div className="mb-4">
<label htmlFor="allow_code_execution" className="checkbox-label">
<input
type="checkbox"
name="allow_code_execution"
id="allow_code_execution"
checked={formData.allow_code_execution || false}
onChange={handleInputChange}
/>
Allow Code Execution
</label>
<small className="form-text text-muted">Allow the agent to execute code (use with caution)</small>
</div>
</div>
);
};
export default AdvancedSettingsSection;

View File

@@ -0,0 +1,79 @@
import React from 'react';
/**
* Basic Information section of the agent form
*/
const BasicInfoSection = ({ formData, handleInputChange, isEdit, isGroupForm }) => {
// In group form context, we hide the basic info section entirely
if (isGroupForm) {
return null;
}
return (
<div id="basic-section">
<h3 className="section-title">Basic Information</h3>
<div className="mb-4">
<label htmlFor="name">Name</label>
<input
type="text"
name="name"
id="name"
value={formData.name || ''}
onChange={handleInputChange}
required
disabled={isEdit} // Disable name field in edit mode
/>
{isEdit && <small className="form-text text-muted">Agent name cannot be changed after creation</small>}
</div>
<div className="mb-4">
<label htmlFor="description">Description</label>
<textarea
name="description"
id="description"
value={formData.description || ''}
onChange={handleInputChange}
/>
</div>
<div className="mb-4">
<label htmlFor="identity_guidance">Identity Guidance</label>
<textarea
name="identity_guidance"
id="identity_guidance"
value={formData.identity_guidance || ''}
onChange={handleInputChange}
/>
</div>
<div className="mb-4">
<label htmlFor="random_identity" className="checkbox-label">
<input
type="checkbox"
name="random_identity"
id="random_identity"
checked={formData.random_identity || false}
onChange={handleInputChange}
/>
Random Identity
</label>
</div>
<div className="mb-4">
<label htmlFor="hud" className="checkbox-label">
<input
type="checkbox"
name="hud"
id="hud"
checked={formData.hud || false}
onChange={handleInputChange}
/>
HUD
</label>
</div>
</div>
);
};
export default BasicInfoSection;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import ConnectorForm from '../ConnectorForm';
/**
* Connectors section of the agent form
*/
const ConnectorsSection = ({
formData,
handleAddConnector,
handleRemoveConnector,
handleConnectorNameChange,
handleConnectorConfigChange
}) => {
return (
<div id="connectors-section">
<h3 className="section-title">Connectors</h3>
<p className="section-description">
Configure the connectors that this agent will use to communicate with external services.
</p>
<ConnectorForm
connectors={formData.connectors || []}
onAddConnector={handleAddConnector}
onRemoveConnector={handleRemoveConnector}
onConnectorNameChange={handleConnectorNameChange}
onConnectorConfigChange={handleConnectorConfigChange}
/>
</div>
);
};
export default ConnectorsSection;

View File

@@ -0,0 +1,36 @@
import React from 'react';
/**
* Navigation sidebar for the agent form
*/
const FormNavSidebar = ({ activeSection, handleSectionChange }) => {
// Define the navigation items
const navItems = [
{ id: 'basic-section', icon: 'fas fa-info-circle', label: 'Basic Information' },
{ id: 'connectors-section', icon: 'fas fa-plug', label: 'Connectors' },
{ id: 'actions-section', icon: 'fas fa-bolt', label: 'Actions' },
{ id: 'mcp-section', icon: 'fas fa-server', label: 'MCP Servers' },
{ id: 'memory-section', icon: 'fas fa-memory', label: 'Memory Settings' },
{ id: 'model-section', icon: 'fas fa-robot', label: 'Model Settings' },
{ id: 'prompts-section', icon: 'fas fa-comment-alt', label: 'Prompts & Goals' },
{ id: 'advanced-section', icon: 'fas fa-cogs', label: 'Advanced Settings' }
];
return (
<div className="wizard-sidebar">
<ul className="wizard-nav">
{navItems.map(item => (
<li
key={item.id}
className={`wizard-nav-item ${activeSection === item.id ? 'active' : ''}`}
onClick={() => handleSectionChange(item.id)}
>
<i className={item.icon}></i> {item.label}
</li>
))}
</ul>
</div>
);
};
export default FormNavSidebar;

View File

@@ -0,0 +1,70 @@
import React from 'react';
/**
* MCP Servers section of the agent form
*/
const MCPServersSection = ({
formData,
handleAddMCPServer,
handleRemoveMCPServer,
handleMCPServerChange
}) => {
return (
<div id="mcp-section">
<h3 className="section-title">MCP Servers</h3>
<p className="section-description">
Configure MCP servers for this agent.
</p>
<div className="mcp-servers-container">
{formData.mcp_servers && formData.mcp_servers.map((server, index) => (
<div key={index} className="mcp-server-item mb-4">
<div className="mcp-server-header">
<h4>MCP Server #{index + 1}</h4>
<button
type="button"
className="remove-btn"
onClick={() => handleRemoveMCPServer(index)}
>
<i className="fas fa-times"></i>
</button>
</div>
<div className="mb-3">
<label htmlFor={`mcp-url-${index}`}>URL</label>
<input
type="text"
id={`mcp-url-${index}`}
value={server.url || ''}
onChange={(e) => handleMCPServerChange(index, 'url', e.target.value)}
className="form-control"
placeholder="https://example.com/mcp"
/>
</div>
<div className="mb-3">
<label htmlFor={`mcp-api-key-${index}`}>API Key</label>
<input
type="password"
id={`mcp-api-key-${index}`}
value={server.api_key || ''}
onChange={(e) => handleMCPServerChange(index, 'api_key', e.target.value)}
className="form-control"
/>
</div>
</div>
))}
<button
type="button"
className="add-btn"
onClick={handleAddMCPServer}
>
<i className="fas fa-plus"></i> Add MCP Server
</button>
</div>
</div>
);
};
export default MCPServersSection;

View File

@@ -0,0 +1,70 @@
import React from 'react';
/**
* Memory Settings section of the agent form
*/
const MemorySettingsSection = ({ formData, handleInputChange }) => {
return (
<div id="memory-section">
<h3 className="section-title">Memory Settings</h3>
<div className="mb-4">
<label htmlFor="memory_provider">Memory Provider</label>
<select
name="memory_provider"
id="memory_provider"
value={formData.memory_provider || 'local'}
onChange={handleInputChange}
className="form-control"
>
<option value="local">Local</option>
<option value="redis">Redis</option>
<option value="postgres">PostgreSQL</option>
</select>
</div>
<div className="mb-4">
<label htmlFor="memory_collection">Memory Collection</label>
<input
type="text"
name="memory_collection"
id="memory_collection"
value={formData.memory_collection || ''}
onChange={handleInputChange}
className="form-control"
placeholder="agent_memories"
/>
</div>
<div className="mb-4">
<label htmlFor="memory_url">Memory URL</label>
<input
type="text"
name="memory_url"
id="memory_url"
value={formData.memory_url || ''}
onChange={handleInputChange}
className="form-control"
placeholder="redis://localhost:6379"
/>
<small className="form-text text-muted">Connection URL for Redis or PostgreSQL</small>
</div>
<div className="mb-4">
<label htmlFor="memory_window_size">Memory Window Size</label>
<input
type="number"
name="memory_window_size"
id="memory_window_size"
min="1"
value={formData.memory_window_size || 10}
onChange={handleInputChange}
className="form-control"
/>
<small className="form-text text-muted">Number of recent messages to include in context window</small>
</div>
</div>
);
};
export default MemorySettingsSection;

View File

@@ -0,0 +1,84 @@
import React from 'react';
/**
* Model Settings section of the agent form
*/
const ModelSettingsSection = ({ formData, handleInputChange }) => {
return (
<div id="model-section">
<h3 className="section-title">Model Settings</h3>
<div className="mb-4">
<label htmlFor="model">Model</label>
<input
type="text"
name="model"
id="model"
value={formData.model || ''}
onChange={handleInputChange}
/>
</div>
<div className="mb-4">
<label htmlFor="multimodal_model">Multimodal Model</label>
<input
type="text"
name="multimodal_model"
id="multimodal_model"
value={formData.multimodal_model || ''}
onChange={handleInputChange}
/>
</div>
<div className="mb-4">
<label htmlFor="api_url">API URL</label>
<input
type="text"
name="api_url"
id="api_url"
value={formData.api_url || ''}
onChange={handleInputChange}
/>
</div>
<div className="mb-4">
<label htmlFor="api_key">API Key</label>
<input
type="password"
name="api_key"
id="api_key"
value={formData.api_key || ''}
onChange={handleInputChange}
/>
</div>
<div className="mb-4">
<label htmlFor="temperature">Temperature</label>
<input
type="number"
name="temperature"
id="temperature"
min="0"
max="2"
step="0.1"
value={formData.temperature || 0.7}
onChange={handleInputChange}
/>
</div>
<div className="mb-4">
<label htmlFor="max_tokens">Max Tokens</label>
<input
type="number"
name="max_tokens"
id="max_tokens"
min="1"
value={formData.max_tokens || 2000}
onChange={handleInputChange}
/>
</div>
</div>
);
};
export default ModelSettingsSection;

View File

@@ -0,0 +1,69 @@
import React from 'react';
/**
* Prompts & Goals section of the agent form
*/
const PromptsGoalsSection = ({ formData, handleInputChange, isGroupForm }) => {
// In group form context, we hide the system prompt as it comes from each agent profile
return (
<div id="prompts-section">
<h3 className="section-title">Prompts & Goals</h3>
{!isGroupForm && (
<div className="mb-4">
<label htmlFor="system_prompt">System Prompt</label>
<textarea
name="system_prompt"
id="system_prompt"
value={formData.system_prompt || ''}
onChange={handleInputChange}
className="form-control"
rows="5"
/>
<small className="form-text text-muted">Instructions that define the agent's behavior</small>
</div>
)}
<div className="mb-4">
<label htmlFor="goals">Goals</label>
<textarea
name="goals"
id="goals"
value={formData.goals || ''}
onChange={handleInputChange}
className="form-control"
rows="5"
/>
<small className="form-text text-muted">Define the agent's goals (one per line)</small>
</div>
<div className="mb-4">
<label htmlFor="constraints">Constraints</label>
<textarea
name="constraints"
id="constraints"
value={formData.constraints || ''}
onChange={handleInputChange}
className="form-control"
rows="5"
/>
<small className="form-text text-muted">Define the agent's constraints (one per line)</small>
</div>
<div className="mb-4">
<label htmlFor="tools">Tools</label>
<textarea
name="tools"
id="tools"
value={formData.tools || ''}
onChange={handleInputChange}
className="form-control"
rows="5"
/>
<small className="form-text text-muted">Define the agent's tools (one per line)</small>
</div>
</div>
);
};
export default PromptsGoalsSection;

View File

@@ -0,0 +1,9 @@
export { default as BasicInfoSection } from './BasicInfoSection';
export { default as ModelSettingsSection } from './ModelSettingsSection';
export { default as ConnectorsSection } from './ConnectorsSection';
export { default as ActionsSection } from './ActionsSection';
export { default as MCPServersSection } from './MCPServersSection';
export { default as MemorySettingsSection } from './MemorySettingsSection';
export { default as PromptsGoalsSection } from './PromptsGoalsSection';
export { default as AdvancedSettingsSection } from './AdvancedSettingsSection';
export { default as FormNavSidebar } from './FormNavSidebar';

View File

@@ -0,0 +1,221 @@
/* Agent Form Section Styles */
.form-section {
padding: 1.5rem;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 1.5rem;
}
.section-title {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: #333;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.section-description {
color: #666;
margin-bottom: 1.5rem;
}
.hidden {
display: none;
}
.active {
display: block;
}
/* Form Controls */
.form-controls {
display: flex;
justify-content: flex-end;
margin-top: 2rem;
padding: 1rem;
background-color: #f8f9fa;
border-top: 1px solid #eee;
}
.submit-btn {
background-color: #4a6cf7;
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
transition: background-color 0.2s;
}
.submit-btn:hover {
background-color: #3a5ce5;
}
.submit-btn:disabled {
background-color: #a0a0a0;
cursor: not-allowed;
}
/* Error Message */
.error-message {
background-color: #f8d7da;
color: #721c24;
padding: 1rem;
border-radius: 4px;
margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.close-btn {
background: none;
border: none;
color: #721c24;
cursor: pointer;
font-size: 1rem;
}
/* Navigation Sidebar */
.wizard-sidebar {
width: 250px;
background-color: #f8f9fa;
border-right: 1px solid #eee;
padding: 1.5rem 0;
}
.wizard-nav {
list-style: none;
padding: 0;
margin: 0;
}
.wizard-nav-item {
padding: 0.75rem 1.5rem;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
align-items: center;
gap: 0.75rem;
}
.wizard-nav-item:hover {
background-color: #e9ecef;
}
.wizard-nav-item.active {
background-color: #e9ecef;
border-left: 4px solid #4a6cf7;
font-weight: 600;
}
.wizard-nav-item i {
width: 20px;
text-align: center;
}
/* Form Layout */
.agent-form-container {
display: flex;
min-height: 80vh;
border: 1px solid #eee;
border-radius: 8px;
overflow: hidden;
}
.form-content-area {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
/* Input Styles */
input[type="text"],
input[type="password"],
input[type="number"],
textarea,
select {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
margin-top: 0.25rem;
}
textarea {
min-height: 100px;
resize: vertical;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
margin: 0;
}
/* Add and Remove Buttons */
.add-btn,
.remove-btn {
background: none;
border: none;
cursor: pointer;
transition: color 0.2s;
}
.add-btn {
color: #4a6cf7;
font-weight: 600;
padding: 0.5rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.add-btn:hover {
color: #3a5ce5;
}
.remove-btn {
color: #dc3545;
}
.remove-btn:hover {
color: #c82333;
}
/* Item Containers */
.action-item,
.mcp-server-item {
border: 1px solid #eee;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
}
.action-header,
.mcp-server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.action-header h4,
.mcp-server-header h4 {
margin: 0;
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
/**
* Discord connector template
*/
const DiscordConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`discordToken${index}`}>Discord Bot Token</label>
<input
type="text"
id={`discordToken${index}`}
value={getConfigValue(connector, 'token', '')}
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
className="form-control"
placeholder="Bot token from Discord Developer Portal"
/>
<small className="form-text text-muted">Get this from the Discord Developer Portal</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`discordDefaultChannel${index}`}>Default Channel</label>
<input
type="text"
id={`discordDefaultChannel${index}`}
value={getConfigValue(connector, 'defaultChannel', '')}
onChange={(e) => onConnectorConfigChange(index, 'defaultChannel', e.target.value)}
className="form-control"
placeholder="123456789012345678"
/>
<small className="form-text text-muted">Channel ID to always answer even if not mentioned</small>
</div>
</div>
);
};
export default DiscordConnector;

View File

@@ -0,0 +1,71 @@
import React, { useState } from 'react';
/**
* Fallback connector template for unknown connector types
*/
const FallbackConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
const [newConfigKey, setNewConfigKey] = useState('');
// Parse config if it's a string
let parsedConfig = connector.config;
if (typeof parsedConfig === 'string') {
try {
parsedConfig = JSON.parse(parsedConfig);
} catch (err) {
console.error('Error parsing config:', err);
parsedConfig = {};
}
} else if (!parsedConfig) {
parsedConfig = {};
}
// Handle adding a new custom field
const handleAddCustomField = () => {
if (newConfigKey) {
onConnectorConfigChange(index, newConfigKey, '');
setNewConfigKey('');
}
};
return (
<div className="connector-template">
{/* Individual field inputs */}
{parsedConfig && Object.entries(parsedConfig).map(([key, value]) => (
<div key={key} className="form-group mb-3">
<label htmlFor={`connector-${index}-${key}`}>{key}</label>
<input
type="text"
id={`connector-${index}-${key}`}
className="form-control"
value={value}
onChange={(e) => onConnectorConfigChange(index, key, e.target.value)}
/>
</div>
))}
{/* Add custom configuration field */}
<div className="add-config-field mt-4">
<h5>Add Custom Configuration Field</h5>
<div className="input-group mb-3">
<input
type="text"
placeholder="New config key"
className="form-control"
value={newConfigKey}
onChange={(e) => setNewConfigKey(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddCustomField()}
/>
<button
type="button"
className="btn btn-outline-primary"
onClick={handleAddCustomField}
>
<i className="fas fa-plus"></i> Add Field
</button>
</div>
</div>
</div>
);
};
export default FallbackConnector;

View File

@@ -0,0 +1,75 @@
import React from 'react';
/**
* GitHub Issues connector template
*/
const GithubIssuesConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Personal Access Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue(connector, 'token', '')}
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue(connector, 'owner', '')}
onChange={(e) => onConnectorConfigChange(index, 'owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue(connector, 'repository', '')}
onChange={(e) => onConnectorConfigChange(index, 'repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`replyIfNoReplies${index}`}>Reply Behavior</label>
<select
id={`replyIfNoReplies${index}`}
value={getConfigValue(connector, 'replyIfNoReplies', 'false')}
onChange={(e) => onConnectorConfigChange(index, 'replyIfNoReplies', e.target.value)}
className="form-control"
>
<option value="false">Reply to all issues</option>
<option value="true">Only reply to issues with no comments</option>
</select>
</div>
<div className="form-group mb-3">
<label htmlFor={`pollInterval${index}`}>Poll Interval</label>
<input
type="text"
id={`pollInterval${index}`}
value={getConfigValue(connector, 'pollInterval', '10m')}
onChange={(e) => onConnectorConfigChange(index, 'pollInterval', e.target.value)}
className="form-control"
placeholder="10m"
/>
<small className="form-text text-muted">How often to check for new issues (e.g., 10m, 1h)</small>
</div>
</div>
);
};
export default GithubIssuesConnector;

View File

@@ -0,0 +1,75 @@
import React from 'react';
/**
* GitHub PRs connector template
*/
const GithubPRsConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`githubToken${index}`}>GitHub Personal Access Token</label>
<input
type="text"
id={`githubToken${index}`}
value={getConfigValue(connector, 'token', '')}
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
className="form-control"
placeholder="ghp_..."
/>
<small className="form-text text-muted">Personal access token with repo scope</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
<input
type="text"
id={`githubOwner${index}`}
value={getConfigValue(connector, 'owner', '')}
onChange={(e) => onConnectorConfigChange(index, 'owner', e.target.value)}
className="form-control"
placeholder="username or organization"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
<input
type="text"
id={`githubRepo${index}`}
value={getConfigValue(connector, 'repository', '')}
onChange={(e) => onConnectorConfigChange(index, 'repository', e.target.value)}
className="form-control"
placeholder="repository-name"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`replyIfNoReplies${index}`}>Reply Behavior</label>
<select
id={`replyIfNoReplies${index}`}
value={getConfigValue(connector, 'replyIfNoReplies', 'false')}
onChange={(e) => onConnectorConfigChange(index, 'replyIfNoReplies', e.target.value)}
className="form-control"
>
<option value="false">Reply to all PRs</option>
<option value="true">Only reply to PRs with no comments</option>
</select>
</div>
<div className="form-group mb-3">
<label htmlFor={`pollInterval${index}`}>Poll Interval</label>
<input
type="text"
id={`pollInterval${index}`}
value={getConfigValue(connector, 'pollInterval', '10m')}
onChange={(e) => onConnectorConfigChange(index, 'pollInterval', e.target.value)}
className="form-control"
placeholder="10m"
/>
<small className="form-text text-muted">How often to check for new PRs (e.g., 10m, 1h)</small>
</div>
</div>
);
};
export default GithubPRsConnector;

View File

@@ -0,0 +1,76 @@
import React from 'react';
/**
* IRC connector template
*/
const IRCConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`ircServer${index}`}>IRC Server</label>
<input
type="text"
id={`ircServer${index}`}
value={getConfigValue(connector, 'server', '')}
onChange={(e) => onConnectorConfigChange(index, 'server', e.target.value)}
className="form-control"
placeholder="irc.libera.chat"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`ircPort${index}`}>Port</label>
<input
type="text"
id={`ircPort${index}`}
value={getConfigValue(connector, 'port', '6667')}
onChange={(e) => onConnectorConfigChange(index, 'port', e.target.value)}
className="form-control"
placeholder="6667"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`ircNick${index}`}>Nickname</label>
<input
type="text"
id={`ircNick${index}`}
value={getConfigValue(connector, 'nickname', '')}
onChange={(e) => onConnectorConfigChange(index, 'nickname', e.target.value)}
className="form-control"
placeholder="MyAgentBot"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`ircChannels${index}`}>Channel</label>
<input
type="text"
id={`ircChannels${index}`}
value={getConfigValue(connector, 'channel', '')}
onChange={(e) => onConnectorConfigChange(index, 'channel', e.target.value)}
className="form-control"
placeholder="#channel1"
/>
<small className="form-text text-muted">Channel to join</small>
</div>
<div className="form-group mb-3">
<div className="form-check">
<label className="checkbox-label" htmlFor={`ircAlwaysReply${index}`}>
<input
type="checkbox"
id={`ircAlwaysReply${index}`}
checked={getConfigValue(connector, 'alwaysReply', '') === 'true'}
onChange={(e) => onConnectorConfigChange(index, 'alwaysReply', e.target.checked ? 'true' : 'false')}
/>
Always Reply
</label>
<small className="form-text text-muted d-block">If checked, the agent will reply to all messages in the channel</small>
</div>
</div>
</div>
);
};
export default IRCConnector;

View File

@@ -0,0 +1,67 @@
import React from 'react';
/**
* Slack connector template
*/
const SlackConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`slackAppToken${index}`}>Slack App Token</label>
<input
type="text"
id={`slackAppToken${index}`}
value={getConfigValue(connector, 'appToken', '')}
onChange={(e) => onConnectorConfigChange(index, 'appToken', e.target.value)}
className="form-control"
placeholder="xapp-..."
/>
<small className="form-text text-muted">App-level token starting with xapp-</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`slackBotToken${index}`}>Slack Bot Token</label>
<input
type="text"
id={`slackBotToken${index}`}
value={getConfigValue(connector, 'botToken', '')}
onChange={(e) => onConnectorConfigChange(index, 'botToken', e.target.value)}
className="form-control"
placeholder="xoxb-..."
/>
<small className="form-text text-muted">Bot token starting with xoxb-</small>
</div>
<div className="form-group mb-3">
<label htmlFor={`slackChannelID${index}`}>Slack Channel ID</label>
<input
type="text"
id={`slackChannelID${index}`}
value={getConfigValue(connector, 'channelID', '')}
onChange={(e) => onConnectorConfigChange(index, 'channelID', e.target.value)}
className="form-control"
placeholder="C1234567890"
/>
<small className="form-text text-muted">Optional: Specific channel ID to join</small>
</div>
<div className="form-group mb-3">
<div className="form-check">
<input
type="checkbox"
id={`slackAlwaysReply${index}`}
checked={getConfigValue(connector, 'alwaysReply', '') === 'true'}
onChange={(e) => onConnectorConfigChange(index, 'alwaysReply', e.target.checked ? 'true' : 'false')}
className="form-check-input"
/>
<label className="form-check-label" htmlFor={`slackAlwaysReply${index}`}>
Always Reply
</label>
<small className="form-text text-muted d-block">If checked, the agent will reply to all messages in the channel</small>
</div>
</div>
</div>
);
};
export default SlackConnector;

View File

@@ -0,0 +1,25 @@
import React from 'react';
/**
* Telegram connector template
*/
const TelegramConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`telegramToken${index}`}>Telegram Bot Token</label>
<input
type="text"
id={`telegramToken${index}`}
value={getConfigValue(connector, 'token', '')}
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
className="form-control"
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
/>
<small className="form-text text-muted">Get this from @BotFather on Telegram</small>
</div>
</div>
);
};
export default TelegramConnector;

View File

@@ -0,0 +1,72 @@
import React from 'react';
/**
* Twitter connector template
*/
const TwitterConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
return (
<div className="connector-template">
<div className="form-group mb-3">
<label htmlFor={`twitterApiKey${index}`}>API Key</label>
<input
type="text"
id={`twitterApiKey${index}`}
value={getConfigValue(connector, 'apiKey', '')}
onChange={(e) => onConnectorConfigChange(index, 'apiKey', e.target.value)}
className="form-control"
placeholder="Twitter API Key"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`twitterApiSecret${index}`}>API Secret</label>
<input
type="password"
id={`twitterApiSecret${index}`}
value={getConfigValue(connector, 'apiSecret', '')}
onChange={(e) => onConnectorConfigChange(index, 'apiSecret', e.target.value)}
className="form-control"
placeholder="Twitter API Secret"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`twitterAccessToken${index}`}>Access Token</label>
<input
type="text"
id={`twitterAccessToken${index}`}
value={getConfigValue(connector, 'accessToken', '')}
onChange={(e) => onConnectorConfigChange(index, 'accessToken', e.target.value)}
className="form-control"
placeholder="Twitter Access Token"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`twitterAccessSecret${index}`}>Access Token Secret</label>
<input
type="password"
id={`twitterAccessSecret${index}`}
value={getConfigValue(connector, 'accessSecret', '')}
onChange={(e) => onConnectorConfigChange(index, 'accessSecret', e.target.value)}
className="form-control"
placeholder="Twitter Access Token Secret"
/>
</div>
<div className="form-group mb-3">
<label htmlFor={`twitterBearerToken${index}`}>Bearer Token</label>
<input
type="password"
id={`twitterBearerToken${index}`}
value={getConfigValue(connector, 'bearerToken', '')}
onChange={(e) => onConnectorConfigChange(index, 'bearerToken', e.target.value)}
className="form-control"
placeholder="Twitter Bearer Token"
/>
</div>
</div>
);
};
export default TwitterConnector;