update agents skill design
This commit is contained in:
564
src/pages/admin/WorkspaceAgents.jsx
Normal file
564
src/pages/admin/WorkspaceAgents.jsx
Normal file
@@ -0,0 +1,564 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ArchiveRestore, Archive, Copy, LayoutGrid, List, MoreHorizontal,
|
||||
Plus, RotateCcw, Send, Settings2, Trash2,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Alert, Badge, Button, ConfirmModal, DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
||||
DropdownMenuSeparator, DropdownMenuTrigger, EmptyState, SearchInput, SegmentedToggle, toast,
|
||||
} from '@/components/ds';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useAgents } from '@/lib/agents/useAgents';
|
||||
import { searchAgents } from '@/lib/agents/registry';
|
||||
import { agentIconFor } from '@/components/agents/icons';
|
||||
import OwliverAvatar from '@/components/krow/OwliverAvatar';
|
||||
import { AdminPage } from '@/pages/admin/_shell';
|
||||
|
||||
/**
|
||||
* Agents — the workspace management list.
|
||||
*
|
||||
* Modern, clean, and high-density agent directory with streamlined
|
||||
* status filter tabs, no redundant versioning labels, and full-width layout.
|
||||
*/
|
||||
|
||||
const STATUS_TONE = { published: 'success', draft: 'neutral', archived: 'warning' };
|
||||
|
||||
function AgentGlyph({ agent, className = 'h-10 w-10' }) {
|
||||
const Icon = agentIconFor(agent.icon);
|
||||
if (!Icon) return <OwliverAvatar className={className} rounded="rounded-xl" />;
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex shrink-0 items-center justify-center rounded-xl border border-border/80 bg-surface-subtle shadow-2xs transition-colors group-hover:border-krow-blue/40 group-hover:bg-krow-blue-tint/30',
|
||||
className
|
||||
)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon className="h-5 w-5 text-krow-blue" />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Card representation for Grid view */
|
||||
function AgentCard({ agent, shipped, overridden, onOpen, onAction }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex flex-col justify-between rounded-2xl border border-border bg-surface p-5 shadow-xs',
|
||||
'transition-all duration-200 ease-out hover:border-krow-blue/40 hover:shadow-md'
|
||||
)}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
{/* Top row: Glyph + Title/Badges + Menu */}
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="flex items-start gap-3 min-w-0 flex-1">
|
||||
<AgentGlyph agent={agent} className="h-10 w-10" />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className="font-heading text-body font-semibold text-ink-1 hover:text-krow-blue transition-colors
|
||||
text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-krow-blue/50 rounded truncate"
|
||||
>
|
||||
{agent.name}
|
||||
</button>
|
||||
<Badge variant={STATUS_TONE[agent.status] || 'neutral'} size="sm" className="shrink-0 capitalize">
|
||||
{agent.status}
|
||||
</Badge>
|
||||
{shipped && overridden && (
|
||||
<Badge variant="info" size="sm" className="shrink-0">Customized</Badge>
|
||||
)}
|
||||
{!shipped && <Badge variant="neutral" size="sm" className="shrink-0">Custom</Badge>}
|
||||
</div>
|
||||
|
||||
{agent.description && (
|
||||
<p className="mt-1 text-body-sm leading-relaxed text-ink-3 line-clamp-2">
|
||||
{agent.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 shrink-0 text-ink-4 hover:text-ink-1 hover:bg-surface-sunken rounded-lg"
|
||||
aria-label={`Actions for ${agent.name}`}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={onOpen} className="cursor-pointer">
|
||||
<Settings2 className="mr-2 h-3.5 w-3.5" /> Configure
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAction('duplicate', agent)} className="cursor-pointer">
|
||||
<Copy className="mr-2 h-3.5 w-3.5" /> Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{agent.status !== 'published' && (
|
||||
<DropdownMenuItem onClick={() => onAction('publish', agent)} className="cursor-pointer">
|
||||
<Send className="mr-2 h-3.5 w-3.5" /> Publish
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{agent.status === 'published' && (
|
||||
<DropdownMenuItem onClick={() => onAction('republish', agent)} className="cursor-pointer">
|
||||
<Send className="mr-2 h-3.5 w-3.5" /> Publish update
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{agent.status !== 'archived' ? (
|
||||
<DropdownMenuItem onClick={() => onAction('archive', agent)} className="cursor-pointer">
|
||||
<Archive className="mr-2 h-3.5 w-3.5" /> Archive
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={() => onAction('restore', agent)} className="cursor-pointer">
|
||||
<ArchiveRestore className="mr-2 h-3.5 w-3.5" /> Restore as draft
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(overridden || !shipped) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onAction('remove', agent)} className="cursor-pointer text-destructive focus:text-destructive">
|
||||
{shipped
|
||||
? <><RotateCcw className="mr-2 h-3.5 w-3.5" /> Revert to shipped</>
|
||||
: <><Trash2 className="mr-2 h-3.5 w-3.5" /> Delete</>}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* When to use context */}
|
||||
{agent.trigger && (
|
||||
<div className="rounded-xl border border-border/60 bg-surface-subtle/60 px-3 py-2 text-[12px] text-ink-3">
|
||||
<span className="font-semibold text-ink-2">Trigger: </span>
|
||||
<span>{agent.trigger}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Card bottom */}
|
||||
<div className="mt-4 pt-3.5 border-t border-border/60 space-y-2.5">
|
||||
<div className="flex flex-wrap items-center gap-1.5 text-[11px]">
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-surface-subtle border border-border/60 px-2 py-0.5 font-medium text-ink-3">
|
||||
<span className="text-ink-4">Skills</span>
|
||||
<strong className="font-bold text-ink-1 tabular-nums">{agent.skills.length}</strong>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-surface-subtle border border-border/60 px-2 py-0.5 font-medium text-ink-3">
|
||||
<span className="text-ink-4">Pages</span>
|
||||
<strong className="font-bold text-ink-1 tabular-nums">{agent.pages.length}</strong>
|
||||
</span>
|
||||
{agent.subagents.length > 0 && (
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-surface-subtle border border-border/60 px-2 py-0.5 font-medium text-ink-3">
|
||||
<span className="text-ink-4">Subagents</span>
|
||||
<strong className="font-bold text-ink-1 tabular-nums">{agent.subagents.length}</strong>
|
||||
</span>
|
||||
)}
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-surface-subtle border border-border/60 px-2 py-0.5 font-medium text-ink-3">
|
||||
<span className="text-ink-4">Knowledge</span>
|
||||
<strong className="font-bold text-ink-1 tabular-nums">{agent.knowledge.length}</strong>
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-surface-subtle border border-border/60 px-2 py-0.5 font-medium text-ink-3">
|
||||
<span className="text-ink-4">Reasoning</span>
|
||||
<strong className="font-bold capitalize text-ink-1">{agent.reasoning}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{agent.pages.map((page) => (
|
||||
<span
|
||||
key={page}
|
||||
className="rounded-md border border-border/80 bg-surface-subtle px-2 py-0.5 text-[11px] font-medium text-ink-3"
|
||||
>
|
||||
{page}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** Directory Table Row for List view */
|
||||
function AgentTableRow({ agent, shipped, overridden, onOpen, onAction }) {
|
||||
return (
|
||||
<tr className="group border-b border-border/60 transition-colors hover:bg-surface-subtle/50">
|
||||
{/* Agent Identity */}
|
||||
<td className="py-3.5 pl-4 pr-3 align-middle">
|
||||
<div className="flex items-center gap-3">
|
||||
<AgentGlyph agent={agent} className="h-9 w-9" />
|
||||
<div className="min-w-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className="font-heading text-body-sm font-semibold text-ink-1 hover:text-krow-blue transition-colors text-left focus-visible:outline-none"
|
||||
>
|
||||
{agent.name}
|
||||
</button>
|
||||
{agent.description && (
|
||||
<p className="mt-0.5 text-caption text-ink-3 line-clamp-1 max-w-md">
|
||||
{agent.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Status */}
|
||||
<td className="px-3 py-3.5 align-middle whitespace-nowrap">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Badge variant={STATUS_TONE[agent.status] || 'neutral'} size="sm" className="capitalize">
|
||||
{agent.status}
|
||||
</Badge>
|
||||
{shipped && overridden && (
|
||||
<Badge variant="info" size="sm">Customized</Badge>
|
||||
)}
|
||||
{!shipped && <Badge variant="neutral" size="sm">Custom</Badge>}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Surfaces / Pages */}
|
||||
<td className="px-3 py-3.5 align-middle">
|
||||
<div className="flex flex-wrap gap-1 max-w-xs">
|
||||
{agent.pages.slice(0, 3).map((page) => (
|
||||
<span
|
||||
key={page}
|
||||
className="rounded-md border border-border/80 bg-surface-subtle px-2 py-0.5 text-[11px] font-medium text-ink-3"
|
||||
>
|
||||
{page}
|
||||
</span>
|
||||
))}
|
||||
{agent.pages.length > 3 && (
|
||||
<span className="rounded-md border border-border/60 bg-surface-subtle px-1.5 py-0.5 text-[11px] font-medium text-ink-4">
|
||||
+{agent.pages.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Capabilities */}
|
||||
<td className="px-3 py-3.5 align-middle whitespace-nowrap">
|
||||
<div className="flex items-center gap-2 text-caption text-ink-3">
|
||||
<span className="rounded bg-surface-subtle border border-border/60 px-2 py-0.5 font-medium text-ink-2">
|
||||
<strong className="font-semibold tabular-nums text-ink-1">{agent.skills.length}</strong> skills
|
||||
</span>
|
||||
<span className="rounded bg-surface-subtle border border-border/60 px-2 py-0.5 font-medium capitalize text-ink-2">
|
||||
{agent.reasoning}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Actions */}
|
||||
<td className="py-3.5 pl-3 pr-4 align-middle text-right whitespace-nowrap">
|
||||
<div className="inline-flex items-center gap-1.5">
|
||||
<Button variant="outline" size="sm" onClick={onOpen} className="h-8 text-body-sm font-medium">
|
||||
<Settings2 className="mr-1.5 h-3.5 w-3.5" /> Configure
|
||||
</Button>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 text-ink-4 hover:text-ink-1 rounded-lg"
|
||||
aria-label={`Actions for ${agent.name}`}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={onOpen} className="cursor-pointer">
|
||||
<Settings2 className="mr-2 h-3.5 w-3.5" /> Configure
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => onAction('duplicate', agent)} className="cursor-pointer">
|
||||
<Copy className="mr-2 h-3.5 w-3.5" /> Duplicate
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{agent.status !== 'published' && (
|
||||
<DropdownMenuItem onClick={() => onAction('publish', agent)} className="cursor-pointer">
|
||||
<Send className="mr-2 h-3.5 w-3.5" /> Publish
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{agent.status === 'published' && (
|
||||
<DropdownMenuItem onClick={() => onAction('republish', agent)} className="cursor-pointer">
|
||||
<Send className="mr-2 h-3.5 w-3.5" /> Publish update
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{agent.status !== 'archived' ? (
|
||||
<DropdownMenuItem onClick={() => onAction('archive', agent)} className="cursor-pointer">
|
||||
<Archive className="mr-2 h-3.5 w-3.5" /> Archive
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem onClick={() => onAction('restore', agent)} className="cursor-pointer">
|
||||
<ArchiveRestore className="mr-2 h-3.5 w-3.5" /> Restore as draft
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(overridden || !shipped) && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => onAction('remove', agent)} className="cursor-pointer text-destructive focus:text-destructive">
|
||||
{shipped
|
||||
? <><RotateCcw className="mr-2 h-3.5 w-3.5" /> Revert to shipped</>
|
||||
: <><Trash2 className="mr-2 h-3.5 w-3.5" /> Delete</>}
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdminWorkspaceAgents() {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
agents, diagnostics, saving, isShipped, isOverridden,
|
||||
publish, archive, restore, duplicate, remove,
|
||||
} = useAgents();
|
||||
|
||||
const [query, setQuery] = useState('');
|
||||
const [status, setStatus] = useState('all');
|
||||
const [view, setView] = useState(() => {
|
||||
try {
|
||||
return localStorage.getItem('krow_agents_view') || 'list';
|
||||
} catch {
|
||||
return 'list';
|
||||
}
|
||||
});
|
||||
const [confirming, setConfirming] = useState(null);
|
||||
|
||||
const handleViewChange = (newView) => {
|
||||
setView(newView);
|
||||
try {
|
||||
localStorage.setItem('krow_agents_view', newView);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
|
||||
const visible = useMemo(() => {
|
||||
const found = searchAgents(agents, query);
|
||||
return status === 'all' ? found : found.filter((a) => a.status === status);
|
||||
}, [agents, query, status]);
|
||||
|
||||
const publishedCount = agents.filter((a) => a.status === 'published').length;
|
||||
const draftCount = agents.filter((a) => a.status === 'draft').length;
|
||||
const archivedCount = agents.filter((a) => a.status === 'archived').length;
|
||||
|
||||
/** Runs a lifecycle action and reports honestly when it is refused. */
|
||||
const run = async (action, agent) => {
|
||||
const fn = { publish, republish: publish, archive, restore, duplicate, remove }[action];
|
||||
const result = await fn(agent.id);
|
||||
if (result?.conflict) toast.error(result.conflict.message);
|
||||
else if (result?.error) toast.error(result.error);
|
||||
else if (action === 'duplicate' && result?.id) navigate(`/admin/workspace/agents/${result.id}`);
|
||||
};
|
||||
|
||||
const onAction = (action, agent) => {
|
||||
/* Destructive and irreversible-looking actions confirm; the rest run. */
|
||||
if (action === 'remove' || action === 'archive') {
|
||||
setConfirming({ action, agent });
|
||||
return;
|
||||
}
|
||||
run(action, agent);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<AdminPage
|
||||
title="Agents"
|
||||
subtitle="The agents Owliver can answer as, what each one covers, and what it may do."
|
||||
meta={`${agents.length} registered · ${publishedCount} published`}
|
||||
actions={
|
||||
<Button onClick={() => navigate('/admin/workspace/agents/new')}>
|
||||
<Plus className="h-4 w-4" /> Create agent
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{diagnostics.length > 0 && (
|
||||
<Alert tone="warning" title={`${diagnostics.length} definition problem${diagnostics.length === 1 ? '' : 's'}`} className="py-2.5 px-3.5">
|
||||
<ul className="mt-1 space-y-0.5 text-body-sm">
|
||||
{diagnostics.slice(0, 5).map((d) => (
|
||||
<li key={`${d.kind}-${d.agentId}-${d.message}`}>{d.message}</li>
|
||||
))}
|
||||
</ul>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Unified horizontal management toolbar with clean filter pills */}
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="w-full sm:w-72">
|
||||
<SearchInput value={query} onChange={setQuery} placeholder="Search agents..." />
|
||||
</div>
|
||||
|
||||
{/* Modern status filter tabs */}
|
||||
<div className="inline-flex items-center rounded-xl border border-border/80 bg-surface-subtle/80 p-1 text-caption">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatus('all')}
|
||||
className={cn(
|
||||
'rounded-lg px-3 py-1 font-medium transition-all',
|
||||
status === 'all'
|
||||
? 'bg-surface text-ink-1 shadow-2xs font-semibold'
|
||||
: 'text-ink-3 hover:text-ink-1'
|
||||
)}
|
||||
>
|
||||
All <span className="text-ink-4 font-normal">({agents.length})</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatus('published')}
|
||||
className={cn(
|
||||
'rounded-lg px-3 py-1 font-medium transition-all',
|
||||
status === 'published'
|
||||
? 'bg-surface text-emerald-700 shadow-2xs font-semibold'
|
||||
: 'text-ink-3 hover:text-ink-1'
|
||||
)}
|
||||
>
|
||||
Published <span className="text-ink-4 font-normal">({publishedCount})</span>
|
||||
</button>
|
||||
{draftCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatus('draft')}
|
||||
className={cn(
|
||||
'rounded-lg px-3 py-1 font-medium transition-all',
|
||||
status === 'draft'
|
||||
? 'bg-surface text-ink-1 shadow-2xs font-semibold'
|
||||
: 'text-ink-3 hover:text-ink-1'
|
||||
)}
|
||||
>
|
||||
Drafts <span className="text-ink-4 font-normal">({draftCount})</span>
|
||||
</button>
|
||||
)}
|
||||
{archivedCount > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStatus('archived')}
|
||||
className={cn(
|
||||
'rounded-lg px-3 py-1 font-medium transition-all',
|
||||
status === 'archived'
|
||||
? 'bg-surface text-amber-700 shadow-2xs font-semibold'
|
||||
: 'text-ink-3 hover:text-ink-1'
|
||||
)}
|
||||
>
|
||||
Archived <span className="text-ink-4 font-normal">({archivedCount})</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
to="/admin/workspace/skills"
|
||||
className="hidden md:inline-flex items-center gap-1.5 rounded-lg border border-border/70 bg-surface px-3 py-1.5 text-body-sm text-ink-3 transition-colors hover:border-krow-blue/40 hover:text-krow-blue hover:bg-surface-subtle"
|
||||
>
|
||||
<span>Skills managed in</span>
|
||||
<span className="font-semibold text-krow-blue">Workspace → Skills</span>
|
||||
</Link>
|
||||
|
||||
<SegmentedToggle
|
||||
value={view}
|
||||
onChange={handleViewChange}
|
||||
variant="surface"
|
||||
size="sm"
|
||||
options={[
|
||||
{ value: 'list', icon: List, ariaLabel: 'Directory table view' },
|
||||
{ value: 'grid', icon: LayoutGrid, ariaLabel: 'Grid cards view' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{visible.length === 0 ? (
|
||||
<EmptyState
|
||||
title={agents.length ? 'No agents match that' : 'No agents yet'}
|
||||
description={
|
||||
agents.length
|
||||
? 'Try a different search, or clear the status filter.'
|
||||
: 'An agent decides which skills Owliver can use on a page. Create one to get started.'
|
||||
}
|
||||
/>
|
||||
) : view === 'list' ? (
|
||||
/* Clean Modern Directory Table View */
|
||||
<div className="overflow-hidden rounded-2xl border border-border bg-surface shadow-xs">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-border bg-surface-subtle/60 text-[11px] font-semibold uppercase tracking-wider text-ink-4">
|
||||
<th className="py-3 pl-4 pr-3 font-semibold">Agent</th>
|
||||
<th className="px-3 py-3 font-semibold">Status</th>
|
||||
<th className="px-3 py-3 font-semibold">Surfaces Covered</th>
|
||||
<th className="px-3 py-3 font-semibold">Capabilities</th>
|
||||
<th className="py-3 pl-3 pr-4 text-right font-semibold">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{visible.map((agent) => (
|
||||
<AgentTableRow
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
shipped={isShipped(agent.id)}
|
||||
overridden={isOverridden(agent.id)}
|
||||
onOpen={() => navigate(`/admin/workspace/agents/${agent.id}`)}
|
||||
onAction={onAction}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Modern Grid View */
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{visible.map((agent) => (
|
||||
<AgentCard
|
||||
key={agent.id}
|
||||
agent={agent}
|
||||
shipped={isShipped(agent.id)}
|
||||
overridden={isOverridden(agent.id)}
|
||||
onOpen={() => navigate(`/admin/workspace/agents/${agent.id}`)}
|
||||
onAction={onAction}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
open={Boolean(confirming)}
|
||||
onOpenChange={(open) => !open && setConfirming(null)}
|
||||
title={
|
||||
confirming?.action === 'archive'
|
||||
? `Archive ${confirming?.agent.name}?`
|
||||
: isShipped(confirming?.agent?.id) ? 'Revert to the shipped definition?' : `Delete ${confirming?.agent.name}?`
|
||||
}
|
||||
description={
|
||||
confirming?.action === 'archive'
|
||||
? 'It stops answering and disappears from the switcher. Its definition is kept, and it can be restored as a draft.'
|
||||
: isShipped(confirming?.agent?.id)
|
||||
? 'Your changes to this agent are discarded and the shipped definition takes over again.'
|
||||
: 'This definition is removed. It cannot be recovered.'
|
||||
}
|
||||
confirmLabel={confirming?.action === 'archive' ? 'Archive' : 'Confirm'}
|
||||
busy={saving}
|
||||
onConfirm={async () => {
|
||||
const pending = confirming;
|
||||
setConfirming(null);
|
||||
if (pending) await run(pending.action, pending.agent);
|
||||
}}
|
||||
/>
|
||||
</AdminPage>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user