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

3
.gitignore vendored
View File

@@ -3,4 +3,5 @@ data/
pool
uploads/
local-agent-framework
localagent
localagent
**/.env

View File

@@ -1,6 +1,24 @@
# Define argument for linker flags
ARG LDFLAGS=-s -w
# Use Bun container for building the React UI
FROM oven/bun:1 as ui-builder
# Set the working directory for the React UI
WORKDIR /app
# Copy package.json and bun.lockb (if exists)
COPY webui/react-ui/package.json webui/react-ui/bun.lockb* ./
# Install dependencies
RUN bun install --frozen-lockfile
# Copy the rest of the React UI source code
COPY webui/react-ui/ ./
# Build the React UI
RUN bun run build
# Use a temporary build image based on Golang 1.22-alpine
FROM golang:1.22-alpine as builder
@@ -23,6 +41,9 @@ RUN go mod download
# Now copy the rest of the source code
COPY . .
# Copy the built React UI from the ui-builder stage
COPY --from=ui-builder /app/dist /work/webui/react-ui/dist
# Build the application
RUN go build -ldflags="$LDFLAGS" -o localagent ./
@@ -33,6 +54,5 @@ COPY --from=builder /work/localagent /localagent
COPY --from=builder /etc/ssl/ /etc/ssl/
COPY --from=builder /tmp /tmp
# Define the command that will be run when the container is started
ENTRYPOINT ["/localagent"]

View File

@@ -104,6 +104,7 @@ Download ready-to-run binaries from the [Releases](https://github.com/mudler/Loc
Requirements:
- Go 1.20+
- Git
- Bun 1.2+
```bash
# Clone repo
@@ -111,6 +112,8 @@ git clone https://github.com/mudler/LocalAgent.git
cd LocalAgent
# Build it
cd webui/react-ui && bun i && bun run build
cd ../..
go build -o localagent
# Run it

24
webui/react-ui/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

12
webui/react-ui/README.md Normal file
View File

@@ -0,0 +1,12 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript and enable type-aware lint rules. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.

BIN
webui/react-ui/bun.lockb Executable file

Binary file not shown.

View File

@@ -0,0 +1,33 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
},
},
]

13
webui/react-ui/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

View File

@@ -0,0 +1,28 @@
{
"name": "react-ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/js": "^9.21.0",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"eslint": "^9.21.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^15.15.0",
"react-router-dom": "^7.4.0",
"vite": "^6.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1657
webui/react-ui/src/App.css Normal file

File diff suppressed because it is too large Load Diff

127
webui/react-ui/src/App.jsx Normal file
View File

@@ -0,0 +1,127 @@
import { useState } from 'react'
import { Outlet, Link } from 'react-router-dom'
import './App.css'
function App() {
const [toast, setToast] = useState({ visible: false, message: '', type: 'success' });
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
// Show toast notification
const showToast = (message, type = 'success') => {
setToast({ visible: true, message, type });
setTimeout(() => {
setToast({ visible: false, message: '', type: 'success' });
}, 3000);
};
// Toggle mobile menu
const toggleMobileMenu = () => {
setMobileMenuOpen(!mobileMenuOpen);
};
return (
<div className="app-container">
{/* Navigation Menu */}
<nav className="main-nav" style={{ backgroundColor: 'var(--darker-bg)', borderBottom: '1px solid var(--medium-bg)', zIndex: 10 }}>
<div className="container">
<div className="nav-content">
<div className="logo-container">
{/* Logo with glow effect */}
<Link to="/" className="logo-link">
<div className="logo-image-container">
<img src="/app/logo_1.png" alt="Logo" className="logo-image" />
</div>
<span className="logo-text">LocalAgent</span>
</Link>
</div>
<div className="desktop-menu">
<ul className="nav-links">
<li>
<Link to="/" className="nav-link">
<i className="fas fa-home mr-2"></i> Home
</Link>
</li>
<li>
<Link to="/agents" className="nav-link">
<i className="fas fa-users mr-2"></i> Agent List
</Link>
</li>
<li>
<Link to="/actions-playground" className="nav-link">
<i className="fas fa-bolt mr-2"></i> Actions Playground
</Link>
</li>
<li>
<Link to="/group-create" className="nav-link">
<i className="fas fa-users-cog mr-2"></i> Create Agent Group
</Link>
</li>
</ul>
</div>
<div className="">
<span className="status-indicator"></span>
<span className="status-text">State: <span className="status-value">active</span></span>
</div>
<div className="mobile-menu-toggle" onClick={toggleMobileMenu}>
<i className="fas fa-bars"></i>
</div>
</div>
</div>
</nav>
{/* Mobile Menu */}
{mobileMenuOpen && (
<div className="mobile-menu" style={{ backgroundColor: 'var(--darker-bg)', borderTop: '1px solid var(--medium-bg)' }}>
<ul className="mobile-nav-links">
<li>
<Link to="/" className="mobile-nav-link" onClick={() => setMobileMenuOpen(false)}>
<i className="fas fa-home mr-2"></i> Home
</Link>
</li>
<li>
<Link to="/agents" className="mobile-nav-link" onClick={() => setMobileMenuOpen(false)}>
<i className="fas fa-users mr-2"></i> Agent List
</Link>
</li>
<li>
<Link to="/actions-playground" className="mobile-nav-link" onClick={() => setMobileMenuOpen(false)}>
<i className="fas fa-bolt mr-2"></i> Actions Playground
</Link>
</li>
<li>
<Link to="/group-create" className="mobile-nav-link" onClick={() => setMobileMenuOpen(false)}>
<i className="fas fa-users-cog mr-2"></i> Create Agent Group
</Link>
</li>
</ul>
</div>
)}
{/* Toast Notification */}
{toast.visible && (
<div className={`toast ${toast.type}`}>
<span>{toast.message}</span>
</div>
)}
{/* Main Content Area */}
<main className="main-content">
<div className="container">
<Outlet context={{ showToast }} />
</div>
</main>
{/* Footer */}
<footer className="main-footer">
<div className="container">
<p>&copy; {new Date().getFullYear()} LocalAgent - Cybernetic Intelligence</p>
</div>
</footer>
</div>
)
}
export default App

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

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;

112
webui/react-ui/src/hooks/useAgent.js vendored Normal file
View File

@@ -0,0 +1,112 @@
import { useState, useEffect, useCallback } from 'react';
import { agentApi } from '../utils/api';
/**
* Custom hook for managing agent state
* @param {string} agentName - Name of the agent to manage
* @returns {Object} - Agent state and management functions
*/
export function useAgent(agentName) {
const [agent, setAgent] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// Fetch agent configuration
const fetchAgent = useCallback(async () => {
if (!agentName) return;
setLoading(true);
setError(null);
try {
const config = await agentApi.getAgentConfig(agentName);
setAgent(config);
} catch (err) {
setError(err.message || 'Failed to fetch agent configuration');
console.error('Error fetching agent:', err);
} finally {
setLoading(false);
}
}, [agentName]);
// Update agent configuration
const updateAgent = useCallback(async (config) => {
if (!agentName) return;
setLoading(true);
setError(null);
try {
await agentApi.updateAgentConfig(agentName, config);
// Refresh agent data after update
await fetchAgent();
return true;
} catch (err) {
setError(err.message || 'Failed to update agent configuration');
console.error('Error updating agent:', err);
return false;
} finally {
setLoading(false);
}
}, [agentName, fetchAgent]);
// Toggle agent status (pause/start)
const toggleAgentStatus = useCallback(async (isActive) => {
if (!agentName) return;
setLoading(true);
setError(null);
try {
if (isActive) {
await agentApi.pauseAgent(agentName);
} else {
await agentApi.startAgent(agentName);
}
// Refresh agent data after status change
await fetchAgent();
return true;
} catch (err) {
setError(err.message || 'Failed to toggle agent status');
console.error('Error toggling agent status:', err);
return false;
} finally {
setLoading(false);
}
}, [agentName, fetchAgent]);
// Delete agent
const deleteAgent = useCallback(async () => {
if (!agentName) return;
setLoading(true);
setError(null);
try {
await agentApi.deleteAgent(agentName);
setAgent(null);
return true;
} catch (err) {
setError(err.message || 'Failed to delete agent');
console.error('Error deleting agent:', err);
return false;
} finally {
setLoading(false);
}
}, [agentName]);
// Load agent data on mount or when agentName changes
useEffect(() => {
fetchAgent();
}, [agentName, fetchAgent]);
return {
agent,
loading,
error,
fetchAgent,
updateAgent,
toggleAgentStatus,
deleteAgent,
};
}

78
webui/react-ui/src/hooks/useChat.js vendored Normal file
View File

@@ -0,0 +1,78 @@
import { useState, useCallback, useEffect } from 'react';
import { chatApi } from '../utils/api';
import { useSSE } from './useSSE';
/**
* Custom hook for chat functionality
* @param {string} agentName - Name of the agent to chat with
* @returns {Object} - Chat state and functions
*/
export function useChat(agentName) {
const [messages, setMessages] = useState([]);
const [sending, setSending] = useState(false);
const [error, setError] = useState(null);
// Use SSE hook to receive real-time messages
const { data: sseData, isConnected } = useSSE(agentName);
// Process SSE data into messages
useEffect(() => {
if (sseData && sseData.length > 0) {
// Process the latest SSE data
const latestData = sseData[sseData.length - 1];
if (latestData.type === 'message') {
setMessages(prev => [...prev, {
id: Date.now().toString(),
sender: 'agent',
content: latestData.content,
timestamp: new Date().toISOString(),
}]);
}
}
}, [sseData]);
// Send a message to the agent
const sendMessage = useCallback(async (content) => {
if (!agentName || !content) return;
setSending(true);
setError(null);
// Add user message to the list
const userMessage = {
id: Date.now().toString(),
sender: 'user',
content,
timestamp: new Date().toISOString(),
};
setMessages(prev => [...prev, userMessage]);
try {
await chatApi.sendMessage(agentName, content);
// The agent's response will come through SSE
return true;
} catch (err) {
setError(err.message || 'Failed to send message');
console.error('Error sending message:', err);
return false;
} finally {
setSending(false);
}
}, [agentName]);
// Clear chat history
const clearChat = useCallback(() => {
setMessages([]);
}, []);
return {
messages,
sending,
error,
isConnected,
sendMessage,
clearChat,
};
}

63
webui/react-ui/src/hooks/useSSE.js vendored Normal file
View File

@@ -0,0 +1,63 @@
import { useState, useEffect } from 'react';
import { API_CONFIG } from '../utils/config';
/**
* Helper function to build a full URL
* @param {string} endpoint - API endpoint
* @returns {string} - Full URL
*/
const buildUrl = (endpoint) => {
return `${API_CONFIG.baseUrl}${endpoint.startsWith('/') ? endpoint.substring(1) : endpoint}`;
};
/**
* Custom hook for handling Server-Sent Events (SSE)
* @param {string} agentName - Name of the agent to connect to
* @returns {Object} - SSE data and connection status
*/
export function useSSE(agentName) {
const [data, setData] = useState([]);
const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
if (!agentName) return;
// Create EventSource for SSE connection
const eventSource = new EventSource(buildUrl(API_CONFIG.endpoints.sse(agentName)));
// Connection opened
eventSource.onopen = () => {
setIsConnected(true);
setError(null);
};
// Handle incoming messages
eventSource.onmessage = (event) => {
try {
const parsedData = JSON.parse(event.data);
setData((prevData) => [...prevData, parsedData]);
} catch (err) {
console.error('Error parsing SSE data:', err);
}
};
// Handle errors
eventSource.onerror = (err) => {
setIsConnected(false);
setError('SSE connection error');
console.error('SSE connection error:', err);
};
// Clean up on unmount
return () => {
eventSource.close();
setIsConnected(false);
};
}, [agentName]);
// Function to clear the data
const clearData = () => setData([]);
return { data, isConnected, error, clearData };
}

View File

@@ -0,0 +1,68 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -0,0 +1,24 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { router } from './router'
import './index.css'
import './App.css'
// Add the Google Fonts for the cyberpunk styling
const fontLink = document.createElement('link');
fontLink.rel = 'stylesheet';
fontLink.href = 'https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;700&family=Permanent+Marker&display=swap';
document.head.appendChild(fontLink);
// Add Font Awesome for icons
const fontAwesomeLink = document.createElement('link');
fontAwesomeLink.rel = 'stylesheet';
fontAwesomeLink.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css';
document.head.appendChild(fontAwesomeLink);
createRoot(document.getElementById('root')).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>,
)

View File

@@ -0,0 +1,204 @@
import { useState, useEffect } from 'react';
import { useOutletContext } from 'react-router-dom';
import { actionApi } from '../utils/api';
function ActionsPlayground() {
const { showToast } = useOutletContext();
const [actions, setActions] = useState([]);
const [selectedAction, setSelectedAction] = useState('');
const [configJson, setConfigJson] = useState('{}');
const [paramsJson, setParamsJson] = useState('{}');
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const [loadingActions, setLoadingActions] = useState(true);
// Fetch available actions
useEffect(() => {
const fetchActions = async () => {
try {
const response = await actionApi.listActions();
setActions(response);
} catch (err) {
console.error('Error fetching actions:', err);
showToast('Failed to load actions', 'error');
} finally {
setLoadingActions(false);
}
};
fetchActions();
}, [showToast]);
// Handle action selection
const handleActionChange = (e) => {
setSelectedAction(e.target.value);
setResult(null);
};
// Handle JSON input changes
const handleConfigChange = (e) => {
setConfigJson(e.target.value);
};
const handleParamsChange = (e) => {
setParamsJson(e.target.value);
};
// Execute the selected action
const handleExecuteAction = async (e) => {
e.preventDefault();
if (!selectedAction) {
showToast('Please select an action', 'warning');
return;
}
setLoading(true);
setResult(null);
try {
// Parse JSON inputs
let config = {};
let params = {};
try {
config = JSON.parse(configJson);
} catch (err) {
showToast('Invalid configuration JSON', 'error');
setLoading(false);
return;
}
try {
params = JSON.parse(paramsJson);
} catch (err) {
showToast('Invalid parameters JSON', 'error');
setLoading(false);
return;
}
// Prepare action data
const actionData = {
action: selectedAction,
config: config,
params: params
};
// Execute action
const response = await actionApi.executeAction(selectedAction, actionData);
setResult(response);
showToast('Action executed successfully', 'success');
} catch (err) {
console.error('Error executing action:', err);
showToast(`Failed to execute action: ${err.message}`, 'error');
} finally {
setLoading(false);
}
};
return (
<div className="actions-playground-container">
<header className="page-header">
<h1>Actions Playground</h1>
<p>Test and execute actions directly from the UI</p>
</header>
<div className="actions-playground-content">
<div className="section-box">
<h2>Select an Action</h2>
<div className="form-group mb-4">
<label htmlFor="action-select">Available Actions:</label>
<select
id="action-select"
value={selectedAction}
onChange={handleActionChange}
className="form-control"
disabled={loadingActions}
>
<option value="">-- Select an action --</option>
{actions.map((action) => (
<option key={action} value={action}>{action}</option>
))}
</select>
</div>
</div>
{selectedAction && (
<div className="section-box">
<h2>Action Configuration</h2>
<form onSubmit={handleExecuteAction}>
<div className="form-group mb-6">
<label htmlFor="config-json">Configuration (JSON):</label>
<textarea
id="config-json"
value={configJson}
onChange={handleConfigChange}
className="form-control"
rows="5"
placeholder='{"key": "value"}'
/>
<p className="text-xs text-gray-400 mt-1">Enter JSON configuration for the action</p>
</div>
<div className="form-group mb-6">
<label htmlFor="params-json">Parameters (JSON):</label>
<textarea
id="params-json"
value={paramsJson}
onChange={handleParamsChange}
className="form-control"
rows="5"
placeholder='{"key": "value"}'
/>
<p className="text-xs text-gray-400 mt-1">Enter JSON parameters for the action</p>
</div>
<div className="flex justify-end">
<button
type="submit"
className="action-btn"
disabled={loading}
>
{loading ? (
<><i className="fas fa-spinner fa-spin"></i> Executing...</>
) : (
<><i className="fas fa-play"></i> Execute Action</>
)}
</button>
</div>
</form>
</div>
)}
{result && (
<div className="section-box">
<h2>Action Results</h2>
<div className="result-container" style={{
maxHeight: '400px',
overflow: 'auto',
border: '1px solid rgba(94, 0, 255, 0.2)',
borderRadius: '4px',
padding: '10px',
backgroundColor: 'rgba(30, 30, 30, 0.7)'
}}>
{typeof result === 'object' ? (
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{JSON.stringify(result, null, 2)}
</pre>
) : (
<pre style={{ margin: 0, whiteSpace: 'pre-wrap', wordBreak: 'break-word' }}>
{result}
</pre>
)}
</div>
</div>
)}
</div>
</div>
);
}
export default ActionsPlayground;

View File

@@ -0,0 +1,172 @@
import { useState, useEffect } from 'react';
import { useParams, useOutletContext, useNavigate } from 'react-router-dom';
import { useAgent } from '../hooks/useAgent';
import AgentForm from '../components/AgentForm';
function AgentSettings() {
const { name } = useParams();
const { showToast } = useOutletContext();
const navigate = useNavigate();
const [formData, setFormData] = useState({
name: '',
description: '',
identity_guidance: '',
random_identity: false,
hud: false,
model: '',
multimodal_model: '',
api_url: '',
api_key: '',
local_rag_url: '',
local_rag_api_key: '',
enable_reasoning: false,
enable_kb: false,
kb_results: 3,
long_term_memory: false,
summary_long_term_memory: false,
connectors: [],
actions: [],
mcp_servers: [],
system_prompt: '',
user_prompt: '',
goals: '',
standalone_job: false,
standalone_job_interval: 60,
avatar: '',
avatar_seed: '',
avatar_style: 'default',
});
// Use our custom agent hook
const {
agent,
loading,
error,
updateAgent,
toggleAgentStatus,
deleteAgent
} = useAgent(name);
// Load agent data when component mounts
useEffect(() => {
if (agent) {
setFormData({
...formData,
...agent,
name: name // Ensure name is set correctly
});
}
}, [agent]);
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
try {
const success = await updateAgent(formData);
if (success) {
showToast('Agent updated successfully', 'success');
}
} catch (err) {
showToast(`Error updating agent: ${err.message}`, 'error');
}
};
// Handle agent toggle (pause/start)
const handleToggleStatus = async () => {
const isActive = agent?.active || false;
try {
const success = await toggleAgentStatus(isActive);
if (success) {
const action = isActive ? 'paused' : 'started';
showToast(`Agent "${name}" ${action} successfully`, 'success');
}
} catch (err) {
showToast(`Error toggling agent status: ${err.message}`, 'error');
}
};
// Handle agent deletion
const handleDelete = async () => {
if (!confirm(`Are you sure you want to delete agent "${name}"? This action cannot be undone.`)) {
return;
}
try {
const success = await deleteAgent();
if (success) {
showToast(`Agent "${name}" deleted successfully`, 'success');
navigate('/agents');
}
} catch (err) {
showToast(`Error deleting agent: ${err.message}`, 'error');
}
};
if (loading && !agent) {
return (
<div className="settings-container">
<div className="loading">
<i className="fas fa-spinner fa-spin"></i>
<p>Loading agent settings...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="settings-container">
<div className="error">
<i className="fas fa-exclamation-triangle"></i>
<p>{error}</p>
</div>
</div>
);
}
return (
<div className="settings-container">
<header className="page-header">
<h1>
<i className="fas fa-cog"></i> Agent Settings - {name}
</h1>
<div className="header-actions">
<button
className={`action-btn ${agent?.active ? 'warning' : 'success'}`}
onClick={handleToggleStatus}
>
{agent?.active ? (
<><i className="fas fa-pause"></i> Pause Agent</>
) : (
<><i className="fas fa-play"></i> Start Agent</>
)}
</button>
<button
className="action-btn delete-btn"
onClick={handleDelete}
>
<i className="fas fa-trash"></i> Delete Agent
</button>
</div>
</header>
<div className="settings-content">
{/* Agent Configuration Form Section */}
<div className="section-box">
<AgentForm
isEdit={true}
formData={formData}
setFormData={setFormData}
onSubmit={handleSubmit}
loading={loading}
submitButtonText="Save Changes"
/>
</div>
</div>
</div>
);
}
export default AgentSettings;

View File

@@ -0,0 +1,181 @@
import { useState, useEffect } from 'react';
import { Link, useOutletContext } from 'react-router-dom';
import { agentApi } from '../utils/api';
function AgentsList() {
const [agents, setAgents] = useState([]);
const [statuses, setStatuses] = useState({});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const { showToast } = useOutletContext();
// Fetch agents data
const fetchAgents = async () => {
setLoading(true);
try {
const response = await fetch('/agents');
const html = await response.text();
// Create a temporary element to parse the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Extract agent names and statuses from the HTML
const agentElements = tempDiv.querySelectorAll('[data-agent]');
const agentList = [];
const statusMap = {};
agentElements.forEach(el => {
const name = el.getAttribute('data-agent');
const status = el.getAttribute('data-active') === 'true';
if (name) {
agentList.push(name);
statusMap[name] = status;
}
});
setAgents(agentList);
setStatuses(statusMap);
} catch (err) {
console.error('Error fetching agents:', err);
setError('Failed to load agents');
} finally {
setLoading(false);
}
};
// Toggle agent status (pause/start)
const toggleAgentStatus = async (name, isActive) => {
try {
const endpoint = isActive ? `/pause/${name}` : `/start/${name}`;
const response = await fetch(endpoint, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
});
if (response.ok) {
// Update local state
setStatuses(prev => ({
...prev,
[name]: !isActive
}));
// Show success toast
const action = isActive ? 'paused' : 'started';
showToast(`Agent "${name}" ${action} successfully`, 'success');
} else {
throw new Error(`Server responded with status: ${response.status}`);
}
} catch (err) {
console.error(`Error toggling agent status:`, err);
showToast(`Failed to update agent status: ${err.message}`, 'error');
}
};
// Delete an agent
const deleteAgent = async (name) => {
if (!confirm(`Are you sure you want to delete agent "${name}"? This action cannot be undone.`)) {
return;
}
try {
const response = await fetch(`/delete/${name}`, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
});
if (response.ok) {
// Remove from local state
setAgents(prev => prev.filter(agent => agent !== name));
// Show success toast
showToast(`Agent "${name}" deleted successfully`, 'success');
} else {
throw new Error(`Server responded with status: ${response.status}`);
}
} catch (err) {
console.error(`Error deleting agent:`, err);
showToast(`Failed to delete agent: ${err.message}`, 'error');
}
};
// Load agents on mount
useEffect(() => {
fetchAgents();
}, []);
if (loading) {
return <div className="loading">Loading agents...</div>;
}
if (error) {
return <div className="error">{error}</div>;
}
return (
<div className="agents-container">
<header className="page-header">
<h1>Manage Agents</h1>
<Link to="/create" className="create-btn">
<i className="fas fa-plus"></i> Create New Agent
</Link>
</header>
{agents.length > 0 ? (
<div className="agents-grid">
{agents.map(name => (
<div key={name} className="agent-card" data-agent={name} data-active={statuses[name]}>
<div className="agent-header">
<h2>{name}</h2>
<span className={`status-badge ${statuses[name] ? 'active' : 'inactive'}`}>
{statuses[name] ? 'Active' : 'Paused'}
</span>
</div>
<div className="agent-actions">
<Link to={`/talk/${name}`} className="action-btn chat-btn">
<i className="fas fa-comment"></i> Chat
</Link>
<Link to={`/settings/${name}`} className="action-btn settings-btn">
<i className="fas fa-cog"></i> Settings
</Link>
<Link to={`/status/${name}`} className="action-btn status-btn">
<i className="fas fa-chart-line"></i> Status
</Link>
<button
className="action-btn toggle-btn"
onClick={() => toggleAgentStatus(name, statuses[name])}
>
{statuses[name] ? (
<><i className="fas fa-pause"></i> Pause</>
) : (
<><i className="fas fa-play"></i> Start</>
)}
</button>
<button
className="action-btn delete-btn"
onClick={() => deleteAgent(name)}
>
<i className="fas fa-trash-alt"></i> Delete
</button>
</div>
</div>
))}
</div>
) : (
<div className="no-agents">
<h2>No Agents Found</h2>
<p>Get started by creating your first agent</p>
<Link to="/create" className="create-agent-btn">
Create Agent
</Link>
</div>
)}
</div>
);
}
export default AgentsList;

View File

@@ -0,0 +1,106 @@
import { useState, useRef, useEffect } from 'react';
import { useParams, useOutletContext } from 'react-router-dom';
import { useChat } from '../hooks/useChat';
function Chat() {
const { name } = useParams();
const { showToast } = useOutletContext();
const [message, setMessage] = useState('');
const messagesEndRef = useRef(null);
// Use our custom chat hook
const {
messages,
sending,
error,
isConnected,
sendMessage,
clearChat
} = useChat(name);
// Scroll to bottom when messages change
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// Show error toast if there's an error
useEffect(() => {
if (error) {
showToast(error, 'error');
}
}, [error, showToast]);
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
if (!message.trim()) return;
const success = await sendMessage(message.trim());
if (success) {
setMessage('');
}
};
return (
<div className="chat-container">
<header className="chat-header">
<h1>Chat with {name}</h1>
<div className="connection-status">
<span className={`status-indicator ${isConnected ? 'connected' : 'disconnected'}`}>
{isConnected ? 'Connected' : 'Disconnected'}
</span>
</div>
<button
className="clear-chat-btn"
onClick={clearChat}
disabled={messages.length === 0}
>
Clear Chat
</button>
</header>
<div className="messages-container">
{messages.length === 0 ? (
<div className="empty-chat">
<p>No messages yet. Start a conversation with {name}!</p>
</div>
) : (
messages.map((msg) => (
<div
key={msg.id}
className={`message ${msg.sender === 'user' ? 'user-message' : 'agent-message'}`}
>
<div className="message-content">
{msg.content}
</div>
<div className="message-timestamp">
{new Date(msg.timestamp).toLocaleTimeString()}
</div>
</div>
))
)}
<div ref={messagesEndRef} />
</div>
<form className="message-form" onSubmit={handleSubmit}>
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Type your message..."
disabled={sending || !isConnected}
className="message-input"
/>
<button
type="submit"
disabled={sending || !message.trim() || !isConnected}
className="send-button"
>
{sending ? 'Sending...' : 'Send'}
</button>
</form>
</div>
);
}
export default Chat;

View File

@@ -0,0 +1,90 @@
import { useState } from 'react';
import { useNavigate, useOutletContext } from 'react-router-dom';
import { agentApi } from '../utils/api';
import AgentForm from '../components/AgentForm';
function CreateAgent() {
const navigate = useNavigate();
const { showToast } = useOutletContext();
const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({
name: '',
description: '',
identity_guidance: '',
random_identity: false,
hud: false,
model: '',
multimodal_model: '',
api_url: '',
api_key: '',
local_rag_url: '',
local_rag_api_key: '',
enable_reasoning: false,
enable_kb: false,
kb_results: 3,
long_term_memory: false,
summary_long_term_memory: false,
connectors: [],
actions: [],
mcp_servers: [],
system_prompt: '',
user_prompt: '',
goals: '',
standalone_job: false,
standalone_job_interval: 60,
avatar: '',
avatar_seed: '',
avatar_style: 'default',
});
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.name.trim()) {
showToast('Agent name is required', 'error');
return;
}
setLoading(true);
try {
const response = await agentApi.createAgent(formData);
showToast(`Agent "${formData.name}" created successfully`, 'success');
navigate(`/settings/${formData.name}`);
} catch (err) {
showToast(`Error creating agent: ${err.message}`, 'error');
} finally {
setLoading(false);
}
};
return (
<div className="create-agent-container">
<header className="page-header">
<h1>
<i className="fas fa-plus-circle"></i> Create New Agent
</h1>
</header>
<div className="create-agent-content">
<div className="section-box">
<h2>
<i className="fas fa-robot"></i> Agent Configuration
</h2>
<AgentForm
formData={formData}
setFormData={setFormData}
onSubmit={handleSubmit}
loading={loading}
submitButtonText="Create Agent"
isEdit={false}
/>
</div>
</div>
</div>
);
}
export default CreateAgent;

View File

@@ -0,0 +1,490 @@
import { useState } from 'react';
import { useNavigate, useOutletContext } from 'react-router-dom';
import { agentApi } from '../utils/api';
import AgentForm from '../components/AgentForm';
function GroupCreate() {
const navigate = useNavigate();
const { showToast } = useOutletContext();
const [loading, setLoading] = useState(false);
const [generatingProfiles, setGeneratingProfiles] = useState(false);
const [activeStep, setActiveStep] = useState(1);
const [selectedProfiles, setSelectedProfiles] = useState([]);
const [formData, setFormData] = useState({
description: '',
model: '',
api_url: '',
api_key: '',
connectors: [],
actions: [],
profiles: []
});
// Handle form field changes
const handleInputChange = (e) => {
const { name, value, type } = e.target;
setFormData({
...formData,
[name]: type === 'number' ? parseInt(value, 10) : value
});
};
// Handle profile selection
const handleProfileSelection = (index) => {
const newSelectedProfiles = [...selectedProfiles];
if (newSelectedProfiles.includes(index)) {
// Remove from selection
const profileIndex = newSelectedProfiles.indexOf(index);
newSelectedProfiles.splice(profileIndex, 1);
} else {
// Add to selection
newSelectedProfiles.push(index);
}
setSelectedProfiles(newSelectedProfiles);
};
// Handle select all profiles
const handleSelectAll = (e) => {
if (e.target.checked) {
// Select all profiles
setSelectedProfiles(formData.profiles.map((_, index) => index));
} else {
// Deselect all profiles
setSelectedProfiles([]);
}
};
// Navigate to next step
const nextStep = () => {
setActiveStep(activeStep + 1);
};
// Navigate to previous step
const prevStep = () => {
setActiveStep(activeStep - 1);
};
// Generate agent profiles
const handleGenerateProfiles = async () => {
if (!formData.description.trim()) {
showToast('Please enter a description', 'warning');
return;
}
setGeneratingProfiles(true);
try {
const response = await agentApi.generateGroupProfiles({
description: formData.description
});
// The API returns an array of agent profiles directly
const profiles = Array.isArray(response) ? response : [];
setFormData({
...formData,
profiles: profiles
});
// Auto-select all profiles
setSelectedProfiles(profiles.map((_, index) => index));
// Move to next step
nextStep();
showToast('Agent profiles generated successfully', 'success');
} catch (err) {
console.error('Error generating profiles:', err);
showToast(`Failed to generate profiles: ${err.message}`, 'error');
} finally {
setGeneratingProfiles(false);
}
};
// Create agent group
const handleCreateGroup = async (e) => {
e.preventDefault();
if (selectedProfiles.length === 0) {
showToast('Please select at least one agent profile', 'warning');
return;
}
// Filter profiles to only include selected ones
const selectedProfilesData = selectedProfiles.map(index => formData.profiles[index]);
setLoading(true);
try {
// Structure the data according to what the server expects
const groupData = {
agents: selectedProfilesData,
agent_config: {
// Don't set name/description as they'll be overridden by each agent's values
model: formData.model,
api_url: formData.api_url,
api_key: formData.api_key,
connectors: formData.connectors,
actions: formData.actions
}
};
const response = await agentApi.createGroup(groupData);
showToast(`Agent group "${formData.group_name}" created successfully`, 'success');
navigate('/agents');
} catch (err) {
console.error('Error creating group:', err);
showToast(`Failed to create group: ${err.message}`, 'error');
} finally {
setLoading(false);
}
};
return (
<div className="group-create-container">
<div className="section-box">
<h1>Create Agent Group</h1>
{/* Progress Bar */}
<div className="progress-container">
<div className={`progress-step ${activeStep === 1 ? 'step-active' : ''}`}>
<div className="step-circle">1</div>
<div className="step-label">Generate Profiles</div>
</div>
<div className={`progress-step ${activeStep === 2 ? 'step-active' : ''}`}>
<div className="step-circle">2</div>
<div className="step-label">Review & Select</div>
</div>
<div className={`progress-step ${activeStep === 3 ? 'step-active' : ''}`}>
<div className="step-circle">3</div>
<div className="step-label">Configure Settings</div>
</div>
</div>
{/* Step 1: Generate Profiles */}
<div className={`page-section ${activeStep === 1 ? 'section-active' : ''}`}>
<h2>Generate Agent Profiles</h2>
<p>Describe the group of agents you want to create. Be specific about their roles, relationships, and purpose.</p>
<div className="prompt-container">
<textarea
id="description"
name="description"
value={formData.description}
onChange={handleInputChange}
placeholder="Example: Create a team of agents for a software development project including a project manager, developer, tester, and designer. They should collaborate to build web applications."
rows="5"
/>
</div>
<div className="action-buttons">
<button
type="button"
className="action-btn"
onClick={handleGenerateProfiles}
disabled={generatingProfiles || !formData.description}
>
{generatingProfiles ? (
<><i className="fas fa-spinner fa-spin"></i> Generating Profiles...</>
) : (
<><i className="fas fa-magic"></i> Generate Profiles</>
)}
</button>
</div>
</div>
{/* Loader */}
{generatingProfiles && (
<div className="loader" style={{ display: 'block' }}>
<i className="fas fa-spinner fa-spin"></i>
<p>Generating agent profiles...</p>
</div>
)}
{/* Step 2: Review & Select Profiles */}
<div className={`page-section ${activeStep === 2 ? 'section-active' : ''}`}>
<h2>Review & Select Agent Profiles</h2>
<p>Select the agents you want to create. You can customize their details before creation.</p>
<div className="select-all-container">
<label htmlFor="select-all" className="checkbox-label">
<input
type="checkbox"
id="select-all"
checked={selectedProfiles.length === formData.profiles.length}
onChange={handleSelectAll}
/>
<span>Select All</span>
</label>
</div>
<div className="agent-profiles-container">
{formData.profiles.map((profile, index) => (
<div
key={index}
className={`agent-profile ${selectedProfiles.includes(index) ? 'selected' : ''}`}
onClick={() => handleProfileSelection(index)}
>
<div className="select-checkbox">
<input
type="checkbox"
checked={selectedProfiles.includes(index)}
onChange={() => handleProfileSelection(index)}
/>
</div>
<h3>{profile.name || `Agent ${index + 1}`}</h3>
<div className="description">{profile.description || 'No description available.'}</div>
<div className="system-prompt">{profile.system_prompt || 'No system prompt defined.'}</div>
</div>
))}
</div>
<div className="action-buttons">
<button type="button" className="nav-btn" onClick={prevStep}>
<i className="fas fa-arrow-left"></i> Back
</button>
<button
type="button"
className="action-btn"
onClick={nextStep}
disabled={selectedProfiles.length === 0}
>
Continue <i className="fas fa-arrow-right"></i>
</button>
</div>
</div>
{/* Step 3: Common Settings */}
<div className={`page-section ${activeStep === 3 ? 'section-active' : ''}`}>
<h2>Configure Common Settings</h2>
<p>Configure common settings for all selected agents. These settings will be applied to each agent.</p>
<form id="group-settings-form" onSubmit={handleCreateGroup}>
{/* Informative message about profile data */}
<div className="info-message">
<i className="fas fa-info-circle"></i>
<span>
Each agent will be created with its own name, description, and system prompt from the selected profiles.
The settings below will be applied to all agents.
</span>
</div>
{/* Use AgentForm for common settings */}
<div className="agent-form-wrapper">
<AgentForm
formData={formData}
setFormData={setFormData}
onSubmit={handleCreateGroup}
loading={loading}
submitButtonText="Create Group"
isGroupForm={true}
noFormWrapper={true}
/>
</div>
<div className="action-buttons">
<button type="button" className="nav-btn" onClick={prevStep}>
<i className="fas fa-arrow-left"></i> Back
</button>
<button
type="submit"
className="action-btn"
disabled={loading}
>
{loading ? (
<><i className="fas fa-spinner fa-spin"></i> Creating Group...</>
) : (
<><i className="fas fa-users"></i> Create Group</>
)}
</button>
</div>
</form>
</div>
</div>
<style>{`
.progress-container {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
.progress-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding: 0 20px;
}
.progress-step:not(:last-child)::after {
content: '';
position: absolute;
top: 12px;
right: -30px;
width: 60px;
height: 3px;
background-color: var(--medium-bg);
}
.progress-step.step-active:not(:last-child)::after {
background-color: var(--primary);
}
.step-circle {
width: 28px;
height: 28px;
border-radius: 50%;
background-color: var(--medium-bg);
display: flex;
justify-content: center;
align-items: center;
color: var(--text);
margin-bottom: 8px;
transition: all 0.3s ease;
}
.progress-step.step-active .step-circle {
background-color: var(--primary);
box-shadow: 0 0 10px var(--primary);
}
.step-label {
font-size: 0.9rem;
color: var(--muted-text);
transition: all 0.3s ease;
}
.progress-step.step-active .step-label {
color: var(--primary);
font-weight: bold;
}
.page-section {
display: none;
animation: fadeIn 0.5s;
}
.page-section.section-active {
display: block;
}
.prompt-container {
margin-bottom: 30px;
}
.prompt-container textarea {
width: 100%;
min-height: 120px;
padding: 15px;
border-radius: 6px;
background-color: var(--lighter-bg);
border: 1px solid var(--medium-bg);
color: var(--text);
font-size: 1rem;
resize: vertical;
}
.action-buttons {
display: flex;
justify-content: space-between;
margin-top: 30px;
}
.select-all-container {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.loader {
text-align: center;
margin: 40px 0;
}
.loader i {
color: var(--primary);
font-size: 2rem;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.agent-profile {
border: 1px solid var(--medium-bg);
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
background-color: var(--lighter-bg);
position: relative;
transition: all 0.3s ease;
cursor: pointer;
}
.agent-profile:hover {
transform: translateY(-3px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
.agent-profile h3 {
color: var(--primary);
text-shadow: var(--neon-glow);
margin-top: 0;
margin-bottom: 15px;
border-bottom: 1px solid var(--medium-bg);
padding-bottom: 10px;
}
.agent-profile .description {
color: var(--text);
font-size: 0.9rem;
margin-bottom: 15px;
}
.agent-profile .system-prompt {
background-color: var(--darker-bg);
border-radius: 6px;
padding: 10px;
font-size: 0.85rem;
max-height: 150px;
overflow-y: auto;
margin-bottom: 10px;
white-space: pre-wrap;
}
.agent-profile.selected {
border: 2px solid var(--primary);
background-color: rgba(94, 0, 255, 0.1);
}
.agent-profile .select-checkbox {
position: absolute;
top: 10px;
right: 10px;
}
.info-message {
background-color: rgba(94, 0, 255, 0.1);
border-left: 4px solid var(--primary);
padding: 15px;
margin: 20px 0;
border-radius: 0 8px 8px 0;
display: flex;
align-items: center;
}
.info-message i {
font-size: 1.5rem;
color: var(--primary);
margin-right: 15px;
}
.info-message-content {
flex: 1;
}
.info-message-content h4 {
margin-top: 0;
margin-bottom: 5px;
color: var(--primary);
}
.info-message-content p {
margin-bottom: 0;
}
.nav-btn {
background-color: var(--medium-bg);
color: var(--text);
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.nav-btn:hover {
background-color: var(--lighter-bg);
}
`}</style>
</div>
);
}
export default GroupCreate;

View File

@@ -0,0 +1,137 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { agentApi } from '../utils/api';
function Home() {
const [stats, setStats] = useState({
agents: [],
agentCount: 0,
actions: 0,
connectors: 0,
status: {},
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Fetch dashboard data
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const agents = await agentApi.getAgents();
setStats({
agents: agents.Agents || [],
agentCount: agents.AgentCount || 0,
actions: agents.Actions || 0,
connectors: agents.Connectors || 0,
status: agents.Status || {},
});
} catch (err) {
console.error('Error fetching dashboard data:', err);
setError('Failed to load dashboard data');
} finally {
setLoading(false);
}
};
fetchData();
}, []);
if (loading) {
return <div className="loading">Loading dashboard data...</div>;
}
if (error) {
return <div className="error">{error}</div>;
}
return (
<div>
<div className="image-container">
<img src="/app/logo_1.png" width="250" alt="LocalAgent Logo" />
</div>
<h1 className="dashboard-title">LocalAgent</h1>
{/* Dashboard Stats */}
<div className="dashboard-stats">
<div className="stat-item">
<div className="stat-count">{stats.actions}</div>
<div className="stat-label">Available Actions</div>
</div>
<div className="stat-item">
<div className="stat-count">{stats.connectors}</div>
<div className="stat-label">Available Connectors</div>
</div>
<div className="stat-item">
<div className="stat-count">{stats.agentCount}</div>
<div className="stat-label">Agents</div>
</div>
</div>
{/* Cards Container */}
<div className="cards-container">
{/* Card for Agent List Page */}
<Link to="/agents" className="card-link">
<div className="card">
<h2><i className="fas fa-robot"></i> Agent List</h2>
<p>View and manage your list of agents, including detailed profiles and statistics.</p>
</div>
</Link>
{/* Card for Create Agent */}
<Link to="/create" className="card-link">
<div className="card">
<h2><i className="fas fa-plus-circle"></i> Create Agent</h2>
<p>Create a new intelligent agent with custom behaviors, connectors, and actions.</p>
</div>
</Link>
{/* Card for Actions Playground */}
<Link to="/actions-playground" className="card-link">
<div className="card">
<h2><i className="fas fa-code"></i> Actions Playground</h2>
<p>Explore and test available actions for your agents.</p>
</div>
</Link>
{/* Card for Group Create */}
<Link to="/group-create" className="card-link">
<div className="card">
<h2><i className="fas fa-users"></i> Create Group</h2>
<p>Create agent groups for collaborative intelligence.</p>
</div>
</Link>
</div>
{stats.agents.length > 0 && (
<div className="recent-agents">
<h2>Your Agents</h2>
<div className="cards-container">
{stats.agents.map((agent) => (
<div key={agent} className="card">
<div className={`status-badge ${stats.status[agent] ? 'status-active' : 'status-paused'}`}>
{stats.status[agent] ? 'Active' : 'Paused'}
</div>
<h2><i className="fas fa-robot"></i> {agent}</h2>
<div className="agent-actions">
<Link to={`/talk/${agent}`} className="agent-action">
Chat
</Link>
<Link to={`/settings/${agent}`} className="agent-action">
Settings
</Link>
<Link to={`/status/${agent}`} className="agent-action">
Status
</Link>
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
export default Home;

View File

@@ -0,0 +1,52 @@
import { createBrowserRouter } from 'react-router-dom';
import App from './App';
import Home from './pages/Home';
import AgentSettings from './pages/AgentSettings';
import AgentsList from './pages/AgentsList';
import CreateAgent from './pages/CreateAgent';
import Chat from './pages/Chat';
import ActionsPlayground from './pages/ActionsPlayground';
import GroupCreate from './pages/GroupCreate';
// Get the base URL from Vite's environment variables or default to '/app/'
const BASE_URL = import.meta.env.BASE_URL || '/app';
// Create a router with the base URL
export const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{
index: true,
element: <Home />
},
{
path: 'agents',
element: <AgentsList />
},
{
path: 'create',
element: <CreateAgent />
},
{
path: 'settings/:name',
element: <AgentSettings />
},
{
path: 'talk/:name',
element: <Chat />
},
{
path: 'actions-playground',
element: <ActionsPlayground />
},
{
path: 'group-create',
element: <GroupCreate />
}
]
}
], {
basename: BASE_URL // Set the base URL for all routes
});

198
webui/react-ui/src/utils/api.js vendored Normal file
View File

@@ -0,0 +1,198 @@
/**
* API utility for communicating with the Go backend
*/
import { API_CONFIG } from './config';
// Helper function for handling API responses
const handleResponse = async (response) => {
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `API error: ${response.status}`);
}
// Check if response is JSON
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return response.json();
}
return response.text();
};
// Helper function to build a full URL
const buildUrl = (endpoint) => {
return `${API_CONFIG.baseUrl}${endpoint.startsWith('/') ? endpoint.substring(1) : endpoint}`;
};
// Agent-related API calls
export const agentApi = {
// Get list of all agents
getAgents: async () => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.agents), {
headers: API_CONFIG.headers
});
return handleResponse(response);
},
// Get a specific agent's configuration
getAgentConfig: async (name) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.agentConfig(name)), {
headers: API_CONFIG.headers
});
return handleResponse(response);
},
// Create a new agent
createAgent: async (config) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.createAgent), {
method: 'POST',
headers: API_CONFIG.headers,
body: JSON.stringify(config),
});
return handleResponse(response);
},
// Update an existing agent's configuration
updateAgentConfig: async (name, config) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.agentConfig(name)), {
method: 'PUT',
headers: API_CONFIG.headers,
body: JSON.stringify(config),
});
return handleResponse(response);
},
// Delete an agent
deleteAgent: async (name) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.deleteAgent(name)), {
method: 'DELETE',
headers: API_CONFIG.headers,
});
return handleResponse(response);
},
// Pause an agent
pauseAgent: async (name) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.pauseAgent(name)), {
method: 'PUT',
headers: API_CONFIG.headers,
body: JSON.stringify({}),
});
return handleResponse(response);
},
// Start an agent
startAgent: async (name) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.startAgent(name)), {
method: 'PUT',
headers: API_CONFIG.headers,
body: JSON.stringify({}),
});
return handleResponse(response);
},
// Export agent configuration
exportAgentConfig: async (name) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.exportAgent(name)), {
headers: API_CONFIG.headers
});
return handleResponse(response);
},
// Import agent configuration
importAgentConfig: async (configData) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.importAgent), {
method: 'POST',
headers: API_CONFIG.headers,
body: JSON.stringify(configData),
});
return handleResponse(response);
},
// Generate group profiles
generateGroupProfiles: async (data) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.generateGroupProfiles), {
method: 'POST',
headers: API_CONFIG.headers,
body: JSON.stringify(data),
});
return handleResponse(response);
},
// Create a group of agents
createGroup: async (data) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.createGroup), {
method: 'POST',
headers: API_CONFIG.headers,
body: JSON.stringify(data),
});
return handleResponse(response);
},
};
// Chat-related API calls
export const chatApi = {
// Send a chat message to an agent
sendMessage: async (name, message) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.chat(name)), {
method: 'POST',
headers: API_CONFIG.headers,
body: JSON.stringify({ message }),
});
return handleResponse(response);
},
// Send a notification to an agent
sendNotification: async (name, message) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.notify(name)), {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({ message }),
});
return handleResponse(response);
},
// Get responses from an agent
getResponses: async (data) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.responses), {
method: 'POST',
headers: API_CONFIG.headers,
body: JSON.stringify(data),
});
return handleResponse(response);
},
};
// Action-related API calls
export const actionApi = {
// List available actions
listActions: async () => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.listActions), {
headers: API_CONFIG.headers
});
return handleResponse(response);
},
// Execute an action for an agent
executeAction: async (name, actionData) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.executeAction(name)), {
method: 'POST',
headers: API_CONFIG.headers,
body: JSON.stringify(actionData),
});
return handleResponse(response);
},
};
// Status-related API calls
export const statusApi = {
// Get agent status history
getStatusHistory: async (name) => {
const response = await fetch(buildUrl(API_CONFIG.endpoints.status(name)), {
headers: API_CONFIG.headers
});
return handleResponse(response);
},
};

49
webui/react-ui/src/utils/config.js vendored Normal file
View File

@@ -0,0 +1,49 @@
/**
* Application configuration
*/
// Get the base URL from Vite's environment variables or default to '/app/'
export const BASE_URL = import.meta.env.BASE_URL || '/app/';
// API endpoints configuration
export const API_CONFIG = {
// Base URL for API requests
baseUrl: '/', // API endpoints are at the root, not under /app/
// Default headers for API requests
headers: {
'Content-Type': 'application/json',
},
// Endpoints
endpoints: {
// Agent endpoints
agents: '/api/agents',
agentConfig: (name) => `/api/agent/${name}/config`,
createAgent: '/create',
deleteAgent: (name) => `/delete/${name}`,
pauseAgent: (name) => `/pause/${name}`,
startAgent: (name) => `/start/${name}`,
exportAgent: (name) => `/settings/export/${name}`,
importAgent: '/settings/import',
// Group endpoints
generateGroupProfiles: '/api/agent/group/generateProfiles',
createGroup: '/api/agent/group/create',
// Chat endpoints
chat: (name) => `/chat/${name}`,
notify: (name) => `/notify/${name}`,
responses: '/v1/responses',
// SSE endpoint
sse: (name) => `/sse/${name}`,
// Action endpoints
listActions: '/actions',
executeAction: (name) => `/action/${name}/run`,
// Status endpoint
status: (name) => `/status/${name}`,
}
};

View File

@@ -0,0 +1,32 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// Define backend URL with port from environment variable or default to 8080
const backendUrl = `http://${process.env.BACKEND_HOST || 'localhost'}:${process.env.BACKEND_PORT || '3000'}`
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
base: '/app', // Set the base path for production builds
server: {
proxy: {
// Proxy API requests to your Go backend
'/api': backendUrl,
// Proxy SSE endpoints
'/sse': backendUrl,
// Add other endpoints as needed
'/settings': backendUrl,
'/agents': backendUrl,
'/create': backendUrl,
'/delete': backendUrl,
'/pause': backendUrl,
'/start': backendUrl,
'/talk': backendUrl,
'/notify': backendUrl,
'/chat': backendUrl,
'/status': backendUrl,
'/action': backendUrl,
'/actions': backendUrl,
}
}
});

View File

@@ -25,6 +25,9 @@ var viewsfs embed.FS
//go:embed public/*
var embeddedFiles embed.FS
//go:embed react-ui/dist/*
var reactUI embed.FS
func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
// Static avatars in a.pooldir/avatars
@@ -57,6 +60,21 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
})
})
webapp.Use("/app", filesystem.New(filesystem.Config{
Root: http.FS(reactUI),
PathPrefix: "react-ui/dist",
}))
// Fallback route for SPA
webapp.Get("/app/*", func(c *fiber.Ctx) error {
indexHTML, err := reactUI.ReadFile("react-ui/dist/index.html")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Error reading index.html")
}
c.Set("Content-Type", "text/html")
return c.Send(indexHTML)
})
webapp.Get("/agents", func(c *fiber.Ctx) error {
statuses := map[string]bool{}
for _, a := range pool.List() {
@@ -160,6 +178,28 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
webapp.Post("/api/agent/group/generateProfiles", app.GenerateGroupProfiles(pool))
webapp.Post("/api/agent/group/create", app.CreateGroup(pool))
// Dashboard API endpoint for React UI
webapp.Get("/api/agents", func(c *fiber.Ctx) error {
statuses := map[string]bool{}
agents := pool.List()
for _, a := range agents {
agent := pool.GetAgent(a)
if agent == nil {
xlog.Error("Agent not found", "name", a)
continue
}
statuses[a] = !agent.Paused()
}
return c.JSON(fiber.Map{
"Agents": agents,
"AgentCount": len(agents),
"Actions": len(services.AvailableActions),
"Connectors": len(services.AvailableConnectors),
"Status": statuses,
})
})
webapp.Post("/settings/import", app.ImportAgent(pool))
webapp.Get("/settings/export/:name", app.ExportAgent(pool))