570 lines
25 KiB
JavaScript
570 lines
25 KiB
JavaScript
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 '@/components/admin/PageShell';
|
|
|
|
/**
|
|
* 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}
|
|
/* On a phone the identity column claims the width of the name
|
|
rather than stacking "Control Center Agent" three words tall,
|
|
which it can afford because the table already scrolls inside
|
|
its own container there. Released at `sm`, so every wider
|
|
layout distributes its columns exactly as it did before. */
|
|
className="whitespace-nowrap sm:whitespace-normal 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 min-w-[48rem] 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>
|
|
);
|
|
}
|