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:
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;
|
||||
Reference in New Issue
Block a user