updates on the agentstudio
This commit is contained in:
@@ -2,16 +2,13 @@ import React, { useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
Sparkles,
|
Sparkles,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
|
||||||
LayoutGrid,
|
|
||||||
Zap,
|
Zap,
|
||||||
Wrench,
|
Wrench,
|
||||||
Pencil,
|
|
||||||
Users,
|
|
||||||
Package,
|
|
||||||
Route,
|
|
||||||
Layers,
|
|
||||||
Check,
|
Check,
|
||||||
|
Bot,
|
||||||
|
LayoutGrid,
|
||||||
|
Shield,
|
||||||
|
Activity,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -23,15 +20,15 @@ const STATUS_TONE = {
|
|||||||
indigo: 'bg-indigo-50 text-indigo-700 border-indigo-200',
|
indigo: 'bg-indigo-50 text-indigo-700 border-indigo-200',
|
||||||
};
|
};
|
||||||
|
|
||||||
// The registry's word for what an agent actually does today. Shown beside
|
|
||||||
// every agent, because a simulated agent that looks live is the failure the
|
|
||||||
// registry exists to end.
|
|
||||||
function StatusBadge({ agent }) {
|
function StatusBadge({ agent }) {
|
||||||
const meta = agent.statusMeta;
|
const meta = agent.statusMeta || { label: agent.status || 'Live', tone: 'emerald', hint: '' };
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
title={meta.hint}
|
title={meta.hint}
|
||||||
className={cn('shrink-0 rounded-full border px-1.5 py-px text-[9.5px] font-bold', STATUS_TONE[meta.tone] || STATUS_TONE.slate)}
|
className={cn(
|
||||||
|
'shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-bold tracking-wide',
|
||||||
|
STATUS_TONE[meta.tone] || STATUS_TONE.slate
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{meta.label}
|
{meta.label}
|
||||||
</span>
|
</span>
|
||||||
@@ -48,279 +45,175 @@ export default function AgentScopeNavigator({
|
|||||||
onSelectSkill,
|
onSelectSkill,
|
||||||
onSelectTool,
|
onSelectTool,
|
||||||
}) {
|
}) {
|
||||||
const [surfacesOpen, setSurfacesOpen] = useState(false);
|
|
||||||
const [skillsOpen, setSkillsOpen] = useState(true);
|
|
||||||
const [agentSkillsOpen, setAgentSkillsOpen] = useState(true);
|
|
||||||
const [boardOpen, setBoardOpen] = useState(true);
|
|
||||||
const [toolsOpen, setToolsOpen] = useState(true);
|
|
||||||
const [agentMenuOpen, setAgentMenuOpen] = useState(false);
|
const [agentMenuOpen, setAgentMenuOpen] = useState(false);
|
||||||
|
|
||||||
const currentAgent = agents.find((a) => a.id === activeAgentId) || agents[0];
|
const currentAgent = agents.find((a) => a.id === activeAgentId) || agents[0];
|
||||||
if (!currentAgent) return null;
|
if (!currentAgent) return null;
|
||||||
const agentSkills = skills.filter((s) => s.scope !== 'board');
|
|
||||||
const boardSkills = skills.filter((s) => s.scope === 'board');
|
const activeSkillsCount = skills.filter((s) => s.enabled !== false).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-4 flex flex-col space-y-4">
|
<div className="bg-white rounded-2xl border border-slate-200 shadow-2xs p-4 flex flex-col space-y-4">
|
||||||
{/* 1. Agent Profile Header + Dropdown Switcher */}
|
{/* 1. Agent Switcher */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
<label className="block text-[10.5px] font-bold uppercase tracking-wider text-slate-400 mb-1.5 px-0.5">
|
||||||
|
Active Agent
|
||||||
|
</label>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAgentMenuOpen((v) => !v)}
|
onClick={() => setAgentMenuOpen((v) => !v)}
|
||||||
className="w-full flex items-center justify-between p-2 rounded-xl hover:bg-slate-50 transition-colors text-left cursor-pointer group"
|
className="w-full flex items-center justify-between p-2.5 rounded-xl border border-slate-200 hover:border-slate-300 hover:bg-slate-50/80 transition-all text-left cursor-pointer group"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-3 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className="w-9 h-9 rounded-full bg-brand-tint border border-brand/20 text-brand flex items-center justify-center shrink-0 shadow-2xs">
|
<div className="w-10 h-10 rounded-xl bg-brand/10 border border-brand/20 text-brand flex items-center justify-center shrink-0">
|
||||||
<Sparkles className="w-4 h-4" />
|
<Bot className="w-5 h-5 text-brand" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-xs sm:text-sm font-bold text-slate-900 truncate font-heading group-hover:text-brand transition-colors">
|
<div className="text-sm font-bold text-slate-900 truncate group-hover:text-brand transition-colors">
|
||||||
{currentAgent.name}
|
{currentAgent.name}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
<div className="flex items-center gap-1.5 mt-0.5">
|
||||||
<span>{currentAgent.badge || 'AGENT'}</span>
|
|
||||||
<StatusBadge agent={currentAgent} />
|
<StatusBadge agent={currentAgent} />
|
||||||
|
<span className="text-[10px] font-medium text-slate-400 truncate">
|
||||||
|
{currentAgent.runtime === 'engine' ? 'AI Engine' : 'Console'}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ChevronDown className="w-4 h-4 text-slate-400 shrink-0 ml-1 group-hover:text-slate-600 transition-colors" />
|
<ChevronDown
|
||||||
|
className={cn(
|
||||||
|
'w-4 h-4 text-slate-400 shrink-0 ml-1 transition-transform',
|
||||||
|
agentMenuOpen && 'rotate-180 text-brand'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Agent Switcher Menu */}
|
{/* Dropdown Menu */}
|
||||||
{agentMenuOpen && (
|
{agentMenuOpen && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-40 bg-transparent"
|
className="fixed inset-0 z-40 bg-transparent"
|
||||||
onClick={() => setAgentMenuOpen(false)}
|
onClick={() => setAgentMenuOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="absolute left-0 right-0 top-full mt-1.5 bg-white border border-slate-200 rounded-xl shadow-lg p-1.5 z-50 animate-in fade-in zoom-in-95">
|
<div className="absolute left-0 right-0 top-full mt-1.5 bg-white border border-slate-200 rounded-xl shadow-lg p-1.5 z-50 animate-in fade-in zoom-in-95 max-h-72 overflow-y-auto">
|
||||||
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider px-3 py-1.5">
|
<div className="text-[10px] font-bold text-slate-400 uppercase tracking-wider px-3 py-1.5">
|
||||||
Switch Agent Scope
|
Select Agent
|
||||||
</div>
|
</div>
|
||||||
{agents.map((agent) => (
|
{agents.map((agent) => {
|
||||||
<button
|
const isSelected = agent.id === activeAgentId;
|
||||||
key={agent.id}
|
return (
|
||||||
type="button"
|
<button
|
||||||
onClick={() => {
|
key={agent.id}
|
||||||
onSelectAgent(agent.id);
|
type="button"
|
||||||
setAgentMenuOpen(false);
|
onClick={() => {
|
||||||
}}
|
onSelectAgent(agent.id);
|
||||||
className={cn(
|
setAgentMenuOpen(false);
|
||||||
'w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs font-semibold text-left transition-colors cursor-pointer',
|
}}
|
||||||
agent.id === activeAgentId
|
className={cn(
|
||||||
? 'bg-brand-tint text-brand'
|
'w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs font-semibold text-left transition-colors cursor-pointer',
|
||||||
: 'text-slate-700 hover:bg-slate-50'
|
isSelected
|
||||||
)}
|
? 'bg-brand/10 text-brand font-bold'
|
||||||
>
|
: 'text-slate-700 hover:bg-slate-50'
|
||||||
<span className="flex min-w-0 items-center gap-1.5">
|
)}
|
||||||
<span className="truncate">{agent.name}</span>
|
>
|
||||||
<StatusBadge agent={agent} />
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
</span>
|
<span className="truncate">{agent.name}</span>
|
||||||
{agent.id === activeAgentId && <Check className="w-3.5 h-3.5 shrink-0" />}
|
<StatusBadge agent={agent} />
|
||||||
</button>
|
</div>
|
||||||
))}
|
{isSelected && <Check className="w-3.5 h-3.5 shrink-0 text-brand ml-2" />}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-px bg-slate-100" />
|
{/* 2. Agent Description */}
|
||||||
|
{currentAgent.description && (
|
||||||
|
<div className="p-3 rounded-xl bg-slate-50 border border-slate-100 text-xs text-slate-600 leading-relaxed">
|
||||||
|
{currentAgent.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 2. Hierarchical Tree (Clean, un-cluttered style) */}
|
{/* 3. Summary Stats */}
|
||||||
<div className="space-y-2 text-xs">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{/* SURFACES TREE NODE */}
|
<div className="p-3 rounded-xl border border-slate-100 bg-slate-50/60 flex flex-col">
|
||||||
<div>
|
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
||||||
<button
|
Active Skills
|
||||||
type="button"
|
</span>
|
||||||
onClick={() => setSurfacesOpen((v) => !v)}
|
<div className="flex items-baseline gap-1.5 mt-1">
|
||||||
className="w-full flex items-center justify-between py-1.5 px-2 text-slate-700 hover:text-slate-900 hover:bg-slate-50 rounded-lg font-medium cursor-pointer transition-colors"
|
<span className="text-xl font-bold text-slate-900">{activeSkillsCount}</span>
|
||||||
>
|
<span className="text-xs text-slate-400">/ {skills.length}</span>
|
||||||
<div className="flex items-center gap-2">
|
</div>
|
||||||
{surfacesOpen ? (
|
|
||||||
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
)}
|
|
||||||
<LayoutGrid className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
<span className="font-semibold text-slate-800">Surfaces</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] font-semibold text-slate-400">
|
|
||||||
{surfaces.length || 1}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{surfacesOpen && (
|
|
||||||
<div className="pl-7 pr-1 mt-1 space-y-0.5">
|
|
||||||
{surfaces.map((surf) => (
|
|
||||||
<div
|
|
||||||
key={surf.id}
|
|
||||||
className="py-1 px-2 rounded-md text-slate-600 hover:text-slate-900 hover:bg-slate-50 text-[11px] truncate cursor-pointer"
|
|
||||||
title={surf.description}
|
|
||||||
>
|
|
||||||
{surf.name}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* SKILLS TREE NODE */}
|
<div className="p-3 rounded-xl border border-slate-100 bg-slate-50/60 flex flex-col">
|
||||||
<div>
|
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
|
||||||
<button
|
Linked Tools
|
||||||
type="button"
|
</span>
|
||||||
onClick={() => setSkillsOpen((v) => !v)}
|
<div className="flex items-baseline gap-1.5 mt-1">
|
||||||
className="w-full flex items-center justify-between py-1.5 px-2 text-slate-700 hover:text-slate-900 hover:bg-slate-50 rounded-lg font-medium cursor-pointer transition-colors"
|
<span className="text-xl font-bold text-slate-900">{tools.length}</span>
|
||||||
>
|
<span className="text-xs text-slate-400">ready</span>
|
||||||
<div className="flex items-center gap-2">
|
</div>
|
||||||
{skillsOpen ? (
|
|
||||||
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
)}
|
|
||||||
<Zap className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
<span className="font-semibold text-slate-800">Skills</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] font-semibold text-slate-400">
|
|
||||||
{skills.length}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{skillsOpen && (
|
|
||||||
<div className="pl-4 mt-1 space-y-1">
|
|
||||||
{/* Agent Skills Sub-node */}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setAgentSkillsOpen((v) => !v)}
|
|
||||||
className="w-full flex items-center justify-between py-1 px-2 text-[11px] font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-md cursor-pointer transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{agentSkillsOpen ? (
|
|
||||||
<ChevronDown className="w-3 h-3 text-slate-400" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-3 h-3 text-slate-400" />
|
|
||||||
)}
|
|
||||||
<Zap className="w-3 h-3 text-brand" />
|
|
||||||
<span className="font-medium text-slate-700">Agent Skills</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] text-slate-400 font-semibold">
|
|
||||||
{agentSkills.length}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{agentSkillsOpen && (
|
|
||||||
<div className="pl-6 pr-1 mt-0.5 space-y-0.5">
|
|
||||||
{agentSkills.map((skill) => {
|
|
||||||
const getSkillIcon = (cat) => {
|
|
||||||
switch ((cat || '').toUpperCase()) {
|
|
||||||
case 'AUTHORING':
|
|
||||||
return Pencil;
|
|
||||||
case 'WORKFORCE':
|
|
||||||
return Users;
|
|
||||||
case 'LOGISTICS':
|
|
||||||
return Package;
|
|
||||||
case 'DISPATCH':
|
|
||||||
return Route;
|
|
||||||
default:
|
|
||||||
return Zap;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const SkillIcon = getSkillIcon(skill.category);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={skill.id}
|
|
||||||
type="button"
|
|
||||||
onClick={() => onSelectSkill(skill)}
|
|
||||||
className="w-full text-left py-1.5 px-2 rounded-md text-slate-600 hover:text-brand hover:bg-brand-tint/60 text-[11px] flex items-center gap-2 truncate cursor-pointer transition-colors group"
|
|
||||||
>
|
|
||||||
<SkillIcon className="w-3 h-3 shrink-0 text-slate-400 group-hover:text-brand transition-colors" />
|
|
||||||
<span className="truncate">{skill.title}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Board Skills Sub-node */}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setBoardOpen((v) => !v)}
|
|
||||||
className="w-full flex items-center justify-between py-1 px-2 text-[11px] font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-md cursor-pointer transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1.5">
|
|
||||||
{boardOpen ? (
|
|
||||||
<ChevronDown className="w-3 h-3 text-slate-400" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-3 h-3 text-slate-400" />
|
|
||||||
)}
|
|
||||||
<Layers className="w-3 h-3 text-slate-400" />
|
|
||||||
<span className="font-medium text-slate-700">Board Skills</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[10px] text-slate-400 font-semibold">
|
|
||||||
{boardSkills.length}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{boardOpen && (
|
|
||||||
<div className="pl-7 pr-2 mt-0.5">
|
|
||||||
<p className="text-[10px] text-slate-400 leading-snug py-1">
|
|
||||||
None drawing on this agent's surfaces.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* TOOLS TREE NODE */}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setToolsOpen((v) => !v)}
|
|
||||||
className="w-full flex items-center justify-between py-1.5 px-2 text-slate-700 hover:text-slate-900 hover:bg-slate-50 rounded-lg font-medium cursor-pointer transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{toolsOpen ? (
|
|
||||||
<ChevronDown className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
) : (
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
)}
|
|
||||||
<Wrench className="w-3.5 h-3.5 text-slate-400" />
|
|
||||||
<span className="font-semibold text-slate-800">Tools</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-[11px] font-semibold text-slate-400">
|
|
||||||
{tools.length}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{toolsOpen && (
|
|
||||||
<div className="pl-6 pr-1 mt-1 space-y-0.5">
|
|
||||||
{tools.map((tool) => (
|
|
||||||
<button
|
|
||||||
key={tool.name}
|
|
||||||
type="button"
|
|
||||||
onClick={onSelectTool}
|
|
||||||
className="w-full text-left py-1 px-2 rounded-md hover:bg-slate-50 text-[11px] group cursor-pointer transition-colors block"
|
|
||||||
>
|
|
||||||
<div className="flex items-center gap-1.5 text-slate-700 group-hover:text-brand truncate font-mono text-[10.5px]">
|
|
||||||
<Wrench className="w-3 h-3 shrink-0 text-slate-400 group-hover:text-brand" />
|
|
||||||
<span className="truncate">{tool.name}</span>
|
|
||||||
</div>
|
|
||||||
{tool.parentSkill && (
|
|
||||||
<div className="text-[9.5px] text-slate-400 pl-4 truncate font-sans">
|
|
||||||
{tool.parentSkill}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 4. Quick Shortcuts */}
|
||||||
|
<div className="space-y-1.5 pt-1 border-t border-slate-100">
|
||||||
|
<div className="text-[10.5px] font-bold uppercase tracking-wider text-slate-400 mb-1 px-1">
|
||||||
|
Agent Capabilities
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelectTool && onSelectTool()}
|
||||||
|
className="w-full flex items-center justify-between p-2 rounded-xl text-xs font-medium text-slate-700 hover:text-slate-900 hover:bg-slate-50 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Zap className="w-3.5 h-3.5 text-brand" />
|
||||||
|
<span>Configured Skills</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-semibold bg-slate-100 px-2 py-0.5 rounded-full text-slate-600">
|
||||||
|
{skills.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onSelectTool && onSelectTool()}
|
||||||
|
className="w-full flex items-center justify-between p-2 rounded-xl text-xs font-medium text-slate-700 hover:text-slate-900 hover:bg-slate-50 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Wrench className="w-3.5 h-3.5 text-slate-500" />
|
||||||
|
<span>Available Tools</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-semibold bg-slate-100 px-2 py-0.5 rounded-full text-slate-600">
|
||||||
|
{tools.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 5. Surfaces / Touchpoints */}
|
||||||
|
{surfaces && surfaces.length > 0 && (
|
||||||
|
<div className="space-y-1.5 pt-2 border-t border-slate-100">
|
||||||
|
<div className="flex items-center gap-1.5 text-[10.5px] font-bold uppercase tracking-wider text-slate-400 px-1">
|
||||||
|
<LayoutGrid className="w-3 h-3 text-slate-400" />
|
||||||
|
<span>Active Surfaces</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5 pt-0.5">
|
||||||
|
{surfaces.map((surf) => (
|
||||||
|
<span
|
||||||
|
key={surf.id}
|
||||||
|
className="text-[11px] font-medium text-slate-600 bg-slate-100 px-2 py-1 rounded-lg"
|
||||||
|
title={surf.description}
|
||||||
|
>
|
||||||
|
{surf.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,13 +219,21 @@ export default function AgentStudio() {
|
|||||||
activeAgent={activeAgent}
|
activeAgent={activeAgent}
|
||||||
onTestSkill={handleTestSkill}
|
onTestSkill={handleTestSkill}
|
||||||
onOpenDetails={(skill) => setDrawerSkillId(skill.id)}
|
onOpenDetails={(skill) => setDrawerSkillId(skill.id)}
|
||||||
|
onToggleStatus={handleToggleStatus}
|
||||||
|
canEdit={canEdit}
|
||||||
onNewSkill={() => setCreateModalOpen(true)}
|
onNewSkill={() => setCreateModalOpen(true)}
|
||||||
canCreate={canCreate}
|
canCreate={canCreate}
|
||||||
createDisabledReason={createDisabledReason}
|
createDisabledReason={createDisabledReason}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'tools' && <ToolRegistryView tools={model.tools} />}
|
{activeTab === 'tools' && (
|
||||||
|
<ToolRegistryView
|
||||||
|
tools={model.tools}
|
||||||
|
agentTools={agentTools}
|
||||||
|
activeAgent={activeAgent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeTab === 'configure' && (
|
{activeTab === 'configure' && (
|
||||||
<AgentConfigView
|
<AgentConfigView
|
||||||
|
|||||||
@@ -1,181 +1,142 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {
|
import {
|
||||||
Sparkles,
|
|
||||||
Play,
|
Play,
|
||||||
ArrowRight,
|
Settings2,
|
||||||
Pencil,
|
Wrench,
|
||||||
Users,
|
|
||||||
Package,
|
|
||||||
Route,
|
|
||||||
Check,
|
|
||||||
Layers,
|
Layers,
|
||||||
CircleDot,
|
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const CATEGORY_META = {
|
const CATEGORY_STYLES = {
|
||||||
AUTHORING: {
|
AUTHORING: 'bg-purple-50 text-purple-700 border-purple-200',
|
||||||
icon: Pencil,
|
WORKFORCE: 'bg-blue-50 text-blue-700 border-blue-200',
|
||||||
gradient: 'from-violet-500 to-purple-600',
|
LOGISTICS: 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
||||||
bgGlow: 'bg-violet-50',
|
DISPATCH: 'bg-amber-50 text-amber-700 border-amber-200',
|
||||||
badgeBg: 'bg-violet-100 text-violet-700 border-violet-200',
|
GENERAL: 'bg-slate-50 text-slate-700 border-slate-200',
|
||||||
accent: 'text-violet-600',
|
|
||||||
glow: 'hover:shadow-violet-100',
|
|
||||||
},
|
|
||||||
WORKFORCE: {
|
|
||||||
icon: Users,
|
|
||||||
gradient: 'from-blue-500 to-indigo-600',
|
|
||||||
bgGlow: 'bg-blue-50',
|
|
||||||
badgeBg: 'bg-blue-100 text-blue-700 border-blue-200',
|
|
||||||
accent: 'text-blue-600',
|
|
||||||
glow: 'hover:shadow-blue-100',
|
|
||||||
},
|
|
||||||
LOGISTICS: {
|
|
||||||
icon: Package,
|
|
||||||
gradient: 'from-emerald-500 to-teal-600',
|
|
||||||
bgGlow: 'bg-emerald-50',
|
|
||||||
badgeBg: 'bg-emerald-100 text-emerald-700 border-emerald-200',
|
|
||||||
accent: 'text-emerald-600',
|
|
||||||
glow: 'hover:shadow-emerald-100',
|
|
||||||
},
|
|
||||||
DISPATCH: {
|
|
||||||
icon: Route,
|
|
||||||
gradient: 'from-orange-500 to-rose-500',
|
|
||||||
bgGlow: 'bg-orange-50',
|
|
||||||
badgeBg: 'bg-orange-100 text-orange-700 border-orange-200',
|
|
||||||
accent: 'text-orange-600',
|
|
||||||
glow: 'hover:shadow-orange-100',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_META = {
|
export default function SkillCard({
|
||||||
icon: Layers,
|
skill,
|
||||||
gradient: 'from-slate-500 to-slate-600',
|
onTest,
|
||||||
bgGlow: 'bg-slate-50',
|
onDetails,
|
||||||
badgeBg: 'bg-slate-100 text-slate-600 border-slate-200',
|
onToggleStatus,
|
||||||
accent: 'text-slate-600',
|
canEdit = true,
|
||||||
glow: 'hover:shadow-slate-100',
|
}) {
|
||||||
};
|
|
||||||
|
|
||||||
export default function SkillCard({ skill, onTest, onDetails }) {
|
|
||||||
const isEnabled = skill.enabled !== false;
|
const isEnabled = skill.enabled !== false;
|
||||||
const meta = CATEGORY_META[(skill.category || '').toUpperCase()] || DEFAULT_META;
|
const categoryKey = (skill.category || 'GENERAL').toUpperCase();
|
||||||
const CategoryIcon = meta.icon;
|
const categoryBadgeStyle = CATEGORY_STYLES[categoryKey] || CATEGORY_STYLES.GENERAL;
|
||||||
|
const toolsCount = Array.isArray(skill.tools) ? skill.tools.length : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative bg-white rounded-2xl border border-slate-200/80 shadow-sm transition-all duration-300 overflow-hidden cursor-pointer group',
|
'bg-white rounded-2xl border transition-all duration-200 flex flex-col justify-between p-5 shadow-2xs hover:shadow-md hover:border-slate-300',
|
||||||
'hover:shadow-xl hover:shadow-slate-200/60 hover:-translate-y-0.5 hover:border-slate-300',
|
isEnabled ? 'border-slate-200' : 'border-slate-200/60 bg-slate-50/40 opacity-80'
|
||||||
meta.glow
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Top gradient accent bar */}
|
<div>
|
||||||
<div className={cn('h-1 w-full bg-gradient-to-r', meta.gradient)} />
|
{/* Top Header: Category Tag & Direct Status Toggle Switch */}
|
||||||
|
<div className="flex items-center justify-between gap-2 mb-3">
|
||||||
<div className="p-5">
|
<span
|
||||||
{/* Header row: category badge + status */}
|
|
||||||
<div className="flex items-center justify-between gap-2 mb-4">
|
|
||||||
<div
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-bold tracking-wider border',
|
'px-2.5 py-0.5 rounded-lg text-[10px] font-bold uppercase tracking-wider border',
|
||||||
meta.badgeBg
|
categoryBadgeStyle
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<CategoryIcon className="w-3 h-3" />
|
{categoryKey.replace(/_/g, ' ')}
|
||||||
<span>{(skill.category || 'SKILL').toUpperCase().replace(/_/g, ' ')}</span>
|
</span>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
{/* User-friendly Direct On/Off Toggle */}
|
||||||
className={cn(
|
<div className="flex items-center gap-2">
|
||||||
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-bold border transition-colors',
|
<span
|
||||||
isEnabled
|
className={cn(
|
||||||
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
'text-[11px] font-semibold transition-colors',
|
||||||
: 'bg-slate-100 text-slate-500 border-slate-200'
|
isEnabled ? 'text-emerald-700' : 'text-slate-400'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isEnabled ? (
|
{isEnabled ? 'Active' : 'Disabled'}
|
||||||
<>
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
|
||||||
<span>Active</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<CircleDot className="w-3 h-3" />
|
|
||||||
<span>Disabled</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Icon + Title */}
|
|
||||||
<div className="flex items-start gap-3 mb-3">
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
'w-10 h-10 rounded-xl flex items-center justify-center shrink-0 bg-gradient-to-br shadow-sm',
|
|
||||||
meta.gradient
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<CategoryIcon className="w-5 h-5 text-white" />
|
|
||||||
</div>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<h3 className="text-sm font-bold text-slate-900 font-heading leading-tight tracking-tight">
|
|
||||||
{skill.title}
|
|
||||||
</h3>
|
|
||||||
<p className="text-[11px] text-slate-500 mt-0.5 leading-relaxed line-clamp-2">
|
|
||||||
{skill.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Prompt pill */}
|
|
||||||
{skill.prompt && (
|
|
||||||
<div className="mb-3 px-3 py-2 rounded-xl bg-slate-50 border border-slate-200/80 flex items-start gap-2">
|
|
||||||
<Sparkles className={cn('w-3 h-3 shrink-0 mt-0.5', meta.accent)} />
|
|
||||||
<span className={cn('text-[10.5px] italic truncate font-medium', meta.accent)}>
|
|
||||||
“{skill.prompt}”
|
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={isEnabled}
|
||||||
|
disabled={!canEdit}
|
||||||
|
title={canEdit ? (isEnabled ? 'Click to disable' : 'Click to enable') : 'Admin access required'}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (onToggleStatus && canEdit) {
|
||||||
|
onToggleStatus(skill.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
isEnabled ? 'bg-emerald-500' : 'bg-slate-300'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out',
|
||||||
|
isEnabled ? 'translate-x-4' : 'translate-x-0'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
|
|
||||||
{/* Integration tag */}
|
{/* Skill Title & Plain English Description */}
|
||||||
{skill.integration && (
|
<div className="mb-3">
|
||||||
<div className="mb-3">
|
<h3 className="text-sm font-bold text-slate-900 leading-snug">
|
||||||
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-slate-50 border border-slate-200 text-[10px] font-semibold text-slate-500">
|
{skill.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-xs text-slate-500 mt-1 leading-relaxed line-clamp-2">
|
||||||
|
{skill.description || 'No description provided for this skill.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Linked Tools Counter & Integration info */}
|
||||||
|
<div className="flex items-center gap-2 flex-wrap mb-4">
|
||||||
|
{toolsCount > 0 ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-slate-100 text-slate-600 text-[11px] font-medium">
|
||||||
|
<Wrench className="w-3 h-3 text-slate-400" />
|
||||||
|
<span>{toolsCount} {toolsCount === 1 ? 'tool linked' : 'tools linked'}</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1.5 px-2 py-1 rounded-md bg-slate-50 text-slate-400 text-[11px]">
|
||||||
|
No external tools
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{skill.scope === 'board' && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-md bg-indigo-50 border border-indigo-100 text-indigo-700 text-[10.5px] font-semibold">
|
||||||
<Layers className="w-3 h-3" />
|
<Layers className="w-3 h-3" />
|
||||||
{skill.integration}
|
Shared Board Skill
|
||||||
</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<div className="flex items-center justify-between pt-3.5 mt-1 border-t border-slate-100">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onTest(skill)}
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center gap-1.5 text-[11px] font-bold transition-all cursor-pointer',
|
|
||||||
'px-3 py-1.5 rounded-lg bg-slate-50 hover:bg-slate-100 text-slate-600 hover:text-slate-900 border border-slate-200'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Play className="w-3 h-3 fill-current" />
|
|
||||||
<span>Test</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => onDetails(skill)}
|
|
||||||
className={cn(
|
|
||||||
'inline-flex items-center gap-1 text-[11px] font-semibold transition-all cursor-pointer group/btn',
|
|
||||||
meta.accent, 'hover:opacity-70'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>Details</span>
|
|
||||||
<ChevronRight className="w-3.5 h-3.5 group-hover/btn:translate-x-0.5 transition-transform" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Action Footer: Test and Configure */}
|
||||||
|
<div className="flex items-center justify-between pt-3 border-t border-slate-100 gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onTest && onTest(skill)}
|
||||||
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold text-slate-700 hover:text-slate-900 bg-slate-100 hover:bg-slate-200/80 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Play className="w-3 h-3 fill-current text-slate-600" />
|
||||||
|
<span>Test</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onDetails && onDetails(skill)}
|
||||||
|
className="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-semibold text-brand hover:bg-brand/10 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<Settings2 className="w-3.5 h-3.5 text-brand" />
|
||||||
|
<span>Configure</span>
|
||||||
|
<ChevronRight className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { Search, Plus, X, Layers, AlertCircle } from 'lucide-react';
|
import { Search, Plus, X, AlertCircle, Filter, Zap } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import SkillCard from './SkillCard';
|
import SkillCard from './SkillCard';
|
||||||
|
|
||||||
@@ -8,218 +8,190 @@ export default function SkillCardsGrid({
|
|||||||
activeAgent,
|
activeAgent,
|
||||||
onTestSkill,
|
onTestSkill,
|
||||||
onOpenDetails,
|
onOpenDetails,
|
||||||
|
onToggleStatus,
|
||||||
onNewSkill,
|
onNewSkill,
|
||||||
canCreate = true,
|
canCreate = true,
|
||||||
|
canEdit = true,
|
||||||
createDisabledReason = '',
|
createDisabledReason = '',
|
||||||
}) {
|
}) {
|
||||||
const [scopeTab, setScopeTab] = useState('agent'); // 'agent' or 'board'
|
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [categoryFilter, setCategoryFilter] = useState('ALL');
|
const [categoryFilter, setCategoryFilter] = useState('ALL');
|
||||||
|
const [statusFilter, setStatusFilter] = useState('ALL'); // 'ALL' | 'ACTIVE' | 'DISABLED'
|
||||||
|
const [scopeFilter, setScopeFilter] = useState('ALL'); // 'ALL' | 'AGENT' | 'BOARD'
|
||||||
|
|
||||||
// Filter by active agent and scope
|
const hasBoardSkills = skills.some((s) => s.scope === 'board');
|
||||||
const scopedSkills = useMemo(() => {
|
|
||||||
return skills.filter((s) => {
|
|
||||||
if (scopeTab === 'board') {
|
|
||||||
return s.scope === 'board';
|
|
||||||
}
|
|
||||||
return s.scope !== 'board';
|
|
||||||
});
|
|
||||||
}, [skills, scopeTab]);
|
|
||||||
|
|
||||||
// Categories present in the current scope
|
// Categories present in the skills
|
||||||
const categories = useMemo(() => {
|
const categories = useMemo(() => {
|
||||||
const counts = { ALL: scopedSkills.length };
|
const list = new Set();
|
||||||
scopedSkills.forEach((s) => {
|
skills.forEach((s) => {
|
||||||
const cat = (s.category || 'GENERAL').toUpperCase();
|
if (s.category) list.add(s.category.toUpperCase());
|
||||||
counts[cat] = (counts[cat] || 0) + 1;
|
|
||||||
});
|
});
|
||||||
return counts;
|
return Array.from(list);
|
||||||
}, [scopedSkills]);
|
}, [skills]);
|
||||||
|
|
||||||
// Filter by category and search query
|
// Filter skills
|
||||||
const filteredSkills = useMemo(() => {
|
const filteredSkills = useMemo(() => {
|
||||||
return scopedSkills.filter((s) => {
|
return skills.filter((s) => {
|
||||||
const matchesCat =
|
// Category filter
|
||||||
categoryFilter === 'ALL' ||
|
if (categoryFilter !== 'ALL' && (s.category || '').toUpperCase() !== categoryFilter) {
|
||||||
(s.category || '').toUpperCase() === categoryFilter;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status filter
|
||||||
|
const isEnabled = s.enabled !== false;
|
||||||
|
if (statusFilter === 'ACTIVE' && !isEnabled) return false;
|
||||||
|
if (statusFilter === 'DISABLED' && isEnabled) return false;
|
||||||
|
|
||||||
|
// Scope filter
|
||||||
|
if (scopeFilter === 'AGENT' && s.scope === 'board') return false;
|
||||||
|
if (scopeFilter === 'BOARD' && s.scope !== 'board') return false;
|
||||||
|
|
||||||
|
// Search query
|
||||||
const q = searchQuery.toLowerCase().trim();
|
const q = searchQuery.toLowerCase().trim();
|
||||||
const matchesQuery =
|
if (!q) return true;
|
||||||
!q ||
|
|
||||||
|
return (
|
||||||
(s.title || '').toLowerCase().includes(q) ||
|
(s.title || '').toLowerCase().includes(q) ||
|
||||||
(s.description || '').toLowerCase().includes(q) ||
|
(s.description || '').toLowerCase().includes(q) ||
|
||||||
(s.category || '').toLowerCase().includes(q) ||
|
(s.category || '').toLowerCase().includes(q) ||
|
||||||
(s.integration || '').toLowerCase().includes(q) ||
|
(s.integration || '').toLowerCase().includes(q) ||
|
||||||
(s.prompt || '').toLowerCase().includes(q);
|
(s.prompt || '').toLowerCase().includes(q)
|
||||||
|
);
|
||||||
return matchesCat && matchesQuery;
|
|
||||||
});
|
});
|
||||||
}, [scopedSkills, categoryFilter, searchQuery]);
|
}, [skills, categoryFilter, statusFilter, scopeFilter, searchQuery]);
|
||||||
|
|
||||||
const enabledCount = filteredSkills.filter((s) => s.enabled !== false).length;
|
const activeCount = skills.filter((s) => s.enabled !== false).length;
|
||||||
const agentCount = skills.filter((s) => s.scope !== 'board').length;
|
|
||||||
const boardCount = skills.filter((s) => s.scope === 'board').length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* 1. Scope Sub-tabs (Agent Skills / Board Skills) */}
|
{/* 1. Header Toolbar: Search + Filters + New Skill Button */}
|
||||||
<div className="flex items-center gap-8 border-b border-slate-200/80 text-xs sm:text-sm font-medium">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||||
<button
|
{/* Search Input */}
|
||||||
type="button"
|
<div className="relative flex-1">
|
||||||
onClick={() => {
|
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||||
setScopeTab('agent');
|
<input
|
||||||
setCategoryFilter('ALL');
|
type="text"
|
||||||
}}
|
value={searchQuery}
|
||||||
className={cn(
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
'pb-3 -mb-px transition-all cursor-pointer flex items-center gap-2 border-b-2 select-none',
|
placeholder="Search skills by name, description or role..."
|
||||||
scopeTab === 'agent'
|
className="w-full h-10 bg-white border border-slate-200 rounded-xl pl-10 pr-9 text-xs sm:text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:border-brand focus:ring-2 focus:ring-brand/10 shadow-2xs hover:border-slate-300 transition-all"
|
||||||
? 'border-brand text-brand font-bold'
|
/>
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300'
|
{searchQuery && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSearchQuery('')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-700 rounded-full hover:bg-slate-100 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
>
|
</div>
|
||||||
<span>Agent Skills</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'px-2 py-0.5 rounded-full text-[11px] font-bold transition-colors',
|
|
||||||
scopeTab === 'agent'
|
|
||||||
? 'bg-brand text-white'
|
|
||||||
: 'bg-slate-100 text-slate-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{agentCount}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
{/* Action Controls */}
|
||||||
type="button"
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
onClick={() => {
|
{/* Category Dropdown Filter */}
|
||||||
setScopeTab('board');
|
<div className="relative">
|
||||||
setCategoryFilter('ALL');
|
<select
|
||||||
}}
|
value={categoryFilter}
|
||||||
className={cn(
|
onChange={(e) => setCategoryFilter(e.target.value)}
|
||||||
'pb-3 -mb-px transition-all cursor-pointer flex items-center gap-2 border-b-2 select-none',
|
className="h-10 px-3 pr-8 rounded-xl bg-white border border-slate-200 text-xs font-medium text-slate-700 hover:border-slate-300 focus:outline-none focus:border-brand shadow-2xs cursor-pointer appearance-none"
|
||||||
scopeTab === 'board'
|
>
|
||||||
? 'border-brand text-brand font-bold'
|
<option value="ALL">All Categories</option>
|
||||||
: 'border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300'
|
{categories.map((cat) => (
|
||||||
|
<option key={cat} value={cat}>
|
||||||
|
{cat.charAt(0) + cat.slice(1).toLowerCase().replace(/_/g, ' ')}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<Filter className="w-3.5 h-3.5 text-slate-400 absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Dropdown Filter */}
|
||||||
|
<select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
|
className="h-10 px-3 rounded-xl bg-white border border-slate-200 text-xs font-medium text-slate-700 hover:border-slate-300 focus:outline-none focus:border-brand shadow-2xs cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="ALL">All Status</option>
|
||||||
|
<option value="ACTIVE">Active Only</option>
|
||||||
|
<option value="DISABLED">Disabled Only</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Scope Filter (if board skills exist) */}
|
||||||
|
{hasBoardSkills && (
|
||||||
|
<select
|
||||||
|
value={scopeFilter}
|
||||||
|
onChange={(e) => setScopeFilter(e.target.value)}
|
||||||
|
className="h-10 px-3 rounded-xl bg-white border border-slate-200 text-xs font-medium text-slate-700 hover:border-slate-300 focus:outline-none focus:border-brand shadow-2xs cursor-pointer"
|
||||||
|
>
|
||||||
|
<option value="ALL">All Scopes</option>
|
||||||
|
<option value="AGENT">Agent Skills</option>
|
||||||
|
<option value="BOARD">Board Skills</option>
|
||||||
|
</select>
|
||||||
)}
|
)}
|
||||||
>
|
|
||||||
<span>Board Skills</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'px-2 py-0.5 rounded-full text-[11px] font-bold transition-colors',
|
|
||||||
scopeTab === 'board'
|
|
||||||
? 'bg-brand text-white'
|
|
||||||
: 'bg-slate-100 text-slate-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{boardCount}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 2. Global Search Input */}
|
{/* New Skill Button */}
|
||||||
<div className="relative">
|
|
||||||
<Search className="w-4 h-4 text-slate-400 absolute left-4 top-1/2 -translate-y-1/2 pointer-events-none" />
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
|
||||||
placeholder="Search skills, capabilities or integrations..."
|
|
||||||
className="w-full h-11 bg-white border border-slate-200 rounded-2xl pl-11 pr-10 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:border-brand focus:ring-2 focus:ring-brand/10 shadow-sm hover:border-slate-300 transition-all"
|
|
||||||
/>
|
|
||||||
{searchQuery && (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSearchQuery('')}
|
onClick={onNewSkill}
|
||||||
className="absolute right-3.5 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-700 rounded-full hover:bg-slate-100 transition-colors cursor-pointer"
|
disabled={!canCreate}
|
||||||
|
title={canCreate ? undefined : createDisabledReason}
|
||||||
|
className="inline-flex items-center gap-1.5 h-10 px-4 rounded-xl bg-brand text-white font-semibold text-xs shadow-2xs hover:bg-brand/90 transition-all active:scale-95 cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
>
|
>
|
||||||
<X className="w-3.5 h-3.5" />
|
<Plus className="w-4 h-4 stroke-[2.5]" />
|
||||||
|
<span>New Skill</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2. Summary & Result Stats */}
|
||||||
|
<div className="flex items-center justify-between text-xs text-slate-500 px-0.5">
|
||||||
|
<div>
|
||||||
|
<span>Showing </span>
|
||||||
|
<span className="font-semibold text-slate-800">{filteredSkills.length}</span>
|
||||||
|
<span> of {skills.length} skills (</span>
|
||||||
|
<span className="font-semibold text-emerald-600">{activeCount} active</span>
|
||||||
|
<span>)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(categoryFilter !== 'ALL' || statusFilter !== 'ALL' || scopeFilter !== 'ALL' || searchQuery) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSearchQuery('');
|
||||||
|
setCategoryFilter('ALL');
|
||||||
|
setStatusFilter('ALL');
|
||||||
|
setScopeFilter('ALL');
|
||||||
|
}}
|
||||||
|
className="text-xs text-brand font-medium hover:underline cursor-pointer"
|
||||||
|
>
|
||||||
|
Reset filters
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. Category Filter Pills */}
|
{/* 3. Skill Cards Grid or Empty State */}
|
||||||
<div className="flex items-center gap-2 overflow-x-auto pb-0.5 no-scrollbar">
|
{filteredSkills.length === 0 ? (
|
||||||
{Object.entries(categories).map(([cat, count]) => {
|
|
||||||
const isSelected = categoryFilter === cat;
|
|
||||||
const displayLabel = cat === 'ALL' ? 'All' : (cat.charAt(0) + cat.slice(1).toLowerCase()).replace(/_/g, ' ');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={cat}
|
|
||||||
type="button"
|
|
||||||
onClick={() => setCategoryFilter(cat)}
|
|
||||||
className={cn(
|
|
||||||
'px-3.5 py-1.5 rounded-xl text-xs font-semibold transition-all duration-200 whitespace-nowrap cursor-pointer flex items-center gap-1.5 border',
|
|
||||||
isSelected
|
|
||||||
? 'bg-slate-900 text-white border-slate-900 shadow-sm'
|
|
||||||
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50 hover:border-slate-300 hover:text-slate-900'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<span>{displayLabel}</span>
|
|
||||||
<span
|
|
||||||
className={cn(
|
|
||||||
'text-[10px] font-bold px-1.5 py-0.5 rounded-md',
|
|
||||||
isSelected ? 'bg-white/20 text-white' : 'bg-slate-100 text-slate-500'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{count}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 4. Stats + New Skill Row */}
|
|
||||||
<div className="flex items-center justify-between gap-3">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-50 border border-slate-200 text-xs">
|
|
||||||
<span className="text-slate-500">Total</span>
|
|
||||||
<span className="font-bold text-slate-900">{filteredSkills.length}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-50 border border-emerald-200 text-xs">
|
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
|
|
||||||
<span className="text-emerald-700 font-semibold">{enabledCount} active</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onNewSkill}
|
|
||||||
disabled={!canCreate}
|
|
||||||
title={canCreate ? undefined : createDisabledReason}
|
|
||||||
className="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 text-white font-semibold text-xs shadow-sm transition-all active:scale-95 cursor-pointer disabled:cursor-not-allowed disabled:opacity-40"
|
|
||||||
>
|
|
||||||
<Plus className="w-3.5 h-3.5 stroke-[2.5]" />
|
|
||||||
<span>New Skill</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 5. Responsive Skill Cards Grid */}
|
|
||||||
{scopeTab === 'board' && filteredSkills.length === 0 ? (
|
|
||||||
<div className="p-10 rounded-2xl border border-dashed border-slate-200 bg-white/60 text-center space-y-2">
|
|
||||||
<Layers className="w-7 h-7 text-slate-400 mx-auto" />
|
|
||||||
<div className="text-sm font-semibold text-slate-700">No Board Skills Connected</div>
|
|
||||||
<p className="text-xs text-slate-400 max-w-sm mx-auto">
|
|
||||||
None drawing on this agent's surfaces. Connect board skills to link live operational boards with this agent.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : filteredSkills.length === 0 ? (
|
|
||||||
<div className="p-10 rounded-2xl border border-slate-200 bg-white text-center space-y-3 shadow-2xs">
|
<div className="p-10 rounded-2xl border border-slate-200 bg-white text-center space-y-3 shadow-2xs">
|
||||||
<AlertCircle className="w-7 h-7 text-slate-400 mx-auto" />
|
<AlertCircle className="w-8 h-8 text-slate-400 mx-auto" />
|
||||||
<div className="text-sm font-semibold text-slate-800">No matching capabilities found</div>
|
<div className="text-sm font-bold text-slate-800">No matching skills found</div>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-xs text-slate-500 max-w-sm mx-auto">
|
||||||
Try adjusting your search query or selecting a different category filter.
|
{searchQuery
|
||||||
|
? `No skills matched "${searchQuery}". Try a different keyword or reset filters.`
|
||||||
|
: 'There are no skills configured under the selected filter criteria.'}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setCategoryFilter('ALL');
|
setCategoryFilter('ALL');
|
||||||
|
setStatusFilter('ALL');
|
||||||
|
setScopeFilter('ALL');
|
||||||
}}
|
}}
|
||||||
className="px-3.5 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-xs font-medium text-slate-700 transition-colors cursor-pointer"
|
className="px-4 py-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-xs font-semibold text-slate-700 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
Clear Filters
|
Reset Filters
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -230,6 +202,8 @@ export default function SkillCardsGrid({
|
|||||||
skill={skill}
|
skill={skill}
|
||||||
onTest={onTestSkill}
|
onTest={onTestSkill}
|
||||||
onDetails={onOpenDetails}
|
onDetails={onOpenDetails}
|
||||||
|
onToggleStatus={onToggleStatus}
|
||||||
|
canEdit={canEdit}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,207 +11,262 @@ import {
|
|||||||
Bell,
|
Bell,
|
||||||
Database,
|
Database,
|
||||||
X,
|
X,
|
||||||
Hash,
|
Bot,
|
||||||
MapPin,
|
Info,
|
||||||
Layers,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
/* ─── Kind config ─────────────────────────────────────────────────────────── */
|
/* ─── Friendly Tool Dictionary ────────────────────────────────────────────── */
|
||||||
const KIND = {
|
const TOOL_DICTIONARY = {
|
||||||
|
alert_low_battery_rider: {
|
||||||
|
title: 'Rider Battery Warning',
|
||||||
|
desc: 'Sends an urgent notification to riders with low vehicle battery to recharge or report to a hub.',
|
||||||
|
category: 'Fleet & Riders',
|
||||||
|
},
|
||||||
|
assign_express_batch: {
|
||||||
|
title: 'Assign Express Batch',
|
||||||
|
desc: 'Automatically assigns grouped high-priority express orders to optimal nearby riders.',
|
||||||
|
category: 'Orders & Dispatch',
|
||||||
|
},
|
||||||
|
assign_riders: {
|
||||||
|
title: 'Assign Orders to Riders',
|
||||||
|
desc: 'Dispatches and matches unassigned customer orders with active, available delivery riders.',
|
||||||
|
category: 'Orders & Dispatch',
|
||||||
|
},
|
||||||
|
create_single_order: {
|
||||||
|
title: 'Create Express Booking',
|
||||||
|
desc: 'Creates an on-demand express delivery booking upon operator review and confirmation.',
|
||||||
|
category: 'Orders & Dispatch',
|
||||||
|
},
|
||||||
|
decide_assignment_failure: {
|
||||||
|
title: 'Diagnose Assignment Failure',
|
||||||
|
desc: 'Analyzes why an order failed assignment and recommends an immediate resolution.',
|
||||||
|
category: 'Analysis & Diagnostics',
|
||||||
|
},
|
||||||
|
decide_stall_response: {
|
||||||
|
title: 'Resolve Stalled Rider',
|
||||||
|
desc: 'Detects riders stopped mid-route and initiates automatic reassignment or customer updates.',
|
||||||
|
category: 'Analysis & Diagnostics',
|
||||||
|
},
|
||||||
|
dispatch_hub_idle_parcels: {
|
||||||
|
title: 'Dispatch Idle Hub Parcels',
|
||||||
|
desc: 'Coordinates idle riders to pick up parcels waiting at crowded hubs.',
|
||||||
|
category: 'Orders & Dispatch',
|
||||||
|
},
|
||||||
|
enforce_cash_handoff: {
|
||||||
|
title: 'Enforce Cash Hand-off',
|
||||||
|
desc: 'Routes riders carrying high Cash-on-Delivery balances to deposit cash at the nearest hub.',
|
||||||
|
category: 'Fleet & Riders',
|
||||||
|
},
|
||||||
|
enforce_otp_verification: {
|
||||||
|
title: 'Require Delivery OTP',
|
||||||
|
desc: 'Enforces one-time password verification before handing over high-value COD orders.',
|
||||||
|
category: 'Fleet & Riders',
|
||||||
|
},
|
||||||
|
nearby_milers: {
|
||||||
|
title: 'Locate Nearby Riders',
|
||||||
|
desc: 'Searches real-time GPS locations to find active delivery riders within a specified radius.',
|
||||||
|
category: 'Fleet & Riders',
|
||||||
|
},
|
||||||
|
notify_customer: {
|
||||||
|
title: 'Send Customer Update',
|
||||||
|
desc: 'Sends automated SMS or WhatsApp status notifications to the recipient about order delays or arrivals.',
|
||||||
|
category: 'Customer Alerts',
|
||||||
|
},
|
||||||
|
publish_miler_stalled: {
|
||||||
|
title: 'Broadcast Delay Event',
|
||||||
|
desc: 'Broadcasts a stalled delivery alert to the operations team dashboard.',
|
||||||
|
category: 'Customer Alerts',
|
||||||
|
},
|
||||||
|
reassign_booking: {
|
||||||
|
title: 'Reassign to New Rider',
|
||||||
|
desc: 'Transfers a stalled or delayed order to an alternate available rider to protect delivery SLA.',
|
||||||
|
category: 'Orders & Dispatch',
|
||||||
|
},
|
||||||
|
rebalance_riders: {
|
||||||
|
title: 'Rebalance Fleet Zones',
|
||||||
|
desc: 'Directs idle riders from low-demand areas to high-volume delivery zones.',
|
||||||
|
category: 'Fleet & Riders',
|
||||||
|
},
|
||||||
|
scan_bookings: {
|
||||||
|
title: 'Scan Pending Orders',
|
||||||
|
desc: 'Continuously monitors order queues for delayed or unassigned shipments.',
|
||||||
|
category: 'Orders & Dispatch',
|
||||||
|
},
|
||||||
|
simulate_pricing_quote: {
|
||||||
|
title: 'Calculate Delivery Quote',
|
||||||
|
desc: 'Estimates accurate delivery fares and ETA based on routed distance and vehicle type.',
|
||||||
|
category: 'Pricing & Route',
|
||||||
|
},
|
||||||
|
trigger_auto_dispatch: {
|
||||||
|
title: 'Trigger Auto-Dispatch',
|
||||||
|
desc: 'Automatically pairs waiting orders with the closest available riders during peak rush.',
|
||||||
|
category: 'Orders & Dispatch',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
'Orders & Dispatch',
|
||||||
|
'Fleet & Riders',
|
||||||
|
'Customer Alerts',
|
||||||
|
'Analysis & Diagnostics',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Convert snake_case code into clean Title Case if not in dictionary */
|
||||||
|
function formatToolTitle(rawName) {
|
||||||
|
if (TOOL_DICTIONARY[rawName]?.title) return TOOL_DICTIONARY[rawName].title;
|
||||||
|
return rawName
|
||||||
|
.split('_')
|
||||||
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
|
.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sanitize developer descriptions into clean operational text */
|
||||||
|
function formatToolDescription(tool) {
|
||||||
|
if (TOOL_DICTIONARY[tool.name]?.desc) return TOOL_DICTIONARY[tool.name].desc;
|
||||||
|
let text = tool.description || 'No description provided.';
|
||||||
|
text = text.replace(/REVIEW ONLY\.\s*/gi, '');
|
||||||
|
text = text.replace(/No executor\.\s*/gi, '');
|
||||||
|
text = text.replace(/\(structured output\)\s*/gi, '');
|
||||||
|
text = text.replace(/POST\s+[^\s]+\s+accepts[^\.]+\.\s*/gi, '');
|
||||||
|
text = text.replace(/NOT IMPLEMENTED:[^\.]+\.\s*/gi, '');
|
||||||
|
return text.trim() || 'Executes this automated workflow action.';
|
||||||
|
}
|
||||||
|
|
||||||
|
const KIND_META = {
|
||||||
read: {
|
read: {
|
||||||
icon: Eye,
|
icon: Eye,
|
||||||
label: 'Read',
|
label: 'Lookup',
|
||||||
card: 'border-sky-200 bg-sky-50/40',
|
badge: 'bg-sky-50 text-sky-700 border-sky-200',
|
||||||
iconWrap: 'bg-sky-100 text-sky-600',
|
|
||||||
badge: 'bg-sky-100 text-sky-700 border-sky-200',
|
|
||||||
dot: 'bg-sky-500',
|
|
||||||
},
|
},
|
||||||
write: {
|
write: {
|
||||||
icon: Edit3,
|
icon: Edit3,
|
||||||
label: 'Write',
|
label: 'Action',
|
||||||
card: 'border-violet-200 bg-violet-50/30',
|
badge: 'bg-violet-50 text-violet-700 border-violet-200',
|
||||||
iconWrap: 'bg-violet-100 text-violet-600',
|
|
||||||
badge: 'bg-violet-100 text-violet-700 border-violet-200',
|
|
||||||
dot: 'bg-violet-500',
|
|
||||||
},
|
},
|
||||||
notify: {
|
notify: {
|
||||||
icon: Bell,
|
icon: Bell,
|
||||||
label: 'Notify',
|
label: 'Alert',
|
||||||
card: 'border-amber-200 bg-amber-50/30',
|
badge: 'bg-amber-50 text-amber-700 border-amber-200',
|
||||||
iconWrap: 'bg-amber-100 text-amber-600',
|
|
||||||
badge: 'bg-amber-100 text-amber-700 border-amber-200',
|
|
||||||
dot: 'bg-amber-500',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const KIND_DEFAULT = {
|
|
||||||
|
const DEFAULT_KIND_META = {
|
||||||
icon: Database,
|
icon: Database,
|
||||||
label: 'General',
|
label: 'Action',
|
||||||
card: 'border-slate-200 bg-white',
|
badge: 'bg-slate-100 text-slate-700 border-slate-200',
|
||||||
iconWrap: 'bg-slate-100 text-slate-500',
|
|
||||||
badge: 'bg-slate-100 text-slate-600 border-slate-200',
|
|
||||||
dot: 'bg-slate-400',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function kindOf(tool) {
|
function ToolRow({ tool }) {
|
||||||
return KIND[(tool.kind || '').toLowerCase()] || KIND_DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Stat chip ───────────────────────────────────────────────────────────── */
|
|
||||||
function StatChip({ label, value, tone = 'slate' }) {
|
|
||||||
const tones = {
|
|
||||||
slate: 'bg-slate-100 text-slate-700 border-slate-200',
|
|
||||||
amber: 'bg-amber-50 text-amber-700 border-amber-200',
|
|
||||||
emerald: 'bg-emerald-50 text-emerald-700 border-emerald-200',
|
|
||||||
sky: 'bg-sky-50 text-sky-700 border-sky-200',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div className={cn('flex flex-col items-center px-5 py-3 rounded-2xl border', tones[tone])}>
|
|
||||||
<span className="text-2xl font-black leading-none">{value}</span>
|
|
||||||
<span className="text-[10px] font-semibold uppercase tracking-widest mt-1 opacity-70">{label}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Parameter pill ──────────────────────────────────────────────────────── */
|
|
||||||
function ParamPill({ param }) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1 p-3 rounded-xl bg-white border border-slate-200 shadow-sm">
|
|
||||||
<div className="flex items-center justify-between gap-2">
|
|
||||||
<span className="font-mono font-bold text-[11px] text-brand">{param.name}</span>
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className="font-mono text-[9px] px-1.5 py-0.5 rounded-md bg-slate-100 text-slate-600 border border-slate-200">
|
|
||||||
{param.type}
|
|
||||||
</span>
|
|
||||||
{param.required && (
|
|
||||||
<span className="text-[9px] font-bold text-rose-600 bg-rose-50 px-1.5 py-0.5 rounded-md border border-rose-200">
|
|
||||||
req
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{param.description && (
|
|
||||||
<p className="text-[10.5px] text-slate-500 leading-relaxed">{param.description}</p>
|
|
||||||
)}
|
|
||||||
{param.default !== undefined && (
|
|
||||||
<div className="text-[9.5px] font-mono text-slate-400 bg-slate-50 px-2 py-0.5 rounded-md mt-0.5">
|
|
||||||
default: {String(param.default)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ─── Tool Card ───────────────────────────────────────────────────────────── */
|
|
||||||
function ToolCard({ tool }) {
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const k = kindOf(tool);
|
const meta = KIND_META[(tool.kind || '').toLowerCase()] || DEFAULT_KIND_META;
|
||||||
const KindIcon = k.icon;
|
const KindIcon = meta.icon;
|
||||||
|
const friendlyTitle = formatToolTitle(tool.name);
|
||||||
|
const friendlyDesc = formatToolDescription(tool);
|
||||||
const hasParams = tool.parameters && tool.parameters.length > 0;
|
const hasParams = tool.parameters && tool.parameters.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-2xl border transition-all duration-200 overflow-hidden group',
|
'rounded-xl border bg-white transition-all duration-150 overflow-hidden shadow-2xs',
|
||||||
k.card,
|
open ? 'border-brand/40 shadow-xs' : 'border-slate-200 hover:border-slate-300'
|
||||||
open ? 'shadow-md' : 'shadow-sm hover:shadow-md hover:-translate-y-px'
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{/* Card header — always visible */}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpen((v) => !v)}
|
onClick={() => setOpen((v) => !v)}
|
||||||
className="w-full text-left p-4 flex items-start gap-3 cursor-pointer"
|
className="w-full text-left p-3.5 flex items-center justify-between gap-3 cursor-pointer group"
|
||||||
>
|
>
|
||||||
{/* Kind icon */}
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className={cn('w-9 h-9 rounded-xl flex items-center justify-center shrink-0 shadow-sm', k.iconWrap)}>
|
<div className="w-8 h-8 rounded-lg bg-slate-100 border border-slate-200 text-slate-600 flex items-center justify-center shrink-0 group-hover:text-brand transition-colors">
|
||||||
<KindIcon className="w-4 h-4" />
|
<KindIcon className="w-4 h-4" />
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Name + description */}
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center gap-2 flex-wrap mb-1">
|
|
||||||
<span className="font-mono font-black text-xs text-slate-900 tracking-tight">{tool.name}</span>
|
|
||||||
|
|
||||||
{/* Kind badge */}
|
|
||||||
<span className={cn('px-2 py-0.5 rounded-lg text-[9.5px] font-bold border capitalize', k.badge)}>
|
|
||||||
{k.label}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Safety */}
|
|
||||||
{tool.requiresConfirmation ? (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-[9.5px] font-bold bg-rose-50 text-rose-600 border border-rose-200">
|
|
||||||
<ShieldAlert className="w-2.5 h-2.5" />
|
|
||||||
Gated
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-[9.5px] font-bold bg-emerald-50 text-emerald-700 border border-emerald-200">
|
|
||||||
<ShieldCheck className="w-2.5 h-2.5" />
|
|
||||||
Open
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Params count */}
|
|
||||||
{hasParams && (
|
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-[9.5px] font-bold bg-slate-100 text-slate-600 border border-slate-200">
|
|
||||||
<Hash className="w-2.5 h-2.5" />
|
|
||||||
{tool.parameters.length}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-[11px] text-slate-500 leading-relaxed line-clamp-1">{tool.description}</p>
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-xs sm:text-sm font-bold text-slate-900 group-hover:text-brand transition-colors">
|
||||||
|
{friendlyTitle}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Code pill */}
|
||||||
|
<span className="font-mono text-[10px] text-slate-400 bg-slate-100 px-1.5 py-0.5 rounded">
|
||||||
|
{tool.name}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Kind Badge */}
|
||||||
|
<span className={cn('px-2 py-0.5 rounded-md text-[10px] font-semibold border', meta.badge)}>
|
||||||
|
{meta.label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Safety Badge */}
|
||||||
|
{tool.requiresConfirmation ? (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-semibold bg-amber-50 text-amber-700 border border-amber-200">
|
||||||
|
<ShieldAlert className="w-3 h-3 text-amber-600" />
|
||||||
|
Needs Approval
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-md text-[10px] font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">
|
||||||
|
<ShieldCheck className="w-3 h-3 text-emerald-600" />
|
||||||
|
Automated
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-500 mt-1 line-clamp-1 leading-normal">
|
||||||
|
{friendlyDesc}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expand chevron */}
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={cn(
|
className={cn(
|
||||||
'w-4 h-4 text-slate-400 shrink-0 mt-1 transition-transform duration-200',
|
'w-4 h-4 text-slate-400 group-hover:text-slate-700 transition-transform duration-200',
|
||||||
open && 'rotate-180 text-brand'
|
open && 'rotate-180 text-brand'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Expandable detail panel */}
|
|
||||||
{open && (
|
{open && (
|
||||||
<div className="px-4 pb-4 pt-1 border-t border-black/5 space-y-4">
|
<div className="px-4 pb-4 pt-2 border-t border-slate-100 bg-slate-50/70 space-y-3">
|
||||||
{/* Meta grid */}
|
<div className="text-xs text-slate-600 leading-relaxed">
|
||||||
{(tool.target || tool.implementedAt || tool.parentSkill) && (
|
<span className="font-semibold text-slate-700">Detailed Action: </span>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
{friendlyDesc}
|
||||||
{tool.target && (
|
</div>
|
||||||
<div className="flex items-start gap-2 p-2.5 rounded-xl bg-white/70 border border-slate-200">
|
|
||||||
<MapPin className="w-3.5 h-3.5 text-slate-400 mt-0.5 shrink-0" />
|
{tool.parentSkill && (
|
||||||
<div>
|
<div className="text-xs text-slate-600">
|
||||||
<div className="text-[9px] font-bold text-slate-400 uppercase tracking-wider">Touches</div>
|
<span className="font-semibold text-slate-700">Used by Skills: </span>
|
||||||
<div className="font-mono text-[11px] text-slate-800 mt-0.5">{tool.target}</div>
|
<span className="text-brand font-medium">{tool.parentSkill}</span>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{tool.implementedAt && (
|
|
||||||
<div className="flex items-start gap-2 p-2.5 rounded-xl bg-white/70 border border-slate-200">
|
|
||||||
<Layers className="w-3.5 h-3.5 text-slate-400 mt-0.5 shrink-0" />
|
|
||||||
<div>
|
|
||||||
<div className="text-[9px] font-bold text-slate-400 uppercase tracking-wider">Implemented at</div>
|
|
||||||
<div className="font-mono text-[10px] text-slate-700 mt-0.5 break-all">{tool.implementedAt}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Parameters */}
|
{hasParams && (
|
||||||
{hasParams ? (
|
<div className="space-y-1.5 pt-1">
|
||||||
<div className="space-y-2">
|
<div className="flex items-center gap-1.5 text-[10.5px] font-bold uppercase tracking-wider text-slate-500">
|
||||||
<div className="flex items-center gap-1.5 text-[10px] font-bold text-slate-600 uppercase tracking-wider">
|
|
||||||
<Code2 className="w-3.5 h-3.5 text-brand" />
|
<Code2 className="w-3.5 h-3.5 text-brand" />
|
||||||
Parameters
|
<span>Required Inputs ({tool.parameters.length})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||||
{tool.parameters.map((p) => <ParamPill key={p.name} param={p} />)}
|
{tool.parameters.map((p) => (
|
||||||
|
<div
|
||||||
|
key={p.name}
|
||||||
|
className="p-2.5 rounded-lg bg-white border border-slate-200 flex flex-col gap-0.5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-mono text-xs font-bold text-brand">{p.name}</span>
|
||||||
|
<span className="text-[9.5px] font-mono px-1.5 py-0.5 rounded bg-slate-100 text-slate-600">
|
||||||
|
{p.type || 'text'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{p.description && (
|
||||||
|
<p className="text-[11px] text-slate-500 mt-0.5">{p.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<p className="text-[11px] text-slate-400 italic">No parameters — input is open-ended.</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -219,131 +274,201 @@ function ToolCard({ tool }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Main view ───────────────────────────────────────────────────────────── */
|
export default function ToolRegistryView({
|
||||||
export default function ToolRegistryView({ tools = [] }) {
|
tools = [],
|
||||||
|
agentTools = [],
|
||||||
|
activeAgent = null,
|
||||||
|
}) {
|
||||||
|
const [scope, setScope] = useState('AGENT'); // 'AGENT' | 'ALL'
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [kindFilter, setKindFilter] = useState('all');
|
const [categoryFilter, setCategoryFilter] = useState('Orders & Dispatch');
|
||||||
|
|
||||||
const counts = useMemo(() => {
|
// If the agent has no linked tools, default to showing all tools
|
||||||
const c = { all: tools.length };
|
const effectiveScope = scope === 'AGENT' && agentTools.length > 0 ? 'AGENT' : 'ALL';
|
||||||
tools.forEach((t) => {
|
const baseTools = effectiveScope === 'AGENT' ? agentTools : tools;
|
||||||
const k = (t.kind || 'read').toLowerCase();
|
|
||||||
c[k] = (c[k] || 0) + 1;
|
|
||||||
});
|
|
||||||
return c;
|
|
||||||
}, [tools]);
|
|
||||||
|
|
||||||
const kindTabs = ['all', ...Object.keys(KIND).filter((k) => counts[k])];
|
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
const filtered = useMemo(() => {
|
||||||
const q = search.toLowerCase().trim();
|
const q = search.toLowerCase().trim();
|
||||||
return tools.filter((t) => {
|
return baseTools.filter((t) => {
|
||||||
const matchesKind = kindFilter === 'all' || (t.kind || 'read').toLowerCase() === kindFilter;
|
// In ALL tools mode, filter by selected category unless searching
|
||||||
const matchesSearch =
|
if (effectiveScope === 'ALL' && !q) {
|
||||||
!q ||
|
const cat = TOOL_DICTIONARY[t.name]?.category || 'Orders & Dispatch';
|
||||||
t.name.toLowerCase().includes(q) ||
|
if (cat !== categoryFilter) return false;
|
||||||
t.description.toLowerCase().includes(q) ||
|
}
|
||||||
(t.parentSkill || '').toLowerCase().includes(q) ||
|
|
||||||
(t.target || '').toLowerCase().includes(q);
|
|
||||||
return matchesKind && matchesSearch;
|
|
||||||
});
|
|
||||||
}, [tools, kindFilter, search]);
|
|
||||||
|
|
||||||
const gatedCount = tools.filter((t) => t.requiresConfirmation).length;
|
if (!q) return true;
|
||||||
const openCount = tools.length - gatedCount;
|
const friendlyTitle = formatToolTitle(t.name).toLowerCase();
|
||||||
|
const friendlyDesc = formatToolDescription(t).toLowerCase();
|
||||||
|
return (
|
||||||
|
t.name.toLowerCase().includes(q) ||
|
||||||
|
friendlyTitle.includes(q) ||
|
||||||
|
friendlyDesc.includes(q)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [baseTools, effectiveScope, categoryFilter, search]);
|
||||||
|
|
||||||
|
const agentName = activeAgent?.name || 'Agent';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
{/* ── Stats row ─────────────────────────────────────────────────── */}
|
{/* 1. Scope Selector Tabs: Agent Tools vs All Tools */}
|
||||||
<div className="flex flex-wrap items-stretch gap-3">
|
<div className="flex items-center justify-between gap-3 border-b border-slate-200 pb-3 flex-wrap">
|
||||||
<StatChip label="Total Tools" value={tools.length} tone="slate" />
|
<div className="flex items-center gap-2 bg-slate-100 p-1 rounded-xl">
|
||||||
<StatChip label="Read" value={counts.read || 0} tone="sky" />
|
<button
|
||||||
<StatChip label="Write / Notify" value={(counts.write || 0) + (counts.notify || 0)} tone="amber" />
|
type="button"
|
||||||
<StatChip label="Gated" value={gatedCount} tone="amber" />
|
onClick={() => {
|
||||||
<StatChip label="Open" value={openCount} tone="emerald" />
|
setScope('AGENT');
|
||||||
|
setSearch('');
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'px-3.5 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer flex items-center gap-1.5',
|
||||||
|
effectiveScope === 'AGENT'
|
||||||
|
? 'bg-white text-slate-900 shadow-2xs'
|
||||||
|
: 'text-slate-500 hover:text-slate-800'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Bot className="w-3.5 h-3.5 text-brand" />
|
||||||
|
<span>{agentName}'s Tools</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-1.5 py-0.5 rounded-md text-[10px]',
|
||||||
|
effectiveScope === 'AGENT' ? 'bg-brand/10 text-brand' : 'bg-slate-200 text-slate-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{agentTools.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Right-side header text */}
|
<button
|
||||||
<div className="flex-1 min-w-[200px] flex flex-col justify-center pl-2">
|
type="button"
|
||||||
<h3 className="text-sm font-bold text-slate-900 font-heading flex items-center gap-2">
|
onClick={() => {
|
||||||
<Wrench className="w-4 h-4 text-brand" />
|
setScope('ALL');
|
||||||
Agent Tool Registry
|
setSearch('');
|
||||||
</h3>
|
setCategoryFilter('Orders & Dispatch');
|
||||||
<p className="text-[11px] text-slate-500 mt-0.5 leading-relaxed">
|
}}
|
||||||
Every capability a skill may call. Write & notify tools need operator confirmation unless the agent is autonomous.
|
className={cn(
|
||||||
</p>
|
'px-3.5 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer flex items-center gap-1.5',
|
||||||
|
effectiveScope === 'ALL'
|
||||||
|
? 'bg-white text-slate-900 shadow-2xs'
|
||||||
|
: 'text-slate-500 hover:text-slate-800'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Wrench className="w-3.5 h-3.5 text-slate-400" />
|
||||||
|
<span>All System Tools</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'px-1.5 py-0.5 rounded-md text-[10px]',
|
||||||
|
effectiveScope === 'ALL' ? 'bg-slate-900 text-white' : 'bg-slate-200 text-slate-500'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tools.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Helper Note */}
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-slate-500">
|
||||||
|
<Info className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
||||||
|
<span>Tools are automated actions agents can execute in your system.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Search + Kind filter ───────────────────────────────────────── */}
|
{/* 2. Search & Category Filters */}
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
|
||||||
{/* Search */}
|
{/* Search Input */}
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
<Search className="w-4 h-4 text-slate-400 absolute left-3.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder="Search by name, target system, or description…"
|
placeholder="Search tools by action, rider, booking..."
|
||||||
className="w-full h-10 bg-white border border-slate-200 rounded-xl pl-10 pr-9 text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:border-brand focus:ring-2 focus:ring-brand/10 shadow-sm transition-all"
|
className="w-full h-10 bg-white border border-slate-200 rounded-xl pl-10 pr-9 text-xs sm:text-sm text-slate-800 placeholder:text-slate-400 focus:outline-none focus:border-brand focus:ring-2 focus:ring-brand/10 shadow-2xs hover:border-slate-300 transition-all"
|
||||||
/>
|
/>
|
||||||
{search && (
|
{search && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSearch('')}
|
onClick={() => setSearch('')}
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 p-0.5 rounded-full text-slate-400 hover:text-slate-700 hover:bg-slate-100 cursor-pointer transition-colors"
|
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-700 rounded-full hover:bg-slate-100 transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<X className="w-3.5 h-3.5" />
|
<X className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Kind filter tabs */}
|
{/* Category Pills (Without 'All' filter) */}
|
||||||
<div className="flex items-center gap-1 bg-slate-100 rounded-xl p-1 shrink-0">
|
{effectiveScope === 'ALL' && (
|
||||||
{kindTabs.map((k) => {
|
<div className="flex items-center gap-1.5 overflow-x-auto pb-0.5">
|
||||||
const meta = KIND[k];
|
{CATEGORIES.map((cat) => {
|
||||||
const isActive = kindFilter === k;
|
const isSelected = categoryFilter === cat;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={k}
|
key={cat}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setKindFilter(k)}
|
onClick={() => setCategoryFilter(cat)}
|
||||||
className={cn(
|
|
||||||
'px-3 py-1.5 rounded-lg text-xs font-semibold transition-all cursor-pointer capitalize flex items-center gap-1.5',
|
|
||||||
isActive ? 'bg-white text-slate-900 shadow-sm' : 'text-slate-500 hover:text-slate-800'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{meta && <span className={cn('w-1.5 h-1.5 rounded-full', meta.dot)} />}
|
|
||||||
{k === 'all' ? 'All' : meta?.label || k}
|
|
||||||
<span
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'text-[10px] font-bold',
|
'px-3 py-1.5 rounded-lg text-xs font-semibold whitespace-nowrap cursor-pointer transition-all border',
|
||||||
isActive ? 'text-slate-600' : 'text-slate-400'
|
isSelected
|
||||||
|
? 'bg-slate-900 text-white border-slate-900 shadow-2xs'
|
||||||
|
: 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{counts[k] || 0}
|
{cat}
|
||||||
</span>
|
</button>
|
||||||
</button>
|
);
|
||||||
);
|
})}
|
||||||
})}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Tool Cards ────────────────────────────────────────────────── */}
|
{/* 3. Result Count */}
|
||||||
{filtered.length === 0 ? (
|
<div className="flex items-center justify-between text-xs text-slate-500 px-0.5">
|
||||||
<div className="p-16 rounded-2xl border border-dashed border-slate-200 bg-slate-50 text-center">
|
<div>
|
||||||
<Wrench className="w-8 h-8 text-slate-300 mx-auto mb-3" />
|
<span>Showing </span>
|
||||||
<p className="text-sm font-semibold text-slate-500">No tools match your filter</p>
|
<span className="font-semibold text-slate-800">{filtered.length}</span>
|
||||||
|
<span>
|
||||||
|
{effectiveScope === 'ALL' && !search
|
||||||
|
? ` tools in ${categoryFilter}`
|
||||||
|
: ` of ${baseTools.length} ${effectiveScope === 'AGENT' ? `${agentName} tools` : 'tools'}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{search && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setSearch(''); setKindFilter('all'); }}
|
onClick={() => setSearch('')}
|
||||||
className="mt-3 text-xs text-brand font-semibold hover:underline cursor-pointer"
|
className="text-xs text-brand font-medium hover:underline cursor-pointer"
|
||||||
>
|
>
|
||||||
Clear filters
|
Clear search
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 4. Tool List */}
|
||||||
|
{filtered.length === 0 ? (
|
||||||
|
<div className="p-8 rounded-2xl border border-slate-200 bg-white text-center space-y-2 shadow-2xs">
|
||||||
|
<Wrench className="w-7 h-7 text-slate-300 mx-auto" />
|
||||||
|
<div className="text-sm font-bold text-slate-800">No tools found</div>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
{search
|
||||||
|
? `No tools matched "${search}".`
|
||||||
|
: `No tools configured under ${categoryFilter}.`}
|
||||||
|
</p>
|
||||||
|
{search && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSearch('')}
|
||||||
|
className="px-3 py-1.5 rounded-lg bg-slate-100 hover:bg-slate-200 text-xs font-semibold text-slate-700 transition-colors cursor-pointer"
|
||||||
|
>
|
||||||
|
Clear Search
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
<div className="space-y-2">
|
||||||
{filtered.map((tool) => <ToolCard key={tool.name} tool={tool} />)}
|
{filtered.map((tool) => (
|
||||||
|
<ToolRow key={tool.name} tool={tool} />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { AlertTriangle, CheckCircle2, Info, XCircle } from 'lucide-react';
|
import { AlertTriangle, CheckCircle2, Info, XCircle, ChevronDown, ChevronUp, Cpu } from 'lucide-react';
|
||||||
import { useAiStatus } from '@/lib/doormileHooks';
|
import { useAiStatus } from '@/lib/doormileHooks';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* What this page's settings actually drive, from GET /admin/ai/status — not a
|
* What this page's settings actually drive, from GET /admin/ai/status.
|
||||||
* fixed note. It used to say "Half wired" forever, including after AI_engine
|
* Designed with a clean, unobtrusive status bar that can be expanded
|
||||||
* and the backend were deployed, because nothing checked.
|
* for detailed diagnostics when needed.
|
||||||
*
|
|
||||||
* Green only when AI_engine has read the registry recently (the backend stamps
|
|
||||||
* every poll) and the Test tab has a model. Re-checks every 30 seconds.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
function ago(iso) {
|
function ago(iso) {
|
||||||
@@ -49,11 +47,13 @@ export function wiringLines(status) {
|
|||||||
|
|
||||||
export default function WiringStatusBanner() {
|
export default function WiringStatusBanner() {
|
||||||
const { data, isLoading, isError, error } = useAiStatus();
|
const { data, isLoading, isError, error } = useAiStatus();
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div role="note" className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3.5 text-xs text-slate-500">
|
<div className="flex items-center gap-2 rounded-xl border border-slate-200 bg-slate-50/70 px-3.5 py-2 text-xs text-slate-500">
|
||||||
Checking what is connected…
|
<Cpu className="w-3.5 h-3.5 animate-pulse text-slate-400" />
|
||||||
|
<span>Checking system connectivity…</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -61,17 +61,15 @@ export default function WiringStatusBanner() {
|
|||||||
if (isError) {
|
if (isError) {
|
||||||
const notDeployed = error?.response?.status === 404;
|
const notDeployed = error?.response?.status === 404;
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="flex items-center justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50/60 px-4 py-2.5 text-xs text-amber-900 shadow-2xs">
|
||||||
role="note"
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
className="flex items-start gap-3 rounded-2xl border border-amber-200/80 bg-gradient-to-r from-amber-50 to-orange-50 px-4 py-3.5 text-xs text-amber-900 shadow-sm"
|
<AlertTriangle className="w-4 h-4 shrink-0 text-amber-500" />
|
||||||
>
|
<span className="font-medium truncate">
|
||||||
<AlertTriangle className="w-4 h-4 mt-0.5 shrink-0 text-amber-500" />
|
{notDeployed
|
||||||
<p className="leading-relaxed">
|
? 'API status reporting is starting up. Changes made here are saved safely.'
|
||||||
<span className="font-bold text-amber-800">Connection status unavailable.</span>{' '}
|
: 'Status check retrying shortly. Changes made here are saved safely.'}
|
||||||
{notDeployed
|
</span>
|
||||||
? 'This API server does not report agent status yet — deploy the current doormile_backend build. Changes here are still saved and audited.'
|
</div>
|
||||||
: 'The status check failed; it will retry shortly. Changes here are still saved and audited.'}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -81,28 +79,63 @@ export default function WiringStatusBanner() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
role="note"
|
className={cn(
|
||||||
className={
|
'rounded-xl border transition-all duration-200 shadow-2xs overflow-hidden',
|
||||||
allOk
|
allOk
|
||||||
? 'rounded-2xl border border-emerald-200/80 bg-gradient-to-r from-emerald-50 to-teal-50 px-4 py-3.5 text-xs text-emerald-900 shadow-sm'
|
? 'border-emerald-200/90 bg-emerald-50/40 text-emerald-900'
|
||||||
: 'rounded-2xl border border-amber-200/80 bg-gradient-to-r from-amber-50 to-orange-50 px-4 py-3.5 text-xs text-amber-900 shadow-sm'
|
: 'border-slate-200 bg-white text-slate-700'
|
||||||
}
|
)}
|
||||||
>
|
>
|
||||||
<p className={`font-bold mb-1.5 ${allOk ? 'text-emerald-800' : 'text-amber-800'}`}>
|
{/* Sleek top summary bar */}
|
||||||
{allOk ? 'All connected.' : 'Partly connected.'}
|
<div className="flex items-center justify-between px-3.5 py-2 gap-3">
|
||||||
</p>
|
<div className="flex items-center gap-2.5 min-w-0">
|
||||||
<ul className="space-y-1">
|
<span
|
||||||
{lines.map((l) => {
|
className={cn(
|
||||||
const Icon = l.info ? Info : l.ok ? CheckCircle2 : XCircle;
|
'w-2 h-2 rounded-full shrink-0',
|
||||||
const color = l.info ? 'text-slate-400' : l.ok ? 'text-emerald-600' : 'text-amber-600';
|
allOk ? 'bg-emerald-500 ring-2 ring-emerald-200' : 'bg-amber-400 ring-2 ring-amber-100'
|
||||||
return (
|
)}
|
||||||
<li key={l.text} className="flex items-start gap-2 leading-relaxed">
|
/>
|
||||||
<Icon className={`w-3.5 h-3.5 mt-0.5 shrink-0 ${color}`} />
|
<span className="text-xs font-semibold text-slate-800">
|
||||||
<span>{l.text}</span>
|
{allOk ? 'AI Engine Active' : 'Agent Registry Synced'}
|
||||||
</li>
|
</span>
|
||||||
);
|
<span className="text-slate-300 text-xs hidden sm:inline">•</span>
|
||||||
})}
|
<span className="text-[11px] text-slate-500 truncate hidden sm:inline">
|
||||||
</ul>
|
{allOk
|
||||||
|
? 'All configured skills and tools are actively wired'
|
||||||
|
: 'Settings are saved to registry; agent runtimes sync automatically'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setExpanded((v) => !v)}
|
||||||
|
className="flex items-center gap-1 text-[11px] font-medium text-slate-500 hover:text-slate-900 px-2 py-1 rounded-md hover:bg-slate-100/80 transition-colors cursor-pointer shrink-0"
|
||||||
|
>
|
||||||
|
<span>{expanded ? 'Hide details' : 'System details'}</span>
|
||||||
|
{expanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Collapsible diagnostic details */}
|
||||||
|
{expanded && (
|
||||||
|
<div className="border-t border-slate-200/80 bg-slate-50/70 px-4 py-3 text-xs">
|
||||||
|
<div className="text-[10px] font-bold uppercase tracking-wider text-slate-400 mb-2">
|
||||||
|
Connection Diagnostics
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-1.5">
|
||||||
|
{lines.map((l) => {
|
||||||
|
const Icon = l.info ? Info : l.ok ? CheckCircle2 : XCircle;
|
||||||
|
const color = l.info ? 'text-slate-400' : l.ok ? 'text-emerald-600' : 'text-amber-500';
|
||||||
|
return (
|
||||||
|
<li key={l.text} className="flex items-start gap-2 leading-relaxed text-slate-600">
|
||||||
|
<Icon className={cn('w-3.5 h-3.5 mt-0.5 shrink-0', color)} />
|
||||||
|
<span className="text-[11px]">{l.text}</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user