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:
24
webui/react-ui/.gitignore
vendored
Normal file
24
webui/react-ui/.gitignore
vendored
Normal 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
12
webui/react-ui/README.md
Normal 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
BIN
webui/react-ui/bun.lockb
Executable file
Binary file not shown.
33
webui/react-ui/eslint.config.js
Normal file
33
webui/react-ui/eslint.config.js
Normal 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
13
webui/react-ui/index.html
Normal 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>
|
||||
28
webui/react-ui/package.json
Normal file
28
webui/react-ui/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
BIN
webui/react-ui/public/logo_1.png
Normal file
BIN
webui/react-ui/public/logo_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 886 KiB |
1
webui/react-ui/public/vite.svg
Normal file
1
webui/react-ui/public/vite.svg
Normal 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
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
127
webui/react-ui/src/App.jsx
Normal 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>© {new Date().getFullYear()} LocalAgent - Cybernetic Intelligence</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
1
webui/react-ui/src/assets/react.svg
Normal file
1
webui/react-ui/src/assets/react.svg
Normal 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 |
208
webui/react-ui/src/components/ActionForm.jsx
Normal file
208
webui/react-ui/src/components/ActionForm.jsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import React from 'react';
|
||||
import FallbackAction from './actions/FallbackAction';
|
||||
import GithubIssueLabelerAction from './actions/GithubIssueLabelerAction';
|
||||
import GithubIssueOpenerAction from './actions/GithubIssueOpenerAction';
|
||||
import GithubIssueCloserAction from './actions/GithubIssueCloserAction';
|
||||
import GithubIssueCommenterAction from './actions/GithubIssueCommenterAction';
|
||||
import GithubRepositoryAction from './actions/GithubRepositoryAction';
|
||||
import TwitterPostAction from './actions/TwitterPostAction';
|
||||
import SendMailAction from './actions/SendMailAction';
|
||||
|
||||
/**
|
||||
* ActionForm component for configuring an action
|
||||
*/
|
||||
const ActionForm = ({ actions = [], onChange, onRemove, onAdd }) => {
|
||||
// Available action types
|
||||
const actionTypes = [
|
||||
{ value: '', label: 'Select an action type' },
|
||||
{ value: 'github-issue-labeler', label: 'GitHub Issue Labeler' },
|
||||
{ value: 'github-issue-opener', label: 'GitHub Issue Opener' },
|
||||
{ value: 'github-issue-closer', label: 'GitHub Issue Closer' },
|
||||
{ value: 'github-issue-commenter', label: 'GitHub Issue Commenter' },
|
||||
{ value: 'github-repository-get-content', label: 'GitHub Repository Get Content' },
|
||||
{ value: 'github-repository-create-or-update-content', label: 'GitHub Repository Create/Update Content' },
|
||||
{ value: 'github-readme', label: 'GitHub Readme' },
|
||||
{ value: 'twitter-post', label: 'Twitter Post' },
|
||||
{ value: 'send-mail', label: 'Send Email' },
|
||||
{ value: 'search', label: 'Search' },
|
||||
{ value: 'github-issue-searcher', label: 'GitHub Issue Searcher' },
|
||||
{ value: 'github-issue-reader', label: 'GitHub Issue Reader' },
|
||||
{ value: 'scraper', label: 'Web Scraper' },
|
||||
{ value: 'wikipedia', label: 'Wikipedia' },
|
||||
{ value: 'browse', label: 'Browse' },
|
||||
{ value: 'generate_image', label: 'Generate Image' },
|
||||
{ value: 'counter', label: 'Counter' },
|
||||
{ value: 'call_agents', label: 'Call Agents' },
|
||||
{ value: 'shell-command', label: 'Shell Command' },
|
||||
{ value: 'custom', label: 'Custom' }
|
||||
];
|
||||
|
||||
// Parse the config JSON string to an object
|
||||
const parseConfig = (action) => {
|
||||
if (!action || !action.config) return {};
|
||||
|
||||
try {
|
||||
return JSON.parse(action.config || '{}');
|
||||
} catch (error) {
|
||||
console.error('Error parsing action config:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
// Get a value from the config object
|
||||
const getConfigValue = (action, key, defaultValue = '') => {
|
||||
const config = parseConfig(action);
|
||||
return config[key] !== undefined ? config[key] : defaultValue;
|
||||
};
|
||||
|
||||
// Update a value in the config object
|
||||
const onActionConfigChange = (index, key, value) => {
|
||||
const action = actions[index];
|
||||
const config = parseConfig(action);
|
||||
config[key] = value;
|
||||
|
||||
onChange(index, {
|
||||
...action,
|
||||
config: JSON.stringify(config)
|
||||
});
|
||||
};
|
||||
|
||||
// Handle action type change
|
||||
const handleActionTypeChange = (index, value) => {
|
||||
const action = actions[index];
|
||||
onChange(index, {
|
||||
...action,
|
||||
name: value
|
||||
});
|
||||
};
|
||||
|
||||
// Render the appropriate action component based on the action type
|
||||
const renderActionComponent = (action, index) => {
|
||||
// Common props for all action components
|
||||
const actionProps = {
|
||||
index,
|
||||
onActionConfigChange: (key, value) => onActionConfigChange(index, key, value),
|
||||
getConfigValue: (key, defaultValue) => getConfigValue(action, key, defaultValue)
|
||||
};
|
||||
|
||||
switch (action.name) {
|
||||
case 'github-issue-labeler':
|
||||
return <GithubIssueLabelerAction {...actionProps} />;
|
||||
case 'github-issue-opener':
|
||||
return <GithubIssueOpenerAction {...actionProps} />;
|
||||
case 'github-issue-closer':
|
||||
return <GithubIssueCloserAction {...actionProps} />;
|
||||
case 'github-issue-commenter':
|
||||
return <GithubIssueCommenterAction {...actionProps} />;
|
||||
case 'github-repository-get-content':
|
||||
case 'github-repository-create-or-update-content':
|
||||
case 'github-readme':
|
||||
return <GithubRepositoryAction {...actionProps} />;
|
||||
case 'twitter-post':
|
||||
return <TwitterPostAction {...actionProps} />;
|
||||
case 'send-mail':
|
||||
return <SendMailAction {...actionProps} />;
|
||||
case 'generate_image':
|
||||
return (
|
||||
<div className="generate-image-action">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`apiKey${index}`}>OpenAI API Key</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`apiKey${index}`}
|
||||
value={getConfigValue(action, 'apiKey', '')}
|
||||
onChange={(e) => onActionConfigChange(index, 'apiKey', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="sk-..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`apiURL${index}`}>API URL (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`apiURL${index}`}
|
||||
value={getConfigValue(action, 'apiURL', 'https://api.openai.com/v1')}
|
||||
onChange={(e) => onActionConfigChange(index, 'apiURL', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="https://api.openai.com/v1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`model${index}`}>Model</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`model${index}`}
|
||||
value={getConfigValue(action, 'model', 'dall-e-3')}
|
||||
onChange={(e) => onActionConfigChange(index, 'model', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="dall-e-3"
|
||||
/>
|
||||
<small className="form-text text-muted">Image generation model (e.g., dall-e-3)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return <FallbackAction {...actionProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
// Render a specific action form
|
||||
const renderActionForm = (action, index) => {
|
||||
// Ensure action is an object with expected properties
|
||||
const safeAction = action || {};
|
||||
|
||||
return (
|
||||
<div key={index} className="connector-item mb-4">
|
||||
<div className="connector-header">
|
||||
<h4>Action #{index + 1}</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="remove-btn"
|
||||
onClick={() => onRemove(index)}
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="connector-type mb-3">
|
||||
<label htmlFor={`actionType${index}`}>Action Type</label>
|
||||
<select
|
||||
id={`actionType${index}`}
|
||||
value={safeAction.name || ''}
|
||||
onChange={(e) => handleActionTypeChange(index, e.target.value)}
|
||||
className="form-control"
|
||||
>
|
||||
{actionTypes.map((type) => (
|
||||
<option key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Render specific action template based on type */}
|
||||
{renderActionComponent(safeAction, index)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="connectors-container">
|
||||
{actions && actions.map((action, index) => (
|
||||
renderActionForm(action, index)
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="add-btn"
|
||||
onClick={onAdd}
|
||||
>
|
||||
<i className="fas fa-plus"></i> Add Action
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionForm;
|
||||
321
webui/react-ui/src/components/AgentForm.jsx
Normal file
321
webui/react-ui/src/components/AgentForm.jsx
Normal file
@@ -0,0 +1,321 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigate, useOutletContext } from 'react-router-dom';
|
||||
|
||||
// Import form sections
|
||||
import BasicInfoSection from './agent-form-sections/BasicInfoSection';
|
||||
import ConnectorsSection from './agent-form-sections/ConnectorsSection';
|
||||
import ActionsSection from './agent-form-sections/ActionsSection';
|
||||
import MCPServersSection from './agent-form-sections/MCPServersSection';
|
||||
import MemorySettingsSection from './agent-form-sections/MemorySettingsSection';
|
||||
import ModelSettingsSection from './agent-form-sections/ModelSettingsSection';
|
||||
import PromptsGoalsSection from './agent-form-sections/PromptsGoalsSection';
|
||||
import AdvancedSettingsSection from './agent-form-sections/AdvancedSettingsSection';
|
||||
|
||||
const AgentForm = ({
|
||||
isEdit = false,
|
||||
formData,
|
||||
setFormData,
|
||||
onSubmit,
|
||||
loading = false,
|
||||
submitButtonText,
|
||||
isGroupForm = false,
|
||||
noFormWrapper = false
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const { showToast } = useOutletContext();
|
||||
const [activeSection, setActiveSection] = useState(isGroupForm ? 'model-section' : 'basic-section');
|
||||
|
||||
// Handle input changes
|
||||
const handleInputChange = (e) => {
|
||||
const { name, value, type, checked } = e.target;
|
||||
|
||||
if (name.includes('.')) {
|
||||
const [parent, child] = name.split('.');
|
||||
setFormData({
|
||||
...formData,
|
||||
[parent]: {
|
||||
...formData[parent],
|
||||
[child]: type === 'checkbox' ? checked : value
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setFormData({
|
||||
...formData,
|
||||
[name]: type === 'checkbox' ? checked : value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Handle form submission
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (onSubmit) {
|
||||
onSubmit(e);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle navigation between sections
|
||||
const handleSectionChange = (section) => {
|
||||
setActiveSection(section);
|
||||
};
|
||||
|
||||
// Handle adding a connector
|
||||
const handleAddConnector = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
connectors: [
|
||||
...(formData.connectors || []),
|
||||
{ name: '', config: '{}' }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
// Handle removing a connector
|
||||
const handleRemoveConnector = (index) => {
|
||||
const updatedConnectors = [...formData.connectors];
|
||||
updatedConnectors.splice(index, 1);
|
||||
setFormData({
|
||||
...formData,
|
||||
connectors: updatedConnectors
|
||||
});
|
||||
};
|
||||
|
||||
// Handle connector name change
|
||||
const handleConnectorNameChange = (index, value) => {
|
||||
const updatedConnectors = [...formData.connectors];
|
||||
updatedConnectors[index] = {
|
||||
...updatedConnectors[index],
|
||||
type: value
|
||||
};
|
||||
setFormData({
|
||||
...formData,
|
||||
connectors: updatedConnectors
|
||||
});
|
||||
};
|
||||
|
||||
// Handle connector config change
|
||||
const handleConnectorConfigChange = (index, key, value) => {
|
||||
const updatedConnectors = [...formData.connectors];
|
||||
const currentConnector = updatedConnectors[index];
|
||||
|
||||
// Parse the current config if it's a string
|
||||
let currentConfig = {};
|
||||
if (typeof currentConnector.config === 'string') {
|
||||
try {
|
||||
currentConfig = JSON.parse(currentConnector.config);
|
||||
} catch (err) {
|
||||
console.error('Error parsing config:', err);
|
||||
currentConfig = {};
|
||||
}
|
||||
} else if (currentConnector.config) {
|
||||
currentConfig = currentConnector.config;
|
||||
}
|
||||
|
||||
// Update the config with the new key-value pair
|
||||
currentConfig = {
|
||||
...currentConfig,
|
||||
[key]: value
|
||||
};
|
||||
|
||||
// Update the connector with the stringified config
|
||||
updatedConnectors[index] = {
|
||||
...currentConnector,
|
||||
config: JSON.stringify(currentConfig)
|
||||
};
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
connectors: updatedConnectors
|
||||
});
|
||||
};
|
||||
|
||||
// Handle adding an MCP server
|
||||
const handleAddMCPServer = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
mcp_servers: [
|
||||
...(formData.mcp_servers || []),
|
||||
{ url: '' }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
// Handle removing an MCP server
|
||||
const handleRemoveMCPServer = (index) => {
|
||||
const updatedMCPServers = [...formData.mcp_servers];
|
||||
updatedMCPServers.splice(index, 1);
|
||||
setFormData({
|
||||
...formData,
|
||||
mcp_servers: updatedMCPServers
|
||||
});
|
||||
};
|
||||
|
||||
// Handle MCP server change
|
||||
const handleMCPServerChange = (index, value) => {
|
||||
const updatedMCPServers = [...formData.mcp_servers];
|
||||
updatedMCPServers[index] = { url: value };
|
||||
setFormData({
|
||||
...formData,
|
||||
mcp_servers: updatedMCPServers
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="agent-form-container">
|
||||
{/* Wizard Sidebar */}
|
||||
<div className="wizard-sidebar">
|
||||
<ul className="wizard-nav">
|
||||
{!isGroupForm && (
|
||||
<li
|
||||
className={`wizard-nav-item ${activeSection === 'basic-section' ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange('basic-section')}
|
||||
>
|
||||
<i className="fas fa-info-circle"></i>
|
||||
Basic Information
|
||||
</li>
|
||||
)}
|
||||
<li
|
||||
className={`wizard-nav-item ${activeSection === 'model-section' ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange('model-section')}
|
||||
>
|
||||
<i className="fas fa-brain"></i>
|
||||
Model Settings
|
||||
</li>
|
||||
<li
|
||||
className={`wizard-nav-item ${activeSection === 'connectors-section' ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange('connectors-section')}
|
||||
>
|
||||
<i className="fas fa-plug"></i>
|
||||
Connectors
|
||||
</li>
|
||||
<li
|
||||
className={`wizard-nav-item ${activeSection === 'actions-section' ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange('actions-section')}
|
||||
>
|
||||
<i className="fas fa-bolt"></i>
|
||||
Actions
|
||||
</li>
|
||||
<li
|
||||
className={`wizard-nav-item ${activeSection === 'mcp-section' ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange('mcp-section')}
|
||||
>
|
||||
<i className="fas fa-server"></i>
|
||||
MCP Servers
|
||||
</li>
|
||||
<li
|
||||
className={`wizard-nav-item ${activeSection === 'memory-section' ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange('memory-section')}
|
||||
>
|
||||
<i className="fas fa-memory"></i>
|
||||
Memory Settings
|
||||
</li>
|
||||
<li
|
||||
className={`wizard-nav-item ${activeSection === 'prompts-section' ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange('prompts-section')}
|
||||
>
|
||||
<i className="fas fa-comment-alt"></i>
|
||||
Prompts & Goals
|
||||
</li>
|
||||
<li
|
||||
className={`wizard-nav-item ${activeSection === 'advanced-section' ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange('advanced-section')}
|
||||
>
|
||||
<i className="fas fa-cogs"></i>
|
||||
Advanced Settings
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Form Content */}
|
||||
<div className="form-content-area">
|
||||
{noFormWrapper ? (
|
||||
<div className='agent-form'>
|
||||
{/* Form Sections */}
|
||||
<div style={{ display: activeSection === 'basic-section' ? 'block' : 'none' }}>
|
||||
<BasicInfoSection formData={formData} handleInputChange={handleInputChange} isEdit={isEdit} isGroupForm={isGroupForm} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'model-section' ? 'block' : 'none' }}>
|
||||
<ModelSettingsSection formData={formData} handleInputChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'connectors-section' ? 'block' : 'none' }}>
|
||||
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorNameChange={handleConnectorNameChange} handleConnectorConfigChange={handleConnectorConfigChange} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'actions-section' ? 'block' : 'none' }}>
|
||||
<ActionsSection formData={formData} setFormData={setFormData} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'mcp-section' ? 'block' : 'none' }}>
|
||||
<MCPServersSection formData={formData} handleAddMCPServer={handleAddMCPServer} handleRemoveMCPServer={handleRemoveMCPServer} handleMCPServerChange={handleMCPServerChange} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'memory-section' ? 'block' : 'none' }}>
|
||||
<MemorySettingsSection formData={formData} handleInputChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'prompts-section' ? 'block' : 'none' }}>
|
||||
<PromptsGoalsSection formData={formData} handleInputChange={handleInputChange} isGroupForm={isGroupForm} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'advanced-section' ? 'block' : 'none' }}>
|
||||
<AdvancedSettingsSection formData={formData} handleInputChange={handleInputChange} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form className="agent-form" onSubmit={handleSubmit}>
|
||||
{/* Form Sections */}
|
||||
<div style={{ display: activeSection === 'basic-section' ? 'block' : 'none' }}>
|
||||
<BasicInfoSection formData={formData} handleInputChange={handleInputChange} isEdit={isEdit} isGroupForm={isGroupForm} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'model-section' ? 'block' : 'none' }}>
|
||||
<ModelSettingsSection formData={formData} handleInputChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'connectors-section' ? 'block' : 'none' }}>
|
||||
<ConnectorsSection formData={formData} handleAddConnector={handleAddConnector} handleRemoveConnector={handleRemoveConnector} handleConnectorNameChange={handleConnectorNameChange} handleConnectorConfigChange={handleConnectorConfigChange} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'actions-section' ? 'block' : 'none' }}>
|
||||
<ActionsSection formData={formData} setFormData={setFormData} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'mcp-section' ? 'block' : 'none' }}>
|
||||
<MCPServersSection formData={formData} handleAddMCPServer={handleAddMCPServer} handleRemoveMCPServer={handleRemoveMCPServer} handleMCPServerChange={handleMCPServerChange} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'memory-section' ? 'block' : 'none' }}>
|
||||
<MemorySettingsSection formData={formData} handleInputChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'prompts-section' ? 'block' : 'none' }}>
|
||||
<PromptsGoalsSection formData={formData} handleInputChange={handleInputChange} isGroupForm={isGroupForm} />
|
||||
</div>
|
||||
|
||||
<div style={{ display: activeSection === 'advanced-section' ? 'block' : 'none' }}>
|
||||
<AdvancedSettingsSection formData={formData} handleInputChange={handleInputChange} />
|
||||
</div>
|
||||
|
||||
{/* Form Controls */}
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={() => navigate('/agents')}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
{submitButtonText || (isEdit ? 'Update Agent' : 'Create Agent')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentForm;
|
||||
138
webui/react-ui/src/components/ConnectorForm.jsx
Normal file
138
webui/react-ui/src/components/ConnectorForm.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
// Import connector components
|
||||
import TelegramConnector from './connectors/TelegramConnector';
|
||||
import SlackConnector from './connectors/SlackConnector';
|
||||
import DiscordConnector from './connectors/DiscordConnector';
|
||||
import GithubIssuesConnector from './connectors/GithubIssuesConnector';
|
||||
import GithubPRsConnector from './connectors/GithubPRsConnector';
|
||||
import IRCConnector from './connectors/IRCConnector';
|
||||
import TwitterConnector from './connectors/TwitterConnector';
|
||||
import FallbackConnector from './connectors/FallbackConnector';
|
||||
|
||||
/**
|
||||
* ConnectorForm component
|
||||
* Provides specific form templates for different connector types
|
||||
*/
|
||||
function ConnectorForm({
|
||||
connectors = [],
|
||||
onAddConnector,
|
||||
onRemoveConnector,
|
||||
onConnectorNameChange,
|
||||
onConnectorConfigChange
|
||||
}) {
|
||||
const [newConfigKey, setNewConfigKey] = useState('');
|
||||
|
||||
// Render a specific connector form based on its type
|
||||
const renderConnectorForm = (connector, index) => {
|
||||
// Ensure connector is an object with expected properties
|
||||
const safeConnector = connector || {};
|
||||
|
||||
return (
|
||||
<div key={index} className="connector-item mb-4">
|
||||
<div className="connector-header">
|
||||
<h4>Connector #{index + 1}</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="remove-btn"
|
||||
onClick={() => onRemoveConnector(index)}
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="connector-type mb-3">
|
||||
<label htmlFor={`connectorName${index}`}>Connector Type</label>
|
||||
<select
|
||||
id={`connectorName${index}`}
|
||||
value={safeConnector.type || ''}
|
||||
onChange={(e) => onConnectorNameChange(index, e.target.value)}
|
||||
className="form-control"
|
||||
>
|
||||
<option value="">Select a connector type</option>
|
||||
<option value="telegram">Telegram</option>
|
||||
<option value="slack">Slack</option>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="github-issues">GitHub Issues</option>
|
||||
<option value="github-prs">GitHub PRs</option>
|
||||
<option value="irc">IRC</option>
|
||||
<option value="twitter">Twitter</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Render specific connector template based on type */}
|
||||
{renderConnectorTemplate(safeConnector, index)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Get the appropriate form template based on connector type
|
||||
const renderConnectorTemplate = (connector, index) => {
|
||||
// Check if connector.type exists, if not use empty string to avoid errors
|
||||
const connectorType = (connector.type || '').toLowerCase();
|
||||
|
||||
// Common props for all connector components
|
||||
const connectorProps = {
|
||||
connector,
|
||||
index,
|
||||
onConnectorConfigChange,
|
||||
getConfigValue
|
||||
};
|
||||
|
||||
switch (connectorType) {
|
||||
case 'telegram':
|
||||
return <TelegramConnector {...connectorProps} />;
|
||||
case 'slack':
|
||||
return <SlackConnector {...connectorProps} />;
|
||||
case 'discord':
|
||||
return <DiscordConnector {...connectorProps} />;
|
||||
case 'github-issues':
|
||||
return <GithubIssuesConnector {...connectorProps} />;
|
||||
case 'github-prs':
|
||||
return <GithubPRsConnector {...connectorProps} />;
|
||||
case 'irc':
|
||||
return <IRCConnector {...connectorProps} />;
|
||||
case 'twitter':
|
||||
return <TwitterConnector {...connectorProps} />;
|
||||
default:
|
||||
return <FallbackConnector {...connectorProps} />;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to safely get config values
|
||||
const getConfigValue = (connector, key, defaultValue = '') => {
|
||||
if (!connector || !connector.config) return defaultValue;
|
||||
|
||||
// If config is a string (JSON), try to parse it
|
||||
let config = connector.config;
|
||||
if (typeof config === 'string') {
|
||||
try {
|
||||
config = JSON.parse(config);
|
||||
} catch (err) {
|
||||
console.error('Error parsing config:', err);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return config[key] !== undefined ? config[key] : defaultValue;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="connectors-container">
|
||||
{connectors && connectors.map((connector, index) => (
|
||||
renderConnectorForm(connector, index)
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="add-btn"
|
||||
onClick={onAddConnector}
|
||||
>
|
||||
<i className="fas fa-plus"></i> Add Connector
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConnectorForm;
|
||||
16
webui/react-ui/src/components/actions/FallbackAction.jsx
Normal file
16
webui/react-ui/src/components/actions/FallbackAction.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* FallbackAction component for actions without specific configuration
|
||||
*/
|
||||
const FallbackAction = ({ index, onActionConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="fallback-action">
|
||||
<p className="text-muted">
|
||||
This action doesn't require any additional configuration.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FallbackAction;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* GitHub Issue Closer action component
|
||||
*/
|
||||
const GithubIssueCloserAction = ({ index, onActionConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="github-issue-closer-action">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubToken${index}`}
|
||||
value={getConfigValue('token', '')}
|
||||
onChange={(e) => onActionConfigChange('token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="ghp_..."
|
||||
/>
|
||||
<small className="form-text text-muted">Personal access token with repo scope</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubOwner${index}`}
|
||||
value={getConfigValue('owner', '')}
|
||||
onChange={(e) => onActionConfigChange('owner', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="username or organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubRepo${index}`}
|
||||
value={getConfigValue('repository', '')}
|
||||
onChange={(e) => onActionConfigChange('repository', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="repository-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`customActionName${index}`}
|
||||
value={getConfigValue('customActionName', '')}
|
||||
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="close_github_issue"
|
||||
/>
|
||||
<small className="form-text text-muted">Custom name for this action (optional)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubIssueCloserAction;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* GitHub Issue Commenter action component
|
||||
*/
|
||||
const GithubIssueCommenterAction = ({ index, onActionConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="github-issue-commenter-action">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubToken${index}`}
|
||||
value={getConfigValue('token', '')}
|
||||
onChange={(e) => onActionConfigChange('token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="ghp_..."
|
||||
/>
|
||||
<small className="form-text text-muted">Personal access token with repo scope</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubOwner${index}`}
|
||||
value={getConfigValue('owner', '')}
|
||||
onChange={(e) => onActionConfigChange('owner', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="username or organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubRepo${index}`}
|
||||
value={getConfigValue('repository', '')}
|
||||
onChange={(e) => onActionConfigChange('repository', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="repository-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`customActionName${index}`}
|
||||
value={getConfigValue('customActionName', '')}
|
||||
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="comment_on_github_issue"
|
||||
/>
|
||||
<small className="form-text text-muted">Custom name for this action (optional)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubIssueCommenterAction;
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* GitHub Issue Labeler action component
|
||||
*/
|
||||
const GithubIssueLabelerAction = ({ index, onActionConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="github-issue-labeler-action">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubToken${index}`}
|
||||
value={getConfigValue('token', '')}
|
||||
onChange={(e) => onActionConfigChange('token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="ghp_..."
|
||||
/>
|
||||
<small className="form-text text-muted">Personal access token with repo scope</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubOwner${index}`}
|
||||
value={getConfigValue('owner', '')}
|
||||
onChange={(e) => onActionConfigChange('owner', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="username or organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubRepo${index}`}
|
||||
value={getConfigValue('repository', '')}
|
||||
onChange={(e) => onActionConfigChange('repository', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="repository-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`availableLabels${index}`}>Available Labels</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`availableLabels${index}`}
|
||||
value={getConfigValue('availableLabels', 'bug,enhancement')}
|
||||
onChange={(e) => onActionConfigChange('availableLabels', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="bug,enhancement,documentation"
|
||||
/>
|
||||
<small className="form-text text-muted">Comma-separated list of available labels</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`customActionName${index}`}
|
||||
value={getConfigValue('customActionName', '')}
|
||||
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="add_label_to_issue"
|
||||
/>
|
||||
<small className="form-text text-muted">Custom name for this action (optional)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubIssueLabelerAction;
|
||||
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* GitHub Issue Opener action component
|
||||
*/
|
||||
const GithubIssueOpenerAction = ({ index, onActionConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="github-issue-opener-action">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubToken${index}`}
|
||||
value={getConfigValue('token', '')}
|
||||
onChange={(e) => onActionConfigChange('token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="ghp_..."
|
||||
/>
|
||||
<small className="form-text text-muted">Personal access token with repo scope</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubOwner${index}`}
|
||||
value={getConfigValue('owner', '')}
|
||||
onChange={(e) => onActionConfigChange('owner', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="username or organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubRepo${index}`}
|
||||
value={getConfigValue('repository', '')}
|
||||
onChange={(e) => onActionConfigChange('repository', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="repository-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`customActionName${index}`}
|
||||
value={getConfigValue('customActionName', '')}
|
||||
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="open_github_issue"
|
||||
/>
|
||||
<small className="form-text text-muted">Custom name for this action (optional)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubIssueOpenerAction;
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* GitHub Repository action component for repository-related actions
|
||||
* Used for:
|
||||
* - github-repository-get-content
|
||||
* - github-repository-create-or-update-content
|
||||
* - github-readme
|
||||
*/
|
||||
const GithubRepositoryAction = ({ index, onActionConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="github-repository-action">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubToken${index}`}>GitHub Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubToken${index}`}
|
||||
value={getConfigValue('token', '')}
|
||||
onChange={(e) => onActionConfigChange('token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="ghp_..."
|
||||
/>
|
||||
<small className="form-text text-muted">Personal access token with repo scope</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubOwner${index}`}
|
||||
value={getConfigValue('owner', '')}
|
||||
onChange={(e) => onActionConfigChange('owner', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="username or organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubRepo${index}`}
|
||||
value={getConfigValue('repository', '')}
|
||||
onChange={(e) => onActionConfigChange('repository', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="repository-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`customActionName${index}`}>Custom Action Name (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`customActionName${index}`}
|
||||
value={getConfigValue('customActionName', '')}
|
||||
onChange={(e) => onActionConfigChange('customActionName', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="github_repo_action"
|
||||
/>
|
||||
<small className="form-text text-muted">Custom name for this action (optional)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubRepositoryAction;
|
||||
75
webui/react-ui/src/components/actions/SendMailAction.jsx
Normal file
75
webui/react-ui/src/components/actions/SendMailAction.jsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* SendMail action component
|
||||
*/
|
||||
const SendMailAction = ({ index, onActionConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="send-mail-action">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`email${index}`}>Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id={`email${index}`}
|
||||
value={getConfigValue('email', '')}
|
||||
onChange={(e) => onActionConfigChange('email', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="your-email@example.com"
|
||||
/>
|
||||
<small className="form-text text-muted">Email address to send from</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`username${index}`}>Username</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`username${index}`}
|
||||
value={getConfigValue('username', '')}
|
||||
onChange={(e) => onActionConfigChange('username', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="SMTP username (often same as email)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`password${index}`}>Password</label>
|
||||
<input
|
||||
type="password"
|
||||
id={`password${index}`}
|
||||
value={getConfigValue('password', '')}
|
||||
onChange={(e) => onActionConfigChange('password', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="SMTP password or app password"
|
||||
/>
|
||||
<small className="form-text text-muted">For Gmail, use an app password</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`smtpHost${index}`}>SMTP Host</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`smtpHost${index}`}
|
||||
value={getConfigValue('smtpHost', '')}
|
||||
onChange={(e) => onActionConfigChange('smtpHost', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="smtp.gmail.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`smtpPort${index}`}>SMTP Port</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`smtpPort${index}`}
|
||||
value={getConfigValue('smtpPort', '587')}
|
||||
onChange={(e) => onActionConfigChange('smtpPort', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="587"
|
||||
/>
|
||||
<small className="form-text text-muted">Common ports: 587 (TLS), 465 (SSL)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendMailAction;
|
||||
41
webui/react-ui/src/components/actions/TwitterPostAction.jsx
Normal file
41
webui/react-ui/src/components/actions/TwitterPostAction.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Twitter Post action component
|
||||
*/
|
||||
const TwitterPostAction = ({ index, onActionConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="twitter-post-action">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`twitterToken${index}`}>Twitter API Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`twitterToken${index}`}
|
||||
value={getConfigValue('token', '')}
|
||||
onChange={(e) => onActionConfigChange('token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="Twitter API token"
|
||||
/>
|
||||
<small className="form-text text-muted">Twitter API token with posting permissions</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`noCharacterLimits${index}`}
|
||||
checked={getConfigValue('noCharacterLimits', '') === 'true'}
|
||||
onChange={(e) => onActionConfigChange('noCharacterLimits', e.target.checked ? 'true' : 'false')}
|
||||
className="form-check-input"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={`noCharacterLimits${index}`}>
|
||||
Disable character limit (280 characters)
|
||||
</label>
|
||||
<small className="form-text text-muted d-block">Enable to bypass the 280 character limit check</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwitterPostAction;
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import ActionForm from '../ActionForm';
|
||||
|
||||
/**
|
||||
* ActionsSection component for the agent form
|
||||
*/
|
||||
const ActionsSection = ({ formData, setFormData }) => {
|
||||
// Handle action change
|
||||
const handleActionChange = (index, updatedAction) => {
|
||||
const updatedActions = [...(formData.actions || [])];
|
||||
updatedActions[index] = updatedAction;
|
||||
setFormData({
|
||||
...formData,
|
||||
actions: updatedActions
|
||||
});
|
||||
};
|
||||
|
||||
// Handle action removal
|
||||
const handleActionRemove = (index) => {
|
||||
const updatedActions = [...(formData.actions || [])].filter((_, i) => i !== index);
|
||||
setFormData({
|
||||
...formData,
|
||||
actions: updatedActions
|
||||
});
|
||||
};
|
||||
|
||||
// Handle adding an action
|
||||
const handleAddAction = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
actions: [
|
||||
...(formData.actions || []),
|
||||
{ name: '', config: '{}' }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="actions-section">
|
||||
<h3>Actions</h3>
|
||||
<p className="text-muted">
|
||||
Configure actions that the agent can perform.
|
||||
</p>
|
||||
|
||||
<ActionForm
|
||||
actions={formData.actions || []}
|
||||
onChange={handleActionChange}
|
||||
onRemove={handleActionRemove}
|
||||
onAdd={handleAddAction}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ActionsSection;
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Advanced Settings section of the agent form
|
||||
*/
|
||||
const AdvancedSettingsSection = ({ formData, handleInputChange }) => {
|
||||
return (
|
||||
<div id="advanced-section">
|
||||
<h3 className="section-title">Advanced Settings</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="max_steps">Max Steps</label>
|
||||
<input
|
||||
type="number"
|
||||
name="max_steps"
|
||||
id="max_steps"
|
||||
min="1"
|
||||
value={formData.max_steps || 10}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
/>
|
||||
<small className="form-text text-muted">Maximum number of steps the agent can take</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="max_iterations">Max Iterations</label>
|
||||
<input
|
||||
type="number"
|
||||
name="max_iterations"
|
||||
id="max_iterations"
|
||||
min="1"
|
||||
value={formData.max_iterations || 5}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
/>
|
||||
<small className="form-text text-muted">Maximum number of iterations for each step</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="autonomous" className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="autonomous"
|
||||
id="autonomous"
|
||||
checked={formData.autonomous || false}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
Autonomous Mode
|
||||
</label>
|
||||
<small className="form-text text-muted">Allow the agent to operate autonomously</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="verbose" className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="verbose"
|
||||
id="verbose"
|
||||
checked={formData.verbose || false}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
Verbose Mode
|
||||
</label>
|
||||
<small className="form-text text-muted">Enable detailed logging</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="allow_code_execution" className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="allow_code_execution"
|
||||
id="allow_code_execution"
|
||||
checked={formData.allow_code_execution || false}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
Allow Code Execution
|
||||
</label>
|
||||
<small className="form-text text-muted">Allow the agent to execute code (use with caution)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdvancedSettingsSection;
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Basic Information section of the agent form
|
||||
*/
|
||||
const BasicInfoSection = ({ formData, handleInputChange, isEdit, isGroupForm }) => {
|
||||
// In group form context, we hide the basic info section entirely
|
||||
if (isGroupForm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="basic-section">
|
||||
<h3 className="section-title">Basic Information</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
value={formData.name || ''}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
disabled={isEdit} // Disable name field in edit mode
|
||||
/>
|
||||
{isEdit && <small className="form-text text-muted">Agent name cannot be changed after creation</small>}
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="description">Description</label>
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
value={formData.description || ''}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="identity_guidance">Identity Guidance</label>
|
||||
<textarea
|
||||
name="identity_guidance"
|
||||
id="identity_guidance"
|
||||
value={formData.identity_guidance || ''}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="random_identity" className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="random_identity"
|
||||
id="random_identity"
|
||||
checked={formData.random_identity || false}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
Random Identity
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="hud" className="checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="hud"
|
||||
id="hud"
|
||||
checked={formData.hud || false}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
HUD
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicInfoSection;
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import ConnectorForm from '../ConnectorForm';
|
||||
|
||||
/**
|
||||
* Connectors section of the agent form
|
||||
*/
|
||||
const ConnectorsSection = ({
|
||||
formData,
|
||||
handleAddConnector,
|
||||
handleRemoveConnector,
|
||||
handleConnectorNameChange,
|
||||
handleConnectorConfigChange
|
||||
}) => {
|
||||
return (
|
||||
<div id="connectors-section">
|
||||
<h3 className="section-title">Connectors</h3>
|
||||
<p className="section-description">
|
||||
Configure the connectors that this agent will use to communicate with external services.
|
||||
</p>
|
||||
|
||||
<ConnectorForm
|
||||
connectors={formData.connectors || []}
|
||||
onAddConnector={handleAddConnector}
|
||||
onRemoveConnector={handleRemoveConnector}
|
||||
onConnectorNameChange={handleConnectorNameChange}
|
||||
onConnectorConfigChange={handleConnectorConfigChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConnectorsSection;
|
||||
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Navigation sidebar for the agent form
|
||||
*/
|
||||
const FormNavSidebar = ({ activeSection, handleSectionChange }) => {
|
||||
// Define the navigation items
|
||||
const navItems = [
|
||||
{ id: 'basic-section', icon: 'fas fa-info-circle', label: 'Basic Information' },
|
||||
{ id: 'connectors-section', icon: 'fas fa-plug', label: 'Connectors' },
|
||||
{ id: 'actions-section', icon: 'fas fa-bolt', label: 'Actions' },
|
||||
{ id: 'mcp-section', icon: 'fas fa-server', label: 'MCP Servers' },
|
||||
{ id: 'memory-section', icon: 'fas fa-memory', label: 'Memory Settings' },
|
||||
{ id: 'model-section', icon: 'fas fa-robot', label: 'Model Settings' },
|
||||
{ id: 'prompts-section', icon: 'fas fa-comment-alt', label: 'Prompts & Goals' },
|
||||
{ id: 'advanced-section', icon: 'fas fa-cogs', label: 'Advanced Settings' }
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="wizard-sidebar">
|
||||
<ul className="wizard-nav">
|
||||
{navItems.map(item => (
|
||||
<li
|
||||
key={item.id}
|
||||
className={`wizard-nav-item ${activeSection === item.id ? 'active' : ''}`}
|
||||
onClick={() => handleSectionChange(item.id)}
|
||||
>
|
||||
<i className={item.icon}></i> {item.label}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FormNavSidebar;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* MCP Servers section of the agent form
|
||||
*/
|
||||
const MCPServersSection = ({
|
||||
formData,
|
||||
handleAddMCPServer,
|
||||
handleRemoveMCPServer,
|
||||
handleMCPServerChange
|
||||
}) => {
|
||||
return (
|
||||
<div id="mcp-section">
|
||||
<h3 className="section-title">MCP Servers</h3>
|
||||
<p className="section-description">
|
||||
Configure MCP servers for this agent.
|
||||
</p>
|
||||
|
||||
<div className="mcp-servers-container">
|
||||
{formData.mcp_servers && formData.mcp_servers.map((server, index) => (
|
||||
<div key={index} className="mcp-server-item mb-4">
|
||||
<div className="mcp-server-header">
|
||||
<h4>MCP Server #{index + 1}</h4>
|
||||
<button
|
||||
type="button"
|
||||
className="remove-btn"
|
||||
onClick={() => handleRemoveMCPServer(index)}
|
||||
>
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor={`mcp-url-${index}`}>URL</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`mcp-url-${index}`}
|
||||
value={server.url || ''}
|
||||
onChange={(e) => handleMCPServerChange(index, 'url', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="https://example.com/mcp"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<label htmlFor={`mcp-api-key-${index}`}>API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
id={`mcp-api-key-${index}`}
|
||||
value={server.api_key || ''}
|
||||
onChange={(e) => handleMCPServerChange(index, 'api_key', e.target.value)}
|
||||
className="form-control"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="add-btn"
|
||||
onClick={handleAddMCPServer}
|
||||
>
|
||||
<i className="fas fa-plus"></i> Add MCP Server
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCPServersSection;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Memory Settings section of the agent form
|
||||
*/
|
||||
const MemorySettingsSection = ({ formData, handleInputChange }) => {
|
||||
return (
|
||||
<div id="memory-section">
|
||||
<h3 className="section-title">Memory Settings</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="memory_provider">Memory Provider</label>
|
||||
<select
|
||||
name="memory_provider"
|
||||
id="memory_provider"
|
||||
value={formData.memory_provider || 'local'}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
>
|
||||
<option value="local">Local</option>
|
||||
<option value="redis">Redis</option>
|
||||
<option value="postgres">PostgreSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="memory_collection">Memory Collection</label>
|
||||
<input
|
||||
type="text"
|
||||
name="memory_collection"
|
||||
id="memory_collection"
|
||||
value={formData.memory_collection || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
placeholder="agent_memories"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="memory_url">Memory URL</label>
|
||||
<input
|
||||
type="text"
|
||||
name="memory_url"
|
||||
id="memory_url"
|
||||
value={formData.memory_url || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
placeholder="redis://localhost:6379"
|
||||
/>
|
||||
<small className="form-text text-muted">Connection URL for Redis or PostgreSQL</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="memory_window_size">Memory Window Size</label>
|
||||
<input
|
||||
type="number"
|
||||
name="memory_window_size"
|
||||
id="memory_window_size"
|
||||
min="1"
|
||||
value={formData.memory_window_size || 10}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
/>
|
||||
<small className="form-text text-muted">Number of recent messages to include in context window</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemorySettingsSection;
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Model Settings section of the agent form
|
||||
*/
|
||||
const ModelSettingsSection = ({ formData, handleInputChange }) => {
|
||||
return (
|
||||
<div id="model-section">
|
||||
<h3 className="section-title">Model Settings</h3>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="model">Model</label>
|
||||
<input
|
||||
type="text"
|
||||
name="model"
|
||||
id="model"
|
||||
value={formData.model || ''}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="multimodal_model">Multimodal Model</label>
|
||||
<input
|
||||
type="text"
|
||||
name="multimodal_model"
|
||||
id="multimodal_model"
|
||||
value={formData.multimodal_model || ''}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="api_url">API URL</label>
|
||||
<input
|
||||
type="text"
|
||||
name="api_url"
|
||||
id="api_url"
|
||||
value={formData.api_url || ''}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="api_key">API Key</label>
|
||||
<input
|
||||
type="password"
|
||||
name="api_key"
|
||||
id="api_key"
|
||||
value={formData.api_key || ''}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="temperature">Temperature</label>
|
||||
<input
|
||||
type="number"
|
||||
name="temperature"
|
||||
id="temperature"
|
||||
min="0"
|
||||
max="2"
|
||||
step="0.1"
|
||||
value={formData.temperature || 0.7}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="max_tokens">Max Tokens</label>
|
||||
<input
|
||||
type="number"
|
||||
name="max_tokens"
|
||||
id="max_tokens"
|
||||
min="1"
|
||||
value={formData.max_tokens || 2000}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModelSettingsSection;
|
||||
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Prompts & Goals section of the agent form
|
||||
*/
|
||||
const PromptsGoalsSection = ({ formData, handleInputChange, isGroupForm }) => {
|
||||
// In group form context, we hide the system prompt as it comes from each agent profile
|
||||
return (
|
||||
<div id="prompts-section">
|
||||
<h3 className="section-title">Prompts & Goals</h3>
|
||||
|
||||
{!isGroupForm && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="system_prompt">System Prompt</label>
|
||||
<textarea
|
||||
name="system_prompt"
|
||||
id="system_prompt"
|
||||
value={formData.system_prompt || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
rows="5"
|
||||
/>
|
||||
<small className="form-text text-muted">Instructions that define the agent's behavior</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="goals">Goals</label>
|
||||
<textarea
|
||||
name="goals"
|
||||
id="goals"
|
||||
value={formData.goals || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
rows="5"
|
||||
/>
|
||||
<small className="form-text text-muted">Define the agent's goals (one per line)</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="constraints">Constraints</label>
|
||||
<textarea
|
||||
name="constraints"
|
||||
id="constraints"
|
||||
value={formData.constraints || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
rows="5"
|
||||
/>
|
||||
<small className="form-text text-muted">Define the agent's constraints (one per line)</small>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label htmlFor="tools">Tools</label>
|
||||
<textarea
|
||||
name="tools"
|
||||
id="tools"
|
||||
value={formData.tools || ''}
|
||||
onChange={handleInputChange}
|
||||
className="form-control"
|
||||
rows="5"
|
||||
/>
|
||||
<small className="form-text text-muted">Define the agent's tools (one per line)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PromptsGoalsSection;
|
||||
9
webui/react-ui/src/components/agent-form-sections/index.js
vendored
Normal file
9
webui/react-ui/src/components/agent-form-sections/index.js
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
export { default as BasicInfoSection } from './BasicInfoSection';
|
||||
export { default as ModelSettingsSection } from './ModelSettingsSection';
|
||||
export { default as ConnectorsSection } from './ConnectorsSection';
|
||||
export { default as ActionsSection } from './ActionsSection';
|
||||
export { default as MCPServersSection } from './MCPServersSection';
|
||||
export { default as MemorySettingsSection } from './MemorySettingsSection';
|
||||
export { default as PromptsGoalsSection } from './PromptsGoalsSection';
|
||||
export { default as AdvancedSettingsSection } from './AdvancedSettingsSection';
|
||||
export { default as FormNavSidebar } from './FormNavSidebar';
|
||||
221
webui/react-ui/src/components/agent-form-sections/styles.css
Normal file
221
webui/react-ui/src/components/agent-form-sections/styles.css
Normal file
@@ -0,0 +1,221 @@
|
||||
/* Agent Form Section Styles */
|
||||
|
||||
.form-section {
|
||||
padding: 1.5rem;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.section-description {
|
||||
color: #666;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Form Controls */
|
||||
.form-controls {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background-color: #4a6cf7;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background-color: #3a5ce5;
|
||||
}
|
||||
|
||||
.submit-btn:disabled {
|
||||
background-color: #a0a0a0;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Error Message */
|
||||
.error-message {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #721c24;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Navigation Sidebar */
|
||||
.wizard-sidebar {
|
||||
width: 250px;
|
||||
background-color: #f8f9fa;
|
||||
border-right: 1px solid #eee;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.wizard-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wizard-nav-item {
|
||||
padding: 0.75rem 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.wizard-nav-item:hover {
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.wizard-nav-item.active {
|
||||
background-color: #e9ecef;
|
||||
border-left: 4px solid #4a6cf7;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wizard-nav-item i {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Form Layout */
|
||||
.agent-form-container {
|
||||
display: flex;
|
||||
min-height: 80vh;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.form-content-area {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Input Styles */
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
input[type="number"],
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-label input[type="checkbox"] {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Add and Remove Buttons */
|
||||
.add-btn,
|
||||
.remove-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
color: #4a6cf7;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
color: #3a5ce5;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.remove-btn:hover {
|
||||
color: #c82333;
|
||||
}
|
||||
|
||||
/* Item Containers */
|
||||
.action-item,
|
||||
.mcp-server-item {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.action-header,
|
||||
.mcp-server-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.action-header h4,
|
||||
.mcp-server-header h4 {
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Discord connector template
|
||||
*/
|
||||
const DiscordConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="connector-template">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`discordToken${index}`}>Discord Bot Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`discordToken${index}`}
|
||||
value={getConfigValue(connector, 'token', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="Bot token from Discord Developer Portal"
|
||||
/>
|
||||
<small className="form-text text-muted">Get this from the Discord Developer Portal</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`discordDefaultChannel${index}`}>Default Channel</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`discordDefaultChannel${index}`}
|
||||
value={getConfigValue(connector, 'defaultChannel', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'defaultChannel', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="123456789012345678"
|
||||
/>
|
||||
<small className="form-text text-muted">Channel ID to always answer even if not mentioned</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DiscordConnector;
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
/**
|
||||
* Fallback connector template for unknown connector types
|
||||
*/
|
||||
const FallbackConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
|
||||
const [newConfigKey, setNewConfigKey] = useState('');
|
||||
|
||||
// Parse config if it's a string
|
||||
let parsedConfig = connector.config;
|
||||
if (typeof parsedConfig === 'string') {
|
||||
try {
|
||||
parsedConfig = JSON.parse(parsedConfig);
|
||||
} catch (err) {
|
||||
console.error('Error parsing config:', err);
|
||||
parsedConfig = {};
|
||||
}
|
||||
} else if (!parsedConfig) {
|
||||
parsedConfig = {};
|
||||
}
|
||||
|
||||
// Handle adding a new custom field
|
||||
const handleAddCustomField = () => {
|
||||
if (newConfigKey) {
|
||||
onConnectorConfigChange(index, newConfigKey, '');
|
||||
setNewConfigKey('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="connector-template">
|
||||
{/* Individual field inputs */}
|
||||
{parsedConfig && Object.entries(parsedConfig).map(([key, value]) => (
|
||||
<div key={key} className="form-group mb-3">
|
||||
<label htmlFor={`connector-${index}-${key}`}>{key}</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`connector-${index}-${key}`}
|
||||
className="form-control"
|
||||
value={value}
|
||||
onChange={(e) => onConnectorConfigChange(index, key, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add custom configuration field */}
|
||||
<div className="add-config-field mt-4">
|
||||
<h5>Add Custom Configuration Field</h5>
|
||||
<div className="input-group mb-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="New config key"
|
||||
className="form-control"
|
||||
value={newConfigKey}
|
||||
onChange={(e) => setNewConfigKey(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleAddCustomField()}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-outline-primary"
|
||||
onClick={handleAddCustomField}
|
||||
>
|
||||
<i className="fas fa-plus"></i> Add Field
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FallbackConnector;
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* GitHub Issues connector template
|
||||
*/
|
||||
const GithubIssuesConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="connector-template">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubToken${index}`}>GitHub Personal Access Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubToken${index}`}
|
||||
value={getConfigValue(connector, 'token', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="ghp_..."
|
||||
/>
|
||||
<small className="form-text text-muted">Personal access token with repo scope</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubOwner${index}`}
|
||||
value={getConfigValue(connector, 'owner', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'owner', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="username or organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubRepo${index}`}
|
||||
value={getConfigValue(connector, 'repository', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'repository', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="repository-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`replyIfNoReplies${index}`}>Reply Behavior</label>
|
||||
<select
|
||||
id={`replyIfNoReplies${index}`}
|
||||
value={getConfigValue(connector, 'replyIfNoReplies', 'false')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'replyIfNoReplies', e.target.value)}
|
||||
className="form-control"
|
||||
>
|
||||
<option value="false">Reply to all issues</option>
|
||||
<option value="true">Only reply to issues with no comments</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`pollInterval${index}`}>Poll Interval</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`pollInterval${index}`}
|
||||
value={getConfigValue(connector, 'pollInterval', '10m')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'pollInterval', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="10m"
|
||||
/>
|
||||
<small className="form-text text-muted">How often to check for new issues (e.g., 10m, 1h)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubIssuesConnector;
|
||||
@@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* GitHub PRs connector template
|
||||
*/
|
||||
const GithubPRsConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="connector-template">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubToken${index}`}>GitHub Personal Access Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubToken${index}`}
|
||||
value={getConfigValue(connector, 'token', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="ghp_..."
|
||||
/>
|
||||
<small className="form-text text-muted">Personal access token with repo scope</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubOwner${index}`}>Repository Owner</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubOwner${index}`}
|
||||
value={getConfigValue(connector, 'owner', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'owner', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="username or organization"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`githubRepo${index}`}>Repository Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`githubRepo${index}`}
|
||||
value={getConfigValue(connector, 'repository', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'repository', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="repository-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`replyIfNoReplies${index}`}>Reply Behavior</label>
|
||||
<select
|
||||
id={`replyIfNoReplies${index}`}
|
||||
value={getConfigValue(connector, 'replyIfNoReplies', 'false')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'replyIfNoReplies', e.target.value)}
|
||||
className="form-control"
|
||||
>
|
||||
<option value="false">Reply to all PRs</option>
|
||||
<option value="true">Only reply to PRs with no comments</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`pollInterval${index}`}>Poll Interval</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`pollInterval${index}`}
|
||||
value={getConfigValue(connector, 'pollInterval', '10m')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'pollInterval', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="10m"
|
||||
/>
|
||||
<small className="form-text text-muted">How often to check for new PRs (e.g., 10m, 1h)</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GithubPRsConnector;
|
||||
76
webui/react-ui/src/components/connectors/IRCConnector.jsx
Normal file
76
webui/react-ui/src/components/connectors/IRCConnector.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* IRC connector template
|
||||
*/
|
||||
const IRCConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="connector-template">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`ircServer${index}`}>IRC Server</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`ircServer${index}`}
|
||||
value={getConfigValue(connector, 'server', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'server', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="irc.libera.chat"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`ircPort${index}`}>Port</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`ircPort${index}`}
|
||||
value={getConfigValue(connector, 'port', '6667')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'port', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="6667"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`ircNick${index}`}>Nickname</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`ircNick${index}`}
|
||||
value={getConfigValue(connector, 'nickname', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'nickname', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="MyAgentBot"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`ircChannels${index}`}>Channel</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`ircChannels${index}`}
|
||||
value={getConfigValue(connector, 'channel', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'channel', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="#channel1"
|
||||
/>
|
||||
<small className="form-text text-muted">Channel to join</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<div className="form-check">
|
||||
<label className="checkbox-label" htmlFor={`ircAlwaysReply${index}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`ircAlwaysReply${index}`}
|
||||
checked={getConfigValue(connector, 'alwaysReply', '') === 'true'}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'alwaysReply', e.target.checked ? 'true' : 'false')}
|
||||
/>
|
||||
Always Reply
|
||||
</label>
|
||||
<small className="form-text text-muted d-block">If checked, the agent will reply to all messages in the channel</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IRCConnector;
|
||||
67
webui/react-ui/src/components/connectors/SlackConnector.jsx
Normal file
67
webui/react-ui/src/components/connectors/SlackConnector.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Slack connector template
|
||||
*/
|
||||
const SlackConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="connector-template">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`slackAppToken${index}`}>Slack App Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`slackAppToken${index}`}
|
||||
value={getConfigValue(connector, 'appToken', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'appToken', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="xapp-..."
|
||||
/>
|
||||
<small className="form-text text-muted">App-level token starting with xapp-</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`slackBotToken${index}`}>Slack Bot Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`slackBotToken${index}`}
|
||||
value={getConfigValue(connector, 'botToken', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'botToken', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="xoxb-..."
|
||||
/>
|
||||
<small className="form-text text-muted">Bot token starting with xoxb-</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`slackChannelID${index}`}>Slack Channel ID</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`slackChannelID${index}`}
|
||||
value={getConfigValue(connector, 'channelID', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'channelID', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="C1234567890"
|
||||
/>
|
||||
<small className="form-text text-muted">Optional: Specific channel ID to join</small>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<div className="form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`slackAlwaysReply${index}`}
|
||||
checked={getConfigValue(connector, 'alwaysReply', '') === 'true'}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'alwaysReply', e.target.checked ? 'true' : 'false')}
|
||||
className="form-check-input"
|
||||
/>
|
||||
<label className="form-check-label" htmlFor={`slackAlwaysReply${index}`}>
|
||||
Always Reply
|
||||
</label>
|
||||
<small className="form-text text-muted d-block">If checked, the agent will reply to all messages in the channel</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SlackConnector;
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Telegram connector template
|
||||
*/
|
||||
const TelegramConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="connector-template">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`telegramToken${index}`}>Telegram Bot Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`telegramToken${index}`}
|
||||
value={getConfigValue(connector, 'token', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'token', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
|
||||
/>
|
||||
<small className="form-text text-muted">Get this from @BotFather on Telegram</small>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TelegramConnector;
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Twitter connector template
|
||||
*/
|
||||
const TwitterConnector = ({ connector, index, onConnectorConfigChange, getConfigValue }) => {
|
||||
return (
|
||||
<div className="connector-template">
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`twitterApiKey${index}`}>API Key</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`twitterApiKey${index}`}
|
||||
value={getConfigValue(connector, 'apiKey', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'apiKey', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="Twitter API Key"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`twitterApiSecret${index}`}>API Secret</label>
|
||||
<input
|
||||
type="password"
|
||||
id={`twitterApiSecret${index}`}
|
||||
value={getConfigValue(connector, 'apiSecret', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'apiSecret', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="Twitter API Secret"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`twitterAccessToken${index}`}>Access Token</label>
|
||||
<input
|
||||
type="text"
|
||||
id={`twitterAccessToken${index}`}
|
||||
value={getConfigValue(connector, 'accessToken', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'accessToken', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="Twitter Access Token"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`twitterAccessSecret${index}`}>Access Token Secret</label>
|
||||
<input
|
||||
type="password"
|
||||
id={`twitterAccessSecret${index}`}
|
||||
value={getConfigValue(connector, 'accessSecret', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'accessSecret', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="Twitter Access Token Secret"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group mb-3">
|
||||
<label htmlFor={`twitterBearerToken${index}`}>Bearer Token</label>
|
||||
<input
|
||||
type="password"
|
||||
id={`twitterBearerToken${index}`}
|
||||
value={getConfigValue(connector, 'bearerToken', '')}
|
||||
onChange={(e) => onConnectorConfigChange(index, 'bearerToken', e.target.value)}
|
||||
className="form-control"
|
||||
placeholder="Twitter Bearer Token"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwitterConnector;
|
||||
112
webui/react-ui/src/hooks/useAgent.js
vendored
Normal file
112
webui/react-ui/src/hooks/useAgent.js
vendored
Normal 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
78
webui/react-ui/src/hooks/useChat.js
vendored
Normal 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
63
webui/react-ui/src/hooks/useSSE.js
vendored
Normal 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 };
|
||||
}
|
||||
68
webui/react-ui/src/index.css
Normal file
68
webui/react-ui/src/index.css
Normal 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;
|
||||
}
|
||||
}
|
||||
24
webui/react-ui/src/main.jsx
Normal file
24
webui/react-ui/src/main.jsx
Normal 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>,
|
||||
)
|
||||
204
webui/react-ui/src/pages/ActionsPlayground.jsx
Normal file
204
webui/react-ui/src/pages/ActionsPlayground.jsx
Normal 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;
|
||||
172
webui/react-ui/src/pages/AgentSettings.jsx
Normal file
172
webui/react-ui/src/pages/AgentSettings.jsx
Normal 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;
|
||||
181
webui/react-ui/src/pages/AgentsList.jsx
Normal file
181
webui/react-ui/src/pages/AgentsList.jsx
Normal 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;
|
||||
106
webui/react-ui/src/pages/Chat.jsx
Normal file
106
webui/react-ui/src/pages/Chat.jsx
Normal 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;
|
||||
90
webui/react-ui/src/pages/CreateAgent.jsx
Normal file
90
webui/react-ui/src/pages/CreateAgent.jsx
Normal 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;
|
||||
490
webui/react-ui/src/pages/GroupCreate.jsx
Normal file
490
webui/react-ui/src/pages/GroupCreate.jsx
Normal 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;
|
||||
137
webui/react-ui/src/pages/Home.jsx
Normal file
137
webui/react-ui/src/pages/Home.jsx
Normal 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;
|
||||
52
webui/react-ui/src/router.jsx
Normal file
52
webui/react-ui/src/router.jsx
Normal 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
198
webui/react-ui/src/utils/api.js
vendored
Normal 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
49
webui/react-ui/src/utils/config.js
vendored
Normal 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}`,
|
||||
}
|
||||
};
|
||||
32
webui/react-ui/vite.config.js
Normal file
32
webui/react-ui/vite.config.js
Normal 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,
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user