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:
208
webui/react-ui/src/components/ActionForm.jsx
Normal file
208
webui/react-ui/src/components/ActionForm.jsx
Normal 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;
|
||||
321
webui/react-ui/src/components/AgentForm.jsx
Normal file
321
webui/react-ui/src/components/AgentForm.jsx
Normal 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;
|
||||
138
webui/react-ui/src/components/ConnectorForm.jsx
Normal file
138
webui/react-ui/src/components/ConnectorForm.jsx
Normal 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;
|
||||
16
webui/react-ui/src/components/actions/FallbackAction.jsx
Normal file
16
webui/react-ui/src/components/actions/FallbackAction.jsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
75
webui/react-ui/src/components/actions/SendMailAction.jsx
Normal file
75
webui/react-ui/src/components/actions/SendMailAction.jsx
Normal 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;
|
||||
41
webui/react-ui/src/components/actions/TwitterPostAction.jsx
Normal file
41
webui/react-ui/src/components/actions/TwitterPostAction.jsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
9
webui/react-ui/src/components/agent-form-sections/index.js
vendored
Normal file
9
webui/react-ui/src/components/agent-form-sections/index.js
vendored
Normal 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';
|
||||
221
webui/react-ui/src/components/agent-form-sections/styles.css
Normal file
221
webui/react-ui/src/components/agent-form-sections/styles.css
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
76
webui/react-ui/src/components/connectors/IRCConnector.jsx
Normal file
76
webui/react-ui/src/components/connectors/IRCConnector.jsx
Normal 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;
|
||||
67
webui/react-ui/src/components/connectors/SlackConnector.jsx
Normal file
67
webui/react-ui/src/components/connectors/SlackConnector.jsx
Normal 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user