feat: add tools and collections management UI to agent detail page
- Add Tools section with search/add/remove functionality - Add Collections section with search/add/remove functionality - Add GET endpoints to /api/agents/[id]/tools and /api/agents/[id]/collections - Use debounced search with dropdown for adding tools/collections - Include click-outside handling to close dropdowns
This commit is contained in:
parent
bfed830187
commit
5a0a21fd60
3 changed files with 581 additions and 1 deletions
|
|
@ -12,6 +12,68 @@ type RouteContext = {
|
|||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/agents/[id]/collections
|
||||
* List collections attached to an agent
|
||||
*/
|
||||
export async function GET(_request: NextRequest, context: RouteContext): Promise<NextResponse> {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
const { id } = await context.params;
|
||||
|
||||
const agent = await prisma.agent.findUnique({
|
||||
where: { id },
|
||||
select: { userId: true, isPublic: true },
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isOwner = session?.user?.id === agent.userId;
|
||||
if (!isOwner && !agent.isPublic) {
|
||||
return NextResponse.json({ success: false, error: 'Access denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
const collections = await prisma.agentCollection.findMany({
|
||||
where: { agentId: id },
|
||||
include: {
|
||||
collection: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
_count: { select: { tools: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { position: 'asc' },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: collections.map((ac) => ({
|
||||
id: ac.id,
|
||||
collectionId: ac.collectionId,
|
||||
position: ac.position,
|
||||
addedAt: ac.addedAt,
|
||||
collection: {
|
||||
id: ac.collection.id,
|
||||
name: ac.collection.name,
|
||||
description: ac.collection.description,
|
||||
toolCount: ac.collection._count.tools,
|
||||
},
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get agent collections:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to get agent collections' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/agents/[id]/collections
|
||||
* Add a collection to an agent
|
||||
|
|
|
|||
|
|
@ -12,6 +12,72 @@ type RouteContext = {
|
|||
params: Promise<{ id: string }>;
|
||||
};
|
||||
|
||||
/**
|
||||
* GET /api/agents/[id]/tools
|
||||
* List tools attached to an agent
|
||||
*/
|
||||
export async function GET(_request: NextRequest, context: RouteContext): Promise<NextResponse> {
|
||||
try {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
const { id } = await context.params;
|
||||
|
||||
const agent = await prisma.agent.findUnique({
|
||||
where: { id },
|
||||
select: { userId: true, isPublic: true },
|
||||
});
|
||||
|
||||
if (!agent) {
|
||||
return NextResponse.json({ success: false, error: 'Agent not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const isOwner = session?.user?.id === agent.userId;
|
||||
if (!isOwner && !agent.isPublic) {
|
||||
return NextResponse.json({ success: false, error: 'Access denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
const tools = await prisma.agentTool.findMany({
|
||||
where: { agentId: id },
|
||||
include: {
|
||||
tool: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
package: {
|
||||
select: {
|
||||
npmPackageName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { position: 'asc' },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: tools.map((at) => ({
|
||||
id: at.id,
|
||||
toolId: at.toolId,
|
||||
position: at.position,
|
||||
addedAt: at.addedAt,
|
||||
tool: {
|
||||
id: at.tool.id,
|
||||
name: at.tool.name,
|
||||
description: at.tool.description,
|
||||
npmPackageName: at.tool.package.npmPackageName,
|
||||
},
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get agent tools:', error);
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Failed to get agent tools' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/agents/[id]/tools
|
||||
* Add an individual tool to an agent
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Button } from '@tpmjs/ui/Button/Button';
|
|||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { AppHeader } from '~/components/AppHeader';
|
||||
|
||||
interface Agent {
|
||||
|
|
@ -27,6 +27,42 @@ interface Agent {
|
|||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface AgentTool {
|
||||
id: string;
|
||||
toolId: string;
|
||||
tool: {
|
||||
id: string;
|
||||
name: string;
|
||||
npmPackageName: string;
|
||||
description: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
interface AgentCollection {
|
||||
id: string;
|
||||
collectionId: string;
|
||||
collection: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
toolCount: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SearchTool {
|
||||
id: string;
|
||||
name: string;
|
||||
npmPackageName: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
interface SearchCollection {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
toolCount: number;
|
||||
}
|
||||
|
||||
const PROVIDER_DISPLAY_NAMES: Record<AIProvider, string> = {
|
||||
OPENAI: 'OpenAI',
|
||||
ANTHROPIC: 'Anthropic',
|
||||
|
|
@ -35,6 +71,7 @@ const PROVIDER_DISPLAY_NAMES: Record<AIProvider, string> = {
|
|||
MISTRAL: 'Mistral',
|
||||
};
|
||||
|
||||
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Agent detail page has multiple interconnected features
|
||||
export default function AgentDetailPage(): React.ReactElement {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
|
|
@ -46,6 +83,22 @@ export default function AgentDetailPage(): React.ReactElement {
|
|||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Tools state
|
||||
const [agentTools, setAgentTools] = useState<AgentTool[]>([]);
|
||||
const [toolSearch, setToolSearch] = useState('');
|
||||
const [toolSearchResults, setToolSearchResults] = useState<SearchTool[]>([]);
|
||||
const [isSearchingTools, setIsSearchingTools] = useState(false);
|
||||
const [showToolSearch, setShowToolSearch] = useState(false);
|
||||
const toolSearchRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Collections state
|
||||
const [agentCollections, setAgentCollections] = useState<AgentCollection[]>([]);
|
||||
const [collectionSearch, setCollectionSearch] = useState('');
|
||||
const [collectionSearchResults, setCollectionSearchResults] = useState<SearchCollection[]>([]);
|
||||
const [isSearchingCollections, setIsSearchingCollections] = useState(false);
|
||||
const [showCollectionSearch, setShowCollectionSearch] = useState(false);
|
||||
const collectionSearchRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
uid: '',
|
||||
|
|
@ -91,10 +144,217 @@ export default function AgentDetailPage(): React.ReactElement {
|
|||
}
|
||||
}, [agentId, router]);
|
||||
|
||||
// Fetch tools attached to agent
|
||||
const fetchAgentTools = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/agents/${agentId}/tools`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setAgentTools(data.data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch agent tools:', err);
|
||||
}
|
||||
}, [agentId]);
|
||||
|
||||
// Fetch collections attached to agent
|
||||
const fetchAgentCollections = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/agents/${agentId}/collections`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setAgentCollections(data.data || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch agent collections:', err);
|
||||
}
|
||||
}, [agentId]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAgent();
|
||||
}, [fetchAgent]);
|
||||
|
||||
useEffect(() => {
|
||||
if (agent) {
|
||||
fetchAgentTools();
|
||||
fetchAgentCollections();
|
||||
}
|
||||
}, [agent, fetchAgentTools, fetchAgentCollections]);
|
||||
|
||||
// Click outside handler for search dropdowns
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (toolSearchRef.current && !toolSearchRef.current.contains(event.target as Node)) {
|
||||
setShowToolSearch(false);
|
||||
}
|
||||
if (
|
||||
collectionSearchRef.current &&
|
||||
!collectionSearchRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setShowCollectionSearch(false);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
// Search tools
|
||||
const searchTools = useCallback(
|
||||
async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
setToolSearchResults([]);
|
||||
return;
|
||||
}
|
||||
setIsSearchingTools(true);
|
||||
try {
|
||||
const response = await fetch(`/api/tools/search?q=${encodeURIComponent(query)}&limit=10`);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Filter out tools already attached
|
||||
const attachedToolIds = new Set(agentTools.map((t) => t.toolId));
|
||||
const filtered = (data.data || []).filter((t: SearchTool) => !attachedToolIds.has(t.id));
|
||||
setToolSearchResults(filtered);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to search tools:', err);
|
||||
} finally {
|
||||
setIsSearchingTools(false);
|
||||
}
|
||||
},
|
||||
[agentTools]
|
||||
);
|
||||
|
||||
// Search collections
|
||||
const searchCollections = useCallback(
|
||||
async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
setCollectionSearchResults([]);
|
||||
return;
|
||||
}
|
||||
setIsSearchingCollections(true);
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/collections?search=${encodeURIComponent(query)}&limit=10`
|
||||
);
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
// Filter out collections already attached
|
||||
const attachedCollectionIds = new Set(agentCollections.map((c) => c.collectionId));
|
||||
const filtered = (data.data || []).filter(
|
||||
(c: SearchCollection) => !attachedCollectionIds.has(c.id)
|
||||
);
|
||||
setCollectionSearchResults(filtered);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to search collections:', err);
|
||||
} finally {
|
||||
setIsSearchingCollections(false);
|
||||
}
|
||||
},
|
||||
[agentCollections]
|
||||
);
|
||||
|
||||
// Debounced search effects
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (toolSearch) searchTools(toolSearch);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [toolSearch, searchTools]);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
if (collectionSearch) searchCollections(collectionSearch);
|
||||
}, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [collectionSearch, searchCollections]);
|
||||
|
||||
// Add tool to agent
|
||||
const addTool = async (toolId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/agents/${agentId}/tools`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ toolId }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await fetchAgentTools();
|
||||
await fetchAgent();
|
||||
setToolSearch('');
|
||||
setToolSearchResults([]);
|
||||
setShowToolSearch(false);
|
||||
} else {
|
||||
alert(data.error || 'Failed to add tool');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to add tool:', err);
|
||||
alert('Failed to add tool');
|
||||
}
|
||||
};
|
||||
|
||||
// Remove tool from agent
|
||||
const removeTool = async (toolId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/agents/${agentId}/tools/${toolId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await fetchAgentTools();
|
||||
await fetchAgent();
|
||||
} else {
|
||||
alert(data.error || 'Failed to remove tool');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to remove tool:', err);
|
||||
alert('Failed to remove tool');
|
||||
}
|
||||
};
|
||||
|
||||
// Add collection to agent
|
||||
const addCollection = async (collectionId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/agents/${agentId}/collections`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ collectionId }),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await fetchAgentCollections();
|
||||
await fetchAgent();
|
||||
setCollectionSearch('');
|
||||
setCollectionSearchResults([]);
|
||||
setShowCollectionSearch(false);
|
||||
} else {
|
||||
alert(data.error || 'Failed to add collection');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to add collection:', err);
|
||||
alert('Failed to add collection');
|
||||
}
|
||||
};
|
||||
|
||||
// Remove collection from agent
|
||||
const removeCollection = async (collectionId: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/agents/${agentId}/collections/${collectionId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
await fetchAgentCollections();
|
||||
await fetchAgent();
|
||||
} else {
|
||||
alert(data.error || 'Failed to remove collection');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to remove collection:', err);
|
||||
alert('Failed to remove collection');
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
|
||||
) => {
|
||||
|
|
@ -285,6 +545,198 @@ export default function AgentDetailPage(): React.ReactElement {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tools Section */}
|
||||
<div className="bg-background border border-border rounded-lg p-6 mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-medium text-foreground">Tools</h2>
|
||||
<span className="text-sm text-foreground-tertiary">{agentTools.length} attached</span>
|
||||
</div>
|
||||
|
||||
{/* Current Tools */}
|
||||
{agentTools.length > 0 && (
|
||||
<div className="space-y-2 mb-4">
|
||||
{agentTools.map((at) => (
|
||||
<div
|
||||
key={at.id}
|
||||
className="flex items-center justify-between p-3 bg-surface-secondary rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<Icon
|
||||
icon="puzzle"
|
||||
size="sm"
|
||||
className="text-foreground-tertiary flex-shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">{at.tool.name}</p>
|
||||
<p className="text-xs text-foreground-tertiary truncate font-mono">
|
||||
{at.tool.npmPackageName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeTool(at.toolId)}
|
||||
className="p-1 text-foreground-tertiary hover:text-red-500 transition-colors flex-shrink-0"
|
||||
title="Remove tool"
|
||||
>
|
||||
<Icon icon="x" size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Tool Search */}
|
||||
<div ref={toolSearchRef} className="relative">
|
||||
<div className="relative">
|
||||
<Icon
|
||||
icon="search"
|
||||
size="xs"
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={toolSearch}
|
||||
onChange={(e) => {
|
||||
setToolSearch(e.target.value);
|
||||
setShowToolSearch(true);
|
||||
}}
|
||||
onFocus={() => setShowToolSearch(true)}
|
||||
placeholder="Search tools to add..."
|
||||
className="w-full pl-9 pr-3 py-2 bg-background border border-border rounded-lg text-foreground text-sm placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
/>
|
||||
{isSearchingTools && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Results Dropdown */}
|
||||
{showToolSearch && toolSearch && (
|
||||
<div className="absolute z-10 w-full mt-1 bg-background border border-border rounded-lg shadow-lg max-h-64 overflow-y-auto">
|
||||
{toolSearchResults.length > 0 ? (
|
||||
toolSearchResults.map((tool) => (
|
||||
<button
|
||||
type="button"
|
||||
key={tool.id}
|
||||
onClick={() => addTool(tool.id)}
|
||||
className="w-full px-3 py-2 text-left hover:bg-surface-secondary transition-colors first:rounded-t-lg last:rounded-b-lg"
|
||||
>
|
||||
<p className="text-sm font-medium text-foreground">{tool.name}</p>
|
||||
<p className="text-xs text-foreground-tertiary font-mono">
|
||||
{tool.npmPackageName}
|
||||
</p>
|
||||
</button>
|
||||
))
|
||||
) : !isSearchingTools ? (
|
||||
<div className="px-3 py-4 text-center text-sm text-foreground-tertiary">
|
||||
No tools found
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collections Section */}
|
||||
<div className="bg-background border border-border rounded-lg p-6 mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-medium text-foreground">Collections</h2>
|
||||
<span className="text-sm text-foreground-tertiary">
|
||||
{agentCollections.length} attached
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Current Collections */}
|
||||
{agentCollections.length > 0 && (
|
||||
<div className="space-y-2 mb-4">
|
||||
{agentCollections.map((ac) => (
|
||||
<div
|
||||
key={ac.id}
|
||||
className="flex items-center justify-between p-3 bg-surface-secondary rounded-lg"
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<Icon
|
||||
icon="folder"
|
||||
size="sm"
|
||||
className="text-foreground-tertiary flex-shrink-0"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium text-foreground truncate">
|
||||
{ac.collection.name}
|
||||
</p>
|
||||
<p className="text-xs text-foreground-tertiary">
|
||||
{ac.collection.toolCount} tools
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeCollection(ac.collectionId)}
|
||||
className="p-1 text-foreground-tertiary hover:text-red-500 transition-colors flex-shrink-0"
|
||||
title="Remove collection"
|
||||
>
|
||||
<Icon icon="x" size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Collection Search */}
|
||||
<div ref={collectionSearchRef} className="relative">
|
||||
<div className="relative">
|
||||
<Icon
|
||||
icon="search"
|
||||
size="xs"
|
||||
className="absolute left-3 top-1/2 -translate-y-1/2 text-foreground-tertiary"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={collectionSearch}
|
||||
onChange={(e) => {
|
||||
setCollectionSearch(e.target.value);
|
||||
setShowCollectionSearch(true);
|
||||
}}
|
||||
onFocus={() => setShowCollectionSearch(true)}
|
||||
placeholder="Search collections to add..."
|
||||
className="w-full pl-9 pr-3 py-2 bg-background border border-border rounded-lg text-foreground text-sm placeholder:text-foreground-tertiary focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
|
||||
/>
|
||||
{isSearchingCollections && (
|
||||
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<div className="w-4 h-4 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search Results Dropdown */}
|
||||
{showCollectionSearch && collectionSearch && (
|
||||
<div className="absolute z-10 w-full mt-1 bg-background border border-border rounded-lg shadow-lg max-h-64 overflow-y-auto">
|
||||
{collectionSearchResults.length > 0 ? (
|
||||
collectionSearchResults.map((collection) => (
|
||||
<button
|
||||
type="button"
|
||||
key={collection.id}
|
||||
onClick={() => addCollection(collection.id)}
|
||||
className="w-full px-3 py-2 text-left hover:bg-surface-secondary transition-colors first:rounded-t-lg last:rounded-b-lg"
|
||||
>
|
||||
<p className="text-sm font-medium text-foreground">{collection.name}</p>
|
||||
<p className="text-xs text-foreground-tertiary">
|
||||
{collection.toolCount} tools
|
||||
</p>
|
||||
</button>
|
||||
))
|
||||
) : !isSearchingCollections ? (
|
||||
<div className="px-3 py-4 text-center text-sm text-foreground-tertiary">
|
||||
No collections found
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuration */}
|
||||
<div className="bg-background border border-border rounded-lg p-6 mb-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue