0b416b0f13
- Add agent orchestrator API (/api/v1/agents/*) - Connect frontend AgentContext to real backend - Add AgentLayerPage with full agent management - Implement specialist agents: finance, sales, hr, crm, legal, marketing, dashboard - Add authentication, RBAC, tenant isolation on agent endpoints - Add rate limiting and audit logging - Update sidebar with Agent Layer navigation - Build fresh web-v2 dist
158 lines
5.1 KiB
TypeScript
158 lines
5.1 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { motion } from 'framer-motion'
|
|
import {
|
|
Bot,
|
|
Users,
|
|
TrendingUp,
|
|
Megaphone,
|
|
Share2,
|
|
Wallet,
|
|
Calculator,
|
|
Briefcase,
|
|
FileText,
|
|
ShieldCheck,
|
|
HeadphonesIcon,
|
|
FolderKanban,
|
|
Zap,
|
|
Sparkles,
|
|
Activity,
|
|
CheckCircle2,
|
|
AlertCircle,
|
|
XCircle
|
|
} from 'lucide-react'
|
|
import { getAllAgents, AgentContext } from '@/components/agent/AgentContext'
|
|
import { AgentChat } from '@/components/agent/AgentChat'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const agentIcons: Record<string, React.ElementType> = {
|
|
crm: Users,
|
|
sales: TrendingUp,
|
|
marketing: Megaphone,
|
|
social: Share2,
|
|
finance: Wallet,
|
|
accounting: Calculator,
|
|
hr: Briefcase,
|
|
legal: FileText,
|
|
compliance: ShieldCheck,
|
|
support: HeadphonesIcon,
|
|
projects: FolderKanban,
|
|
automation: Zap,
|
|
dashboard: Sparkles
|
|
};
|
|
|
|
const statusConfig = {
|
|
operational: { icon: CheckCircle2, color: 'text-green-500', bg: 'bg-green-500/10', label: 'Operational' },
|
|
degraded: { icon: AlertCircle, color: 'text-yellow-500', bg: 'bg-yellow-500/10', label: 'Degraded' },
|
|
error: { icon: XCircle, color: 'text-red-500', bg: 'bg-red-500/10', label: 'Error' },
|
|
disabled: { icon: XCircle, color: 'text-gray-400', bg: 'bg-gray-400/10', label: 'Disabled' }
|
|
};
|
|
|
|
export function AgentLayerPage() {
|
|
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
|
|
const [agents, setAgents] = useState<AgentContext[]>([]);
|
|
const [chatOpen, setChatOpen] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setAgents(getAllAgents());
|
|
}, []);
|
|
|
|
const handleAgentClick = (rum: string) => {
|
|
setSelectedAgent(rum);
|
|
setChatOpen(true);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-text-primary">BOC Agent Layer</h1>
|
|
<p className="text-text-secondary mt-1">
|
|
Professionellt agentlager för hela organisationen
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center gap-2 px-4 py-2 bg-green-500/10 rounded-lg">
|
|
<Activity size={16} className="text-green-500" />
|
|
<span className="text-sm font-medium text-green-600">
|
|
{agents.filter(a => a.status === 'operational').length} / {agents.length} Operational
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Agent Grid */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
|
{agents.map((agent, index) => {
|
|
const Icon = agentIcons[agent.rum] || Bot;
|
|
const status = statusConfig[agent.status];
|
|
const StatusIcon = status.icon;
|
|
|
|
return (
|
|
<motion.div
|
|
key={agent.rum}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ delay: index * 0.05 }}
|
|
onClick={() => handleAgentClick(agent.rum)}
|
|
className={cn(
|
|
'group relative p-5 rounded-2xl border border-border bg-surface',
|
|
'hover:border-primary/30 hover:shadow-lg hover:shadow-primary/5',
|
|
'cursor-pointer transition-all duration-200'
|
|
)}
|
|
>
|
|
{/* Status Indicator */}
|
|
<div className="absolute top-4 right-4">
|
|
<div className={cn('flex items-center gap-1.5 px-2 py-1 rounded-full', status.bg)}>
|
|
<StatusIcon size={12} className={status.color} />
|
|
<span className={cn('text-xs font-medium', status.color)}>
|
|
{status.label}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Icon */}
|
|
<div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
|
<Icon size={24} className="text-primary" />
|
|
</div>
|
|
|
|
{/* Info */}
|
|
<h3 className="font-semibold text-text-primary mb-1">{agent.titel}</h3>
|
|
<p className="text-sm text-text-secondary mb-3 line-clamp-2">
|
|
{agent.kompetenser.slice(0, 3).join(', ')}
|
|
</p>
|
|
|
|
{/* Capabilities */}
|
|
<div className="flex flex-wrap gap-1.5">
|
|
{agent.capabilities.map(cap => (
|
|
<span
|
|
key={cap}
|
|
className="text-xs px-2 py-0.5 rounded-md bg-bg text-text-secondary"
|
|
>
|
|
{cap}
|
|
</span>
|
|
))}
|
|
</div>
|
|
|
|
{/* Hover Action */}
|
|
<div className="mt-4 pt-4 border-t border-border opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<div className="flex items-center gap-2 text-sm text-primary">
|
|
<Sparkles size={14} />
|
|
<span>Öppna agent</span>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Agent Chat */}
|
|
{selectedAgent && (
|
|
<AgentChat
|
|
rum={selectedAgent}
|
|
isOpen={chatOpen}
|
|
onToggle={() => setChatOpen(!chatOpen)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|