feat(agents): own agents page with tree sidebar in StartLayout, like settings/help

This commit is contained in:
Agent Zero
2026-08-17 11:46:08 +02:00
parent cbb17c4ddd
commit 7c6f33983d
5 changed files with 173 additions and 3 deletions
+109
View File
@@ -0,0 +1,109 @@
import React, { useState } from 'react';
import { NavLink, Outlet } from 'react-router-dom';
import { useUIStore } from '@/store/uiStore';
import { ChevronRight, ChevronDown, Bot, Zap, Brain, Cpu, Settings2, Activity, MessageSquare } from 'lucide-react';
interface AgentNode {
title: string;
path?: string;
icon?: React.ReactNode;
children?: AgentNode[];
}
const AGENT_TREE: AgentNode[] = [
{
title: 'Agenten',
icon: <Bot className="w-4 h-4" />,
children: [
{ title: 'Übersicht', path: '/agents/overview' },
{ title: 'Ausführungen', path: '/agents/runs' },
{ title: 'Versionen', path: '/agents/versions' },
],
},
{
title: 'Automation',
icon: <Zap className="w-4 h-4" />,
children: [
{ title: 'Übersicht', path: '/agents/automation' },
{ title: 'Einstellungen', path: '/agents/automation-settings' },
],
},
{
title: 'KI',
icon: <Brain className="w-4 h-4" />,
children: [
{ title: 'Assistent', path: '/agents/ai-assistant' },
{ title: 'Einstellungen', path: '/agents/ai-settings' },
{ title: 'Proactive AI', path: '/agents/ai-proactive' },
],
},
];
function TreeItem({ node, depth }: { node: AgentNode; depth: number }) {
const [expanded, setExpanded] = useState(depth < 1);
const hasChildren = node.children && node.children.length > 0;
const paddingLeft = depth * 16 + 12;
if (hasChildren) {
return (
<div>
<button
onClick={() => setExpanded(!expanded)}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium text-secondary-700 hover:bg-secondary-100 transition-colors min-h-touch"
style={{ paddingLeft }}
aria-expanded={expanded}
>
{expanded ? <ChevronDown className="w-3.5 h-3.5 flex-shrink-0" /> : <ChevronRight className="w-3.5 h-3.5 flex-shrink-0" />}
{node.icon}
<span>{node.title}</span>
</button>
{expanded && (
<div>
{node.children!.map((child) => (
<TreeItem key={child.path || child.title} node={child} depth={depth + 1} />
))}
</div>
)}
</div>
);
}
return (
<NavLink
to={node.path!}
className={({ isActive }) =>
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors min-h-touch ${
isActive
? 'bg-primary-100 text-primary-700 font-medium'
: 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'
}`
}
style={{ paddingLeft: paddingLeft + 20 }}
>
{node.icon}
<span>{node.title}</span>
</NavLink>
);
}
export function AgentsPage() {
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
return (
<div className="flex h-full overflow-hidden" data-testid="agents-page">
<aside className={`${sidebarOpen ? 'w-64 border-r border-secondary-200' : 'w-0'} flex-shrink-0 min-h-[calc(100vh-4rem)] transition-all duration-200 overflow-hidden`}>
<div className="w-64 flex-shrink-0 p-4">
<h1 className="text-xl font-bold text-secondary-900 mb-4">Agenten</h1>
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label="Agenten Navigation">
{AGENT_TREE.map((node) => (
<TreeItem key={node.title} node={node} depth={0} />
))}
</nav>
</div>
</aside>
<main className="flex-1 p-6 overflow-y-auto" data-testid="agents-content">
<Outlet />
</main>
</div>
);
}
+1 -1
View File
@@ -40,7 +40,7 @@ export function StartPage() {
const menuItems = [ const menuItems = [
{ id: 'workspaces', label: 'Workspaces', icon: <LayoutGrid className="w-4 h-4" />, active: true }, { id: 'workspaces', label: 'Workspaces', icon: <LayoutGrid className="w-4 h-4" />, active: true },
{ id: 'settings', label: 'Einstellungen', icon: <Settings className="w-4 h-4" />, onClick: () => navigate('/settings') }, { id: 'settings', label: 'Einstellungen', icon: <Settings className="w-4 h-4" />, onClick: () => navigate('/settings') },
{ id: 'agents', label: 'Agenten', icon: <Bot className="w-4 h-4" />, onClick: () => navigate('/agents') }, { id: 'agents', label: 'Agenten', icon: <Bot className="w-4 h-4" />, onClick: () => navigate('/agents/overview') },
{ id: 'help', label: 'Hilfe', icon: <HelpCircle className="w-4 h-4" />, onClick: () => navigate('/help/welcome') }, { id: 'help', label: 'Hilfe', icon: <HelpCircle className="w-4 h-4" />, onClick: () => navigate('/help/welcome') },
]; ];
@@ -0,0 +1,35 @@
import React from 'react';
export function AgentsOverviewPage() {
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Agenten Übersicht</h1>
<p className="text-secondary-600 mb-6">
Verwalten Sie KI-Agenten, führen Sie diese aus und überwachen Sie deren Ausführungen.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="p-6 bg-white rounded-xl border border-secondary-200">
<div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M3 13l2-6h14l2 6M3 13v4a2 2 0 002 2h14a2 2 0 002-2v-4" /></svg>
</div>
<h3 className="font-semibold text-secondary-900">Agenten</h3>
<p className="text-sm text-secondary-500 mt-1">KI-Agenten erstellen, konfigurieren und ausführen</p>
</div>
<div className="p-6 bg-white rounded-xl border border-secondary-200">
<div className="w-12 h-12 bg-warning-100 rounded-lg flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-warning-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
</div>
<h3 className="font-semibold text-secondary-900">Automation</h3>
<p className="text-sm text-secondary-500 mt-1">Automatisierte Workflows und Trigger</p>
</div>
<div className="p-6 bg-white rounded-xl border border-secondary-200">
<div className="w-12 h-12 bg-success-100 rounded-lg flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-success-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" /></svg>
</div>
<h3 className="font-semibold text-secondary-900">KI</h3>
<p className="text-sm text-secondary-500 mt-1">KI-Assistent und Proactive AI</p>
</div>
</div>
</div>
);
}
@@ -0,0 +1,12 @@
import React from 'react';
export function AgentsPlaceholderPage() {
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Agenten</h1>
<p className="text-secondary-600">
Diese Seite wird gerade erstellt. Wählen Sie ein Thema aus dem Menü links.
</p>
</div>
);
}
+16 -2
View File
@@ -77,6 +77,9 @@ const HelpNavigationPage = React.lazy(() => import('@/pages/help/HelpNavigation'
const HelpContactsPage = React.lazy(() => import('@/pages/help/HelpContacts').then(m => ({ default: m.HelpContactsPage }))); const HelpContactsPage = React.lazy(() => import('@/pages/help/HelpContacts').then(m => ({ default: m.HelpContactsPage })));
const HelpMailSetupPage = React.lazy(() => import('@/pages/help/HelpMailSetup').then(m => ({ default: m.HelpMailSetupPage }))); const HelpMailSetupPage = React.lazy(() => import('@/pages/help/HelpMailSetup').then(m => ({ default: m.HelpMailSetupPage })));
const HelpPlaceholderPage = React.lazy(() => import('@/pages/help/HelpPlaceholder').then(m => ({ default: m.HelpPlaceholderPage }))); const HelpPlaceholderPage = React.lazy(() => import('@/pages/help/HelpPlaceholder').then(m => ({ default: m.HelpPlaceholderPage })));
const AgentsPage = React.lazy(() => import('@/pages/Agents').then(m => ({ default: m.AgentsPage })));
const AgentsOverviewPage = React.lazy(() => import('@/pages/agents/AgentsOverview').then(m => ({ default: m.AgentsOverviewPage })));
const AgentsPlaceholderPage = React.lazy(() => import('@/pages/agents/AgentsPlaceholder').then(m => ({ default: m.AgentsPlaceholderPage })));
const ApiDocsPage = React.lazy(() => import('@/pages/ApiDocs').then(m => ({ default: m.ApiDocsPage }))); const ApiDocsPage = React.lazy(() => import('@/pages/ApiDocs').then(m => ({ default: m.ApiDocsPage })));
/** Centered spinner fallback for lazy-loaded routes */ /** Centered spinner fallback for lazy-loaded routes */
@@ -158,6 +161,19 @@ const router = createBrowserRouter([
{ path: '*', element: withSuspense(<HelpPlaceholderPage />) }, { path: '*', element: withSuspense(<HelpPlaceholderPage />) },
], ],
}, },
{
path: '/agents',
element: withSuspense(<AgentsPage />),
children: [
{ path: 'overview', element: withSuspense(<AgentsOverviewPage />) },
{ path: 'automation', element: withSuspense(<AutomationDashboardPage />) },
{ path: 'automation-settings', element: withSuspense(<AutomationSettingsPage />) },
{ path: 'ai-assistant', element: withSuspense(<AIAssistantPage />) },
{ path: 'ai-settings', element: withSuspense(<AISettingsPage />) },
{ path: 'ai-proactive', element: withSuspense(<ProactiveAISettings />) },
{ path: '*', element: withSuspense(<AgentsPlaceholderPage />) },
],
},
{ {
path: '/settings', path: '/settings',
element: <PermissionRoute permission="settings:read">{withSuspense(<SettingsPage />)}</PermissionRoute>, element: <PermissionRoute permission="settings:read">{withSuspense(<SettingsPage />)}</PermissionRoute>,
@@ -212,8 +228,6 @@ const router = createBrowserRouter([
{ path: '/mail', element: <PermissionRoute permission="mail:read">{withSuspense(<MailPage />)}</PermissionRoute> }, { path: '/mail', element: <PermissionRoute permission="mail:read">{withSuspense(<MailPage />)}</PermissionRoute> },
{ path: '/mail/settings', element: <PermissionRoute permission="mail:read">{withSuspense(<MailSettingsPage />)}</PermissionRoute> }, { path: '/mail/settings', element: <PermissionRoute permission="mail:read">{withSuspense(<MailSettingsPage />)}</PermissionRoute> },
{ path: '/ai-assistant', element: <PermissionRoute permission="ai:read">{withSuspense(<AIAssistantPage />)}</PermissionRoute> }, { path: '/ai-assistant', element: <PermissionRoute permission="ai:read">{withSuspense(<AIAssistantPage />)}</PermissionRoute> },
{ path: '/automation', element: <PermissionRoute permission="automation:read">{withSuspense(<AutomationDashboardPage />)}</PermissionRoute> },
{ path: '/agents', element: <PermissionRoute permission="automation:read">{withSuspense(<AgentDashboardPage />)}</PermissionRoute> },
{ path: '/reports', element: <PermissionRoute permission="reports:read">{withSuspense(<ReportsPage />)}</PermissionRoute> }, { path: '/reports', element: <PermissionRoute permission="reports:read">{withSuspense(<ReportsPage />)}</PermissionRoute> },
{ path: '/tasks', element: <PermissionRoute permission="tasks:read">{withSuspense(<TasksPage />)}</PermissionRoute> }, { path: '/tasks', element: <PermissionRoute permission="tasks:read">{withSuspense(<TasksPage />)}</PermissionRoute> },
{ path: '/communication', element: <PermissionRoute permission="communication:read">{withSuspense(<CommunicationPage />)}</PermissionRoute> }, { path: '/communication', element: <PermissionRoute permission="communication:read">{withSuspense(<CommunicationPage />)}</PermissionRoute> },