feat(ui): Add status page to react frontend
This commit is contained in:
@@ -288,27 +288,17 @@ a:hover {
|
|||||||
|
|
||||||
/* Status badge */
|
/* Status badge */
|
||||||
.status-badge {
|
.status-badge {
|
||||||
display: inline-block;
|
position: absolute;
|
||||||
padding: 0.25rem 0.75rem;
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
padding: 5px 12px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
margin-left: 10px;
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
|
||||||
}
|
z-index: 1;
|
||||||
|
|
||||||
.status-badge.active {
|
|
||||||
background-color: var(--primary);
|
|
||||||
color: #000;
|
|
||||||
box-shadow: 0 0 10px rgba(0, 255, 149, 0.5);
|
|
||||||
animation: pulse 1.5s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-badge.inactive {
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
color: #fff;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-active, .active {
|
.status-active, .active {
|
||||||
@@ -1650,9 +1640,10 @@ select.form-control {
|
|||||||
/* Agent header styling */
|
/* Agent header styling */
|
||||||
.agent-header {
|
.agent-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 15px;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-header h2 {
|
.agent-header h2 {
|
||||||
@@ -1835,3 +1826,143 @@ select.form-control {
|
|||||||
.text-center {
|
.text-center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Agent Status Page */
|
||||||
|
.agent-status-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-status-container .page-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-status-container .header-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-status-container .back-link {
|
||||||
|
margin-right: 1rem;
|
||||||
|
color: var(--primary);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-status-container .back-link:hover {
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
background-color: var(--darker-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
height: 70vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-messages {
|
||||||
|
overflow-y: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary);
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
color: var(--text);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value.reasoning {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-status-data {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 200px;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: block;
|
||||||
|
margin: 15px auto;
|
||||||
|
position: relative;
|
||||||
|
color: var(--primary);
|
||||||
|
box-sizing: border-box;
|
||||||
|
animation: animloader 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animloader {
|
||||||
|
0% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
25% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 2px; }
|
||||||
|
50% { box-shadow: 14px 0 0 -2px, 38px 0 0 -2px, -14px 0 0 2px, -38px 0 0 -2px; }
|
||||||
|
75% { box-shadow: 14px 0 0 2px, 38px 0 0 -2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
100% { box-shadow: 14px 0 0 -2px, 38px 0 0 2px, -14px 0 0 -2px, -38px 0 0 -2px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-container h2 {
|
||||||
|
color: var(--danger);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background-color: var(--medium-bg);
|
||||||
|
color: var(--primary);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn:hover {
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
color: var(--primary);
|
||||||
|
}
|
||||||
|
|||||||
136
webui/react-ui/src/pages/AgentStatus.jsx
Normal file
136
webui/react-ui/src/pages/AgentStatus.jsx
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useParams, Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
function AgentStatus() {
|
||||||
|
const { name } = useParams();
|
||||||
|
const [statusData, setStatusData] = useState(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [eventSource, setEventSource] = useState(null);
|
||||||
|
const [liveUpdates, setLiveUpdates] = useState([]);
|
||||||
|
|
||||||
|
// Fetch initial status data
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchStatusData = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/agent/${name}/status`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Server responded with status: ${response.status}`);
|
||||||
|
}
|
||||||
|
const data = await response.json();
|
||||||
|
setStatusData(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching agent status:', err);
|
||||||
|
setError(`Failed to load status for agent "${name}": ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchStatusData();
|
||||||
|
|
||||||
|
// Setup SSE connection for live updates
|
||||||
|
const sse = new EventSource(`/sse/${name}`);
|
||||||
|
setEventSource(sse);
|
||||||
|
|
||||||
|
sse.addEventListener('status', (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
setLiveUpdates(prev => [data, ...prev.slice(0, 19)]); // Keep last 20 updates
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error parsing SSE data:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
sse.onerror = (err) => {
|
||||||
|
console.error('SSE connection error:', err);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cleanup on unmount
|
||||||
|
return () => {
|
||||||
|
if (sse) {
|
||||||
|
sse.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="loading-container">
|
||||||
|
<div className="loader"></div>
|
||||||
|
<p>Loading agent status...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="error-container">
|
||||||
|
<h2>Error</h2>
|
||||||
|
<p>{error}</p>
|
||||||
|
<Link to="/agents" className="back-btn">
|
||||||
|
<i className="fas fa-arrow-left"></i> Back to Agents
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine live updates with history
|
||||||
|
const allUpdates = [...liveUpdates, ...(statusData?.History || [])];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="agent-status-container">
|
||||||
|
<header className="page-header">
|
||||||
|
<div className="header-content">
|
||||||
|
<h1>
|
||||||
|
<Link to="/agents" className="back-link">
|
||||||
|
<i className="fas fa-arrow-left"></i>
|
||||||
|
</Link>
|
||||||
|
Agent Status: {name}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="chat-container bg-gray-800 shadow-lg rounded-lg">
|
||||||
|
{/* Chat Messages */}
|
||||||
|
<div className="chat-messages p-4">
|
||||||
|
{allUpdates.length > 0 ? (
|
||||||
|
allUpdates.map((item, index) => (
|
||||||
|
<div key={index} className="status-item mb-4">
|
||||||
|
<div className="bg-gray-700 p-4 rounded-lg">
|
||||||
|
<h2 className="text-sm font-semibold mb-2">Agent Action:</h2>
|
||||||
|
<div className="status-details">
|
||||||
|
<div className="status-row">
|
||||||
|
<span className="status-label">Result:</span>
|
||||||
|
<span className="status-value">{item.Result || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="status-row">
|
||||||
|
<span className="status-label">Action:</span>
|
||||||
|
<span className="status-value">{item.Action || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="status-row">
|
||||||
|
<span className="status-label">Parameters:</span>
|
||||||
|
<span className="status-value">{item.Params || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
{item.Reasoning && (
|
||||||
|
<div className="status-row">
|
||||||
|
<span className="status-label">Reasoning:</span>
|
||||||
|
<span className="status-value reasoning">{item.Reasoning}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="no-status-data">
|
||||||
|
<p>No status data available for this agent.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgentStatus;
|
||||||
@@ -7,6 +7,7 @@ import CreateAgent from './pages/CreateAgent';
|
|||||||
import Chat from './pages/Chat';
|
import Chat from './pages/Chat';
|
||||||
import ActionsPlayground from './pages/ActionsPlayground';
|
import ActionsPlayground from './pages/ActionsPlayground';
|
||||||
import GroupCreate from './pages/GroupCreate';
|
import GroupCreate from './pages/GroupCreate';
|
||||||
|
import AgentStatus from './pages/AgentStatus';
|
||||||
|
|
||||||
// Get the base URL from Vite's environment variables or default to '/app/'
|
// Get the base URL from Vite's environment variables or default to '/app/'
|
||||||
const BASE_URL = import.meta.env.BASE_URL || '/app';
|
const BASE_URL = import.meta.env.BASE_URL || '/app';
|
||||||
@@ -44,6 +45,10 @@ export const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: 'group-create',
|
path: 'group-create',
|
||||||
element: <GroupCreate />
|
element: <GroupCreate />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'status/:name',
|
||||||
|
element: <AgentStatus />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,19 @@ func (app *App) registerRoutes(pool *state.AgentPool, webapp *fiber.App) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// API endpoint for agent status history
|
||||||
|
webapp.Get("/api/agent/:name/status", func(c *fiber.Ctx) error {
|
||||||
|
history := pool.GetStatusHistory(c.Params("name"))
|
||||||
|
if history == nil {
|
||||||
|
history = &state.Status{ActionResults: []types.ActionState{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"Name": c.Params("name"),
|
||||||
|
"History": Reverse(history.Results()),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
webapp.Post("/settings/import", app.ImportAgent(pool))
|
webapp.Post("/settings/import", app.ImportAgent(pool))
|
||||||
webapp.Get("/settings/export/:name", app.ExportAgent(pool))
|
webapp.Get("/settings/export/:name", app.ExportAgent(pool))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user