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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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