updates on the agentstudio

This commit is contained in:
2026-09-30 18:05:38 +05:30
parent d33fff01d3
commit 806f4a2b3f
6 changed files with 844 additions and 850 deletions

View File

@@ -2,16 +2,13 @@ import React, { useState } from 'react';
import {
Sparkles,
ChevronDown,
ChevronRight,
LayoutGrid,
Zap,
Wrench,
Pencil,
Users,
Package,
Route,
Layers,
Check,
Bot,
LayoutGrid,
Shield,
Activity,
} from 'lucide-react';
import { cn } from '@/lib/utils';
@@ -23,15 +20,15 @@ const STATUS_TONE = {
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 }) {
const meta = agent.statusMeta;
const meta = agent.statusMeta || { label: agent.status || 'Live', tone: 'emerald', hint: '' };
return (
<span
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}
</span>
@@ -48,279 +45,175 @@ export default function AgentScopeNavigator({
onSelectSkill,
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 currentAgent = agents.find((a) => a.id === activeAgentId) || agents[0];
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 (
<div className="bg-white rounded-2xl border border-slate-200 shadow-xs p-4 flex flex-col space-y-4">
{/* 1. Agent Profile Header + Dropdown Switcher */}
<div className="bg-white rounded-2xl border border-slate-200 shadow-2xs p-4 flex flex-col space-y-4">
{/* 1. Agent Switcher */}
<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
type="button"
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="w-9 h-9 rounded-full bg-brand-tint border border-brand/20 text-brand flex items-center justify-center shrink-0 shadow-2xs">
<Sparkles className="w-4 h-4" />
<div className="w-10 h-10 rounded-xl bg-brand/10 border border-brand/20 text-brand flex items-center justify-center shrink-0">
<Bot className="w-5 h-5 text-brand" />
</div>
<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}
</div>
<div className="flex items-center gap-1.5 text-[10px] font-bold text-slate-400 uppercase tracking-wider">
<span>{currentAgent.badge || 'AGENT'}</span>
<div className="flex items-center gap-1.5 mt-0.5">
<StatusBadge agent={currentAgent} />
<span className="text-[10px] font-medium text-slate-400 truncate">
{currentAgent.runtime === 'engine' ? 'AI Engine' : 'Console'}
</span>
</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>
{/* Agent Switcher Menu */}
{/* Dropdown Menu */}
{agentMenuOpen && (
<>
<div
className="fixed inset-0 z-40 bg-transparent"
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">
Switch Agent Scope
Select Agent
</div>
{agents.map((agent) => (
<button
key={agent.id}
type="button"
onClick={() => {
onSelectAgent(agent.id);
setAgentMenuOpen(false);
}}
className={cn(
'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
? 'bg-brand-tint text-brand'
: '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} />
</span>
{agent.id === activeAgentId && <Check className="w-3.5 h-3.5 shrink-0" />}
</button>
))}
{agents.map((agent) => {
const isSelected = agent.id === activeAgentId;
return (
<button
key={agent.id}
type="button"
onClick={() => {
onSelectAgent(agent.id);
setAgentMenuOpen(false);
}}
className={cn(
'w-full flex items-center justify-between px-3 py-2 rounded-lg text-xs font-semibold text-left transition-colors cursor-pointer',
isSelected
? 'bg-brand/10 text-brand font-bold'
: 'text-slate-700 hover:bg-slate-50'
)}
>
<div className="flex items-center gap-2 min-w-0">
<span className="truncate">{agent.name}</span>
<StatusBadge agent={agent} />
</div>
{isSelected && <Check className="w-3.5 h-3.5 shrink-0 text-brand ml-2" />}
</button>
);
})}
</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) */}
<div className="space-y-2 text-xs">
{/* SURFACES TREE NODE */}
<div>
<button
type="button"
onClick={() => setSurfacesOpen((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">
{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>
)}
{/* 3. Summary Stats */}
<div className="grid grid-cols-2 gap-2">
<div className="p-3 rounded-xl border border-slate-100 bg-slate-50/60 flex flex-col">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
Active Skills
</span>
<div className="flex items-baseline gap-1.5 mt-1">
<span className="text-xl font-bold text-slate-900">{activeSkillsCount}</span>
<span className="text-xs text-slate-400">/ {skills.length}</span>
</div>
</div>
{/* SKILLS TREE NODE */}
<div>
<button
type="button"
onClick={() => setSkillsOpen((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">
{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 className="p-3 rounded-xl border border-slate-100 bg-slate-50/60 flex flex-col">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">
Linked Tools
</span>
<div className="flex items-baseline gap-1.5 mt-1">
<span className="text-xl font-bold text-slate-900">{tools.length}</span>
<span className="text-xs text-slate-400">ready</span>
</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>
);
}

View File

@@ -219,13 +219,21 @@ export default function AgentStudio() {
activeAgent={activeAgent}
onTestSkill={handleTestSkill}
onOpenDetails={(skill) => setDrawerSkillId(skill.id)}
onToggleStatus={handleToggleStatus}
canEdit={canEdit}
onNewSkill={() => setCreateModalOpen(true)}
canCreate={canCreate}
createDisabledReason={createDisabledReason}
/>
)}
{activeTab === 'tools' && <ToolRegistryView tools={model.tools} />}
{activeTab === 'tools' && (
<ToolRegistryView
tools={model.tools}
agentTools={agentTools}
activeAgent={activeAgent}
/>
)}
{activeTab === 'configure' && (
<AgentConfigView

View File

@@ -1,181 +1,142 @@
import React from 'react';
import {
Sparkles,
Play,
ArrowRight,
Pencil,
Users,
Package,
Route,
Check,
Settings2,
Wrench,
Layers,
CircleDot,
ChevronRight,
} from 'lucide-react';
import { cn } from '@/lib/utils';
const CATEGORY_META = {
AUTHORING: {
icon: Pencil,
gradient: 'from-violet-500 to-purple-600',
bgGlow: 'bg-violet-50',
badgeBg: 'bg-violet-100 text-violet-700 border-violet-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 CATEGORY_STYLES = {
AUTHORING: 'bg-purple-50 text-purple-700 border-purple-200',
WORKFORCE: 'bg-blue-50 text-blue-700 border-blue-200',
LOGISTICS: 'bg-emerald-50 text-emerald-700 border-emerald-200',
DISPATCH: 'bg-amber-50 text-amber-700 border-amber-200',
GENERAL: 'bg-slate-50 text-slate-700 border-slate-200',
};
const DEFAULT_META = {
icon: Layers,
gradient: 'from-slate-500 to-slate-600',
bgGlow: 'bg-slate-50',
badgeBg: 'bg-slate-100 text-slate-600 border-slate-200',
accent: 'text-slate-600',
glow: 'hover:shadow-slate-100',
};
export default function SkillCard({ skill, onTest, onDetails }) {
export default function SkillCard({
skill,
onTest,
onDetails,
onToggleStatus,
canEdit = true,
}) {
const isEnabled = skill.enabled !== false;
const meta = CATEGORY_META[(skill.category || '').toUpperCase()] || DEFAULT_META;
const CategoryIcon = meta.icon;
const categoryKey = (skill.category || 'GENERAL').toUpperCase();
const categoryBadgeStyle = CATEGORY_STYLES[categoryKey] || CATEGORY_STYLES.GENERAL;
const toolsCount = Array.isArray(skill.tools) ? skill.tools.length : 0;
return (
<div
className={cn(
'relative bg-white rounded-2xl border border-slate-200/80 shadow-sm transition-all duration-300 overflow-hidden cursor-pointer group',
'hover:shadow-xl hover:shadow-slate-200/60 hover:-translate-y-0.5 hover:border-slate-300',
meta.glow
'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',
isEnabled ? 'border-slate-200' : 'border-slate-200/60 bg-slate-50/40 opacity-80'
)}
>
{/* Top gradient accent bar */}
<div className={cn('h-1 w-full bg-gradient-to-r', meta.gradient)} />
<div className="p-5">
{/* Header row: category badge + status */}
<div className="flex items-center justify-between gap-2 mb-4">
<div
<div>
{/* Top Header: Category Tag & Direct Status Toggle Switch */}
<div className="flex items-center justify-between gap-2 mb-3">
<span
className={cn(
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-bold tracking-wider border',
meta.badgeBg
'px-2.5 py-0.5 rounded-lg text-[10px] font-bold uppercase tracking-wider border',
categoryBadgeStyle
)}
>
<CategoryIcon className="w-3 h-3" />
<span>{(skill.category || 'SKILL').toUpperCase().replace(/_/g, ' ')}</span>
</div>
{categoryKey.replace(/_/g, ' ')}
</span>
<div
className={cn(
'inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[10px] font-bold border transition-colors',
isEnabled
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
: 'bg-slate-100 text-slate-500 border-slate-200'
)}
>
{isEnabled ? (
<>
<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)}>
&ldquo;{skill.prompt}&rdquo;
{/* User-friendly Direct On/Off Toggle */}
<div className="flex items-center gap-2">
<span
className={cn(
'text-[11px] font-semibold transition-colors',
isEnabled ? 'text-emerald-700' : 'text-slate-400'
)}
>
{isEnabled ? 'Active' : 'Disabled'}
</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>
{/* Integration tag */}
{skill.integration && (
<div className="mb-3">
<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 & Plain English Description */}
<div className="mb-3">
<h3 className="text-sm font-bold text-slate-900 leading-snug">
{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" />
{skill.integration}
Shared Board Skill
</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>
{/* 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>
);
}

View File

@@ -1,5 +1,5 @@
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 SkillCard from './SkillCard';
@@ -8,218 +8,190 @@ export default function SkillCardsGrid({
activeAgent,
onTestSkill,
onOpenDetails,
onToggleStatus,
onNewSkill,
canCreate = true,
canEdit = true,
createDisabledReason = '',
}) {
const [scopeTab, setScopeTab] = useState('agent'); // 'agent' or 'board'
const [searchQuery, setSearchQuery] = useState('');
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 scopedSkills = useMemo(() => {
return skills.filter((s) => {
if (scopeTab === 'board') {
return s.scope === 'board';
}
return s.scope !== 'board';
});
}, [skills, scopeTab]);
const hasBoardSkills = skills.some((s) => s.scope === 'board');
// Categories present in the current scope
// Categories present in the skills
const categories = useMemo(() => {
const counts = { ALL: scopedSkills.length };
scopedSkills.forEach((s) => {
const cat = (s.category || 'GENERAL').toUpperCase();
counts[cat] = (counts[cat] || 0) + 1;
const list = new Set();
skills.forEach((s) => {
if (s.category) list.add(s.category.toUpperCase());
});
return counts;
}, [scopedSkills]);
return Array.from(list);
}, [skills]);
// Filter by category and search query
// Filter skills
const filteredSkills = useMemo(() => {
return scopedSkills.filter((s) => {
const matchesCat =
categoryFilter === 'ALL' ||
(s.category || '').toUpperCase() === categoryFilter;
return skills.filter((s) => {
// Category filter
if (categoryFilter !== 'ALL' && (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 matchesQuery =
!q ||
if (!q) return true;
return (
(s.title || '').toLowerCase().includes(q) ||
(s.description || '').toLowerCase().includes(q) ||
(s.category || '').toLowerCase().includes(q) ||
(s.integration || '').toLowerCase().includes(q) ||
(s.prompt || '').toLowerCase().includes(q);
return matchesCat && matchesQuery;
(s.prompt || '').toLowerCase().includes(q)
);
});
}, [scopedSkills, categoryFilter, searchQuery]);
}, [skills, categoryFilter, statusFilter, scopeFilter, searchQuery]);
const enabledCount = filteredSkills.filter((s) => s.enabled !== false).length;
const agentCount = skills.filter((s) => s.scope !== 'board').length;
const boardCount = skills.filter((s) => s.scope === 'board').length;
const activeCount = skills.filter((s) => s.enabled !== false).length;
return (
<div className="space-y-4">
{/* 1. Scope Sub-tabs (Agent Skills / Board Skills) */}
<div className="flex items-center gap-8 border-b border-slate-200/80 text-xs sm:text-sm font-medium">
<button
type="button"
onClick={() => {
setScopeTab('agent');
setCategoryFilter('ALL');
}}
className={cn(
'pb-3 -mb-px transition-all cursor-pointer flex items-center gap-2 border-b-2 select-none',
scopeTab === 'agent'
? 'border-brand text-brand font-bold'
: 'border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300'
{/* 1. Header Toolbar: Search + Filters + New Skill Button */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
{/* Search Input */}
<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" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search skills by name, description or role..."
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"
/>
{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>
)}
>
<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>
</div>
<button
type="button"
onClick={() => {
setScopeTab('board');
setCategoryFilter('ALL');
}}
className={cn(
'pb-3 -mb-px transition-all cursor-pointer flex items-center gap-2 border-b-2 select-none',
scopeTab === 'board'
? 'border-brand text-brand font-bold'
: 'border-transparent text-slate-500 hover:text-slate-900 hover:border-slate-300'
{/* Action Controls */}
<div className="flex items-center gap-2 flex-wrap">
{/* Category Dropdown Filter */}
<div className="relative">
<select
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
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"
>
<option value="ALL">All Categories</option>
{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 */}
<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 && (
{/* New Skill Button */}
<button
type="button"
onClick={() => setSearchQuery('')}
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"
onClick={onNewSkill}
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>
)}
</div>
{/* 3. Category Filter Pills */}
<div className="flex items-center gap-2 overflow-x-auto pb-0.5 no-scrollbar">
{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 ? (
{/* 3. Skill Cards Grid or Empty State */}
{filteredSkills.length === 0 ? (
<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" />
<div className="text-sm font-semibold text-slate-800">No matching capabilities found</div>
<p className="text-xs text-slate-500">
Try adjusting your search query or selecting a different category filter.
<AlertCircle className="w-8 h-8 text-slate-400 mx-auto" />
<div className="text-sm font-bold text-slate-800">No matching skills found</div>
<p className="text-xs text-slate-500 max-w-sm mx-auto">
{searchQuery
? `No skills matched "${searchQuery}". Try a different keyword or reset filters.`
: 'There are no skills configured under the selected filter criteria.'}
</p>
<button
type="button"
onClick={() => {
setSearchQuery('');
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>
</div>
) : (
@@ -230,6 +202,8 @@ export default function SkillCardsGrid({
skill={skill}
onTest={onTestSkill}
onDetails={onOpenDetails}
onToggleStatus={onToggleStatus}
canEdit={canEdit}
/>
))}
</div>

View File

@@ -11,207 +11,262 @@ import {
Bell,
Database,
X,
Hash,
MapPin,
Layers,
Bot,
Info,
} from 'lucide-react';
import { cn } from '@/lib/utils';
/* ─── Kind config ─────────────────────────────────────────────────────────── */
const KIND = {
/* ─── Friendly Tool Dictionary ────────────────────────────────────────────── */
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: {
icon: Eye,
label: 'Read',
card: 'border-sky-200 bg-sky-50/40',
iconWrap: 'bg-sky-100 text-sky-600',
badge: 'bg-sky-100 text-sky-700 border-sky-200',
dot: 'bg-sky-500',
label: 'Lookup',
badge: 'bg-sky-50 text-sky-700 border-sky-200',
},
write: {
icon: Edit3,
label: 'Write',
card: 'border-violet-200 bg-violet-50/30',
iconWrap: 'bg-violet-100 text-violet-600',
badge: 'bg-violet-100 text-violet-700 border-violet-200',
dot: 'bg-violet-500',
label: 'Action',
badge: 'bg-violet-50 text-violet-700 border-violet-200',
},
notify: {
icon: Bell,
label: 'Notify',
card: 'border-amber-200 bg-amber-50/30',
iconWrap: 'bg-amber-100 text-amber-600',
badge: 'bg-amber-100 text-amber-700 border-amber-200',
dot: 'bg-amber-500',
label: 'Alert',
badge: 'bg-amber-50 text-amber-700 border-amber-200',
},
};
const KIND_DEFAULT = {
const DEFAULT_KIND_META = {
icon: Database,
label: 'General',
card: 'border-slate-200 bg-white',
iconWrap: 'bg-slate-100 text-slate-500',
badge: 'bg-slate-100 text-slate-600 border-slate-200',
dot: 'bg-slate-400',
label: 'Action',
badge: 'bg-slate-100 text-slate-700 border-slate-200',
};
function kindOf(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 }) {
function ToolRow({ tool }) {
const [open, setOpen] = useState(false);
const k = kindOf(tool);
const KindIcon = k.icon;
const meta = KIND_META[(tool.kind || '').toLowerCase()] || DEFAULT_KIND_META;
const KindIcon = meta.icon;
const friendlyTitle = formatToolTitle(tool.name);
const friendlyDesc = formatToolDescription(tool);
const hasParams = tool.parameters && tool.parameters.length > 0;
return (
<div
className={cn(
'rounded-2xl border transition-all duration-200 overflow-hidden group',
k.card,
open ? 'shadow-md' : 'shadow-sm hover:shadow-md hover:-translate-y-px'
'rounded-xl border bg-white transition-all duration-150 overflow-hidden shadow-2xs',
open ? 'border-brand/40 shadow-xs' : 'border-slate-200 hover:border-slate-300'
)}
>
{/* Card header — always visible */}
<button
type="button"
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={cn('w-9 h-9 rounded-xl flex items-center justify-center shrink-0 shadow-sm', k.iconWrap)}>
<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 className="flex items-center gap-3 min-w-0">
<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" />
</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>
{/* Expand chevron */}
<ChevronDown
className={cn(
'w-4 h-4 text-slate-400 shrink-0 mt-1 transition-transform duration-200',
open && 'rotate-180 text-brand'
)}
/>
<div className="flex items-center gap-2 shrink-0">
<ChevronDown
className={cn(
'w-4 h-4 text-slate-400 group-hover:text-slate-700 transition-transform duration-200',
open && 'rotate-180 text-brand'
)}
/>
</div>
</button>
{/* Expandable detail panel */}
{open && (
<div className="px-4 pb-4 pt-1 border-t border-black/5 space-y-4">
{/* Meta grid */}
{(tool.target || tool.implementedAt || tool.parentSkill) && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{tool.target && (
<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" />
<div>
<div className="text-[9px] font-bold text-slate-400 uppercase tracking-wider">Touches</div>
<div className="font-mono text-[11px] text-slate-800 mt-0.5">{tool.target}</div>
</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 className="px-4 pb-4 pt-2 border-t border-slate-100 bg-slate-50/70 space-y-3">
<div className="text-xs text-slate-600 leading-relaxed">
<span className="font-semibold text-slate-700">Detailed Action: </span>
{friendlyDesc}
</div>
{tool.parentSkill && (
<div className="text-xs text-slate-600">
<span className="font-semibold text-slate-700">Used by Skills: </span>
<span className="text-brand font-medium">{tool.parentSkill}</span>
</div>
)}
{/* Parameters */}
{hasParams ? (
<div className="space-y-2">
<div className="flex items-center gap-1.5 text-[10px] font-bold text-slate-600 uppercase tracking-wider">
{hasParams && (
<div className="space-y-1.5 pt-1">
<div className="flex items-center gap-1.5 text-[10.5px] font-bold uppercase tracking-wider text-slate-500">
<Code2 className="w-3.5 h-3.5 text-brand" />
Parameters
<span>Required Inputs ({tool.parameters.length})</span>
</div>
<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>
) : (
<p className="text-[11px] text-slate-400 italic">No parameters — input is open-ended.</p>
)}
</div>
)}
@@ -219,131 +274,201 @@ function ToolCard({ tool }) {
);
}
/* ─── Main view ───────────────────────────────────────────────────────────── */
export default function ToolRegistryView({ tools = [] }) {
export default function ToolRegistryView({
tools = [],
agentTools = [],
activeAgent = null,
}) {
const [scope, setScope] = useState('AGENT'); // 'AGENT' | 'ALL'
const [search, setSearch] = useState('');
const [kindFilter, setKindFilter] = useState('all');
const [categoryFilter, setCategoryFilter] = useState('Orders & Dispatch');
const counts = useMemo(() => {
const c = { all: tools.length };
tools.forEach((t) => {
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])];
// If the agent has no linked tools, default to showing all tools
const effectiveScope = scope === 'AGENT' && agentTools.length > 0 ? 'AGENT' : 'ALL';
const baseTools = effectiveScope === 'AGENT' ? agentTools : tools;
const filtered = useMemo(() => {
const q = search.toLowerCase().trim();
return tools.filter((t) => {
const matchesKind = kindFilter === 'all' || (t.kind || 'read').toLowerCase() === kindFilter;
const matchesSearch =
!q ||
t.name.toLowerCase().includes(q) ||
t.description.toLowerCase().includes(q) ||
(t.parentSkill || '').toLowerCase().includes(q) ||
(t.target || '').toLowerCase().includes(q);
return matchesKind && matchesSearch;
});
}, [tools, kindFilter, search]);
return baseTools.filter((t) => {
// In ALL tools mode, filter by selected category unless searching
if (effectiveScope === 'ALL' && !q) {
const cat = TOOL_DICTIONARY[t.name]?.category || 'Orders & Dispatch';
if (cat !== categoryFilter) return false;
}
const gatedCount = tools.filter((t) => t.requiresConfirmation).length;
const openCount = tools.length - gatedCount;
if (!q) return true;
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 (
<div className="space-y-6">
{/* ── Stats row ─────────────────────────────────────────────────── */}
<div className="flex flex-wrap items-stretch gap-3">
<StatChip label="Total Tools" value={tools.length} tone="slate" />
<StatChip label="Read" value={counts.read || 0} tone="sky" />
<StatChip label="Write / Notify" value={(counts.write || 0) + (counts.notify || 0)} tone="amber" />
<StatChip label="Gated" value={gatedCount} tone="amber" />
<StatChip label="Open" value={openCount} tone="emerald" />
<div className="space-y-4">
{/* 1. Scope Selector Tabs: Agent Tools vs All Tools */}
<div className="flex items-center justify-between gap-3 border-b border-slate-200 pb-3 flex-wrap">
<div className="flex items-center gap-2 bg-slate-100 p-1 rounded-xl">
<button
type="button"
onClick={() => {
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 */}
<div className="flex-1 min-w-[200px] flex flex-col justify-center pl-2">
<h3 className="text-sm font-bold text-slate-900 font-heading flex items-center gap-2">
<Wrench className="w-4 h-4 text-brand" />
Agent Tool Registry
</h3>
<p className="text-[11px] text-slate-500 mt-0.5 leading-relaxed">
Every capability a skill may call. Write &amp; notify tools need operator confirmation unless the agent is autonomous.
</p>
<button
type="button"
onClick={() => {
setScope('ALL');
setSearch('');
setCategoryFilter('Orders & Dispatch');
}}
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 === '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>
{/* ── Search + Kind filter ───────────────────────────────────────── */}
<div className="flex flex-col sm:flex-row gap-3">
{/* Search */}
{/* 2. Search & Category Filters */}
<div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-3">
{/* Search Input */}
<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" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search by name, target system, or description…"
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"
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-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 && (
<button
type="button"
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" />
</button>
)}
</div>
{/* Kind filter tabs */}
<div className="flex items-center gap-1 bg-slate-100 rounded-xl p-1 shrink-0">
{kindTabs.map((k) => {
const meta = KIND[k];
const isActive = kindFilter === k;
return (
<button
key={k}
type="button"
onClick={() => setKindFilter(k)}
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
{/* Category Pills (Without 'All' filter) */}
{effectiveScope === 'ALL' && (
<div className="flex items-center gap-1.5 overflow-x-auto pb-0.5">
{CATEGORIES.map((cat) => {
const isSelected = categoryFilter === cat;
return (
<button
key={cat}
type="button"
onClick={() => setCategoryFilter(cat)}
className={cn(
'text-[10px] font-bold',
isActive ? 'text-slate-600' : 'text-slate-400'
'px-3 py-1.5 rounded-lg text-xs font-semibold whitespace-nowrap cursor-pointer transition-all border',
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}
</span>
</button>
);
})}
</div>
{cat}
</button>
);
})}
</div>
)}
</div>
{/* ── Tool Cards ────────────────────────────────────────────────── */}
{filtered.length === 0 ? (
<div className="p-16 rounded-2xl border border-dashed border-slate-200 bg-slate-50 text-center">
<Wrench className="w-8 h-8 text-slate-300 mx-auto mb-3" />
<p className="text-sm font-semibold text-slate-500">No tools match your filter</p>
{/* 3. Result Count */}
<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">{filtered.length}</span>
<span>
{effectiveScope === 'ALL' && !search
? ` tools in ${categoryFilter}`
: ` of ${baseTools.length} ${effectiveScope === 'AGENT' ? `${agentName} tools` : 'tools'}`}
</span>
</div>
{search && (
<button
type="button"
onClick={() => { setSearch(''); setKindFilter('all'); }}
className="mt-3 text-xs text-brand font-semibold hover:underline cursor-pointer"
onClick={() => setSearch('')}
className="text-xs text-brand font-medium hover:underline cursor-pointer"
>
Clear filters
Clear search
</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 className="grid grid-cols-1 lg:grid-cols-2 gap-3">
{filtered.map((tool) => <ToolCard key={tool.name} tool={tool} />)}
<div className="space-y-2">
{filtered.map((tool) => (
<ToolRow key={tool.name} tool={tool} />
))}
</div>
)}
</div>

View File

@@ -1,14 +1,12 @@
import React from 'react';
import { AlertTriangle, CheckCircle2, Info, XCircle } from 'lucide-react';
import React, { useState } from 'react';
import { AlertTriangle, CheckCircle2, Info, XCircle, ChevronDown, ChevronUp, Cpu } from 'lucide-react';
import { useAiStatus } from '@/lib/doormileHooks';
import { cn } from '@/lib/utils';
/**
* What this page's settings actually drive, from GET /admin/ai/status — not a
* fixed note. It used to say "Half wired" forever, including after AI_engine
* and the backend were deployed, because nothing checked.
*
* 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.
* What this page's settings actually drive, from GET /admin/ai/status.
* Designed with a clean, unobtrusive status bar that can be expanded
* for detailed diagnostics when needed.
*/
function ago(iso) {
@@ -49,11 +47,13 @@ export function wiringLines(status) {
export default function WiringStatusBanner() {
const { data, isLoading, isError, error } = useAiStatus();
const [expanded, setExpanded] = useState(false);
if (isLoading) {
return (
<div role="note" className="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3.5 text-xs text-slate-500">
Checking what is connected…
<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">
<Cpu className="w-3.5 h-3.5 animate-pulse text-slate-400" />
<span>Checking system connectivity…</span>
</div>
);
}
@@ -61,17 +61,15 @@ export default function WiringStatusBanner() {
if (isError) {
const notDeployed = error?.response?.status === 404;
return (
<div
role="note"
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 mt-0.5 shrink-0 text-amber-500" />
<p className="leading-relaxed">
<span className="font-bold text-amber-800">Connection status unavailable.</span>{' '}
{notDeployed
? 'This API server does not report agent status yet — deploy the current doormile_backend build. Changes here are still saved and audited.'
: 'The status check failed; it will retry shortly. Changes here are still saved and audited.'}
</p>
<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">
<div className="flex items-center gap-2 min-w-0">
<AlertTriangle className="w-4 h-4 shrink-0 text-amber-500" />
<span className="font-medium truncate">
{notDeployed
? 'API status reporting is starting up. Changes made here are saved safely.'
: 'Status check retrying shortly. Changes made here are saved safely.'}
</span>
</div>
</div>
);
}
@@ -81,28 +79,63 @@ export default function WiringStatusBanner() {
return (
<div
role="note"
className={
className={cn(
'rounded-xl border transition-all duration-200 shadow-2xs overflow-hidden',
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'
: '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-emerald-200/90 bg-emerald-50/40 text-emerald-900'
: 'border-slate-200 bg-white text-slate-700'
)}
>
<p className={`font-bold mb-1.5 ${allOk ? 'text-emerald-800' : 'text-amber-800'}`}>
{allOk ? 'All connected.' : 'Partly connected.'}
</p>
<ul className="space-y-1">
{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-600';
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>{l.text}</span>
</li>
);
})}
</ul>
{/* Sleek top summary bar */}
<div className="flex items-center justify-between px-3.5 py-2 gap-3">
<div className="flex items-center gap-2.5 min-w-0">
<span
className={cn(
'w-2 h-2 rounded-full shrink-0',
allOk ? 'bg-emerald-500 ring-2 ring-emerald-200' : 'bg-amber-400 ring-2 ring-amber-100'
)}
/>
<span className="text-xs font-semibold text-slate-800">
{allOk ? 'AI Engine Active' : 'Agent Registry Synced'}
</span>
<span className="text-slate-300 text-xs hidden sm:inline">•</span>
<span className="text-[11px] text-slate-500 truncate hidden sm:inline">
{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>
);
}