From 9e1fe2be43f07f36747acbd09a5c51cf73f4f84a Mon Sep 17 00:00:00 2001 From: Ajax Davis Date: Wed, 14 Jan 2026 07:12:02 +1000 Subject: [PATCH] revert: restore table layout for agents and collections pages --- apps/web/src/app/dashboard/agents/page.tsx | 445 +++++++---------- .../src/app/dashboard/collections/page.tsx | 468 ++++++------------ 2 files changed, 312 insertions(+), 601 deletions(-) diff --git a/apps/web/src/app/dashboard/agents/page.tsx b/apps/web/src/app/dashboard/agents/page.tsx index dace4c3..d4dcf2a 100644 --- a/apps/web/src/app/dashboard/agents/page.tsx +++ b/apps/web/src/app/dashboard/agents/page.tsx @@ -1,12 +1,21 @@ 'use client'; import type { AIProvider } from '@tpmjs/types/agent'; +import { Badge } from '@tpmjs/ui/Badge/Badge'; import { Button } from '@tpmjs/ui/Button/Button'; import { Icon } from '@tpmjs/ui/Icon/Icon'; -import { Input } from '@tpmjs/ui/Input/Input'; +import { + Table, + TableBody, + TableCell, + TableEmpty, + TableHead, + TableHeader, + TableRow, +} from '@tpmjs/ui/Table/Table'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { DashboardLayout } from '~/components/dashboard/DashboardLayout'; interface Agent { @@ -22,166 +31,29 @@ interface Agent { updatedAt: string; } -const PROVIDER_INFO: Record = { - OPENAI: { name: 'OpenAI', color: 'text-emerald-600 dark:text-emerald-400', bgColor: 'bg-emerald-500/10' }, - ANTHROPIC: { name: 'Anthropic', color: 'text-orange-600 dark:text-orange-400', bgColor: 'bg-orange-500/10' }, - GOOGLE: { name: 'Google', color: 'text-blue-600 dark:text-blue-400', bgColor: 'bg-blue-500/10' }, - GROQ: { name: 'Groq', color: 'text-purple-600 dark:text-purple-400', bgColor: 'bg-purple-500/10' }, - MISTRAL: { name: 'Mistral', color: 'text-red-600 dark:text-red-400', bgColor: 'bg-red-500/10' }, +const PROVIDER_DISPLAY_NAMES: Record = { + OPENAI: 'OpenAI', + ANTHROPIC: 'Anthropic', + GOOGLE: 'Google', + GROQ: 'Groq', + MISTRAL: 'Mistral', }; -function formatRelativeDate(dateString: string): string { +const PROVIDER_COLORS: Record = { + OPENAI: 'default', + ANTHROPIC: 'secondary', + GOOGLE: 'outline', + GROQ: 'outline', + MISTRAL: 'outline', +}; + +function formatDate(dateString: string): string { const date = new Date(dateString); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) { - const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); - if (diffHours === 0) { - const diffMinutes = Math.floor(diffMs / (1000 * 60)); - return diffMinutes <= 1 ? 'Just now' : `${diffMinutes}m ago`; - } - return `${diffHours}h ago`; - } - if (diffDays === 1) return 'Yesterday'; - if (diffDays < 7) return `${diffDays}d ago`; - if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); -} - -function AgentCard({ - agent, - onDelete, - isDeleting, -}: { - agent: Agent; - onDelete: (id: string) => void; - isDeleting: boolean; -}) { - const router = useRouter(); - const providerInfo = PROVIDER_INFO[agent.provider]; - const totalTools = agent.toolCount + agent.collectionCount; - - return ( -
router.push(`/dashboard/agents/${agent.id}`)} - > - {/* Header */} -
-
-
- -
-
-

- {agent.name} -

-
- - {providerInfo.name} - - - - {agent.modelId} - -
-
-
- - {formatRelativeDate(agent.updatedAt)} - -
- - {/* Description */} - {agent.description ? ( -

- {agent.description} -

- ) : ( -

- No description -

- )} - - {/* Stats */} -
-
- - - {totalTools} tool{totalTools !== 1 ? 's' : ''} - -
- {agent.collectionCount > 0 && ( -
- - - {agent.collectionCount} collection{agent.collectionCount !== 1 ? 's' : ''} - -
- )} -
- - {/* Actions */} -
- e.stopPropagation()} - className="flex-1" - > - - - e.stopPropagation()} - > - - - -
-
- ); -} - -function AgentCardSkeleton() { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); } export default function AgentsPage(): React.ReactElement { @@ -190,7 +62,6 @@ export default function AgentsPage(): React.ReactElement { const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [deletingId, setDeletingId] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); const fetchAgents = useCallback(async () => { try { @@ -218,19 +89,8 @@ export default function AgentsPage(): React.ReactElement { fetchAgents(); }, [fetchAgents]); - const filteredAgents = useMemo(() => { - if (!searchQuery.trim()) return agents; - const query = searchQuery.toLowerCase(); - return agents.filter( - (agent) => - agent.name.toLowerCase().includes(query) || - agent.description?.toLowerCase().includes(query) || - agent.modelId.toLowerCase().includes(query) || - PROVIDER_INFO[agent.provider].name.toLowerCase().includes(query) - ); - }, [agents, searchQuery]); - - const handleDelete = async (id: string) => { + const handleDelete = async (id: string, e: React.MouseEvent) => { + e.stopPropagation(); if (!confirm('Are you sure you want to delete this agent? This action cannot be undone.')) { return; } @@ -270,14 +130,10 @@ export default function AgentsPage(): React.ReactElement { } >
-
- -
-

Something went wrong

-

{error}

- + +

Error

+

{error}

+
); @@ -286,7 +142,9 @@ export default function AgentsPage(): React.ReactElement { return ( 0 ? `${agents.length} agent${agents.length !== 1 ? 's' : ''}` : undefined + } actions={ - - - - -
-
- )} - - {/* No Results State */} - {!isLoading && agents.length > 0 && filteredAgents.length === 0 && ( -
-
- -
-

No agents found

-

- No agents match "{searchQuery}" -

- -
- )} - - {/* Agents Grid */} - {!isLoading && filteredAgents.length > 0 && ( - <> - {searchQuery && ( -

- Showing {filteredAgents.length} of {agents.length} agents -

- )} -
- {filteredAgents.map((agent) => ( - + + + + Name + Provider + Tools + Updated + Actions + + + + {isLoading ? ( + // Loading skeleton + <> + {[0, 1, 2].map((idx) => ( + + +
+
+
+
+
+
+
+ + +
+ + +
+ + +
+ + +
+ + + ))} + + ) : agents.length === 0 ? ( + + +
+ } + title="No agents yet" + description="Create your first AI agent to start chatting with tools. Agents can use any tools from your collections or individual tools." + action={ + + + + } /> - ))} -
- - )} + ) : ( + agents.map((agent) => ( + router.push(`/dashboard/agents/${agent.id}`)} + className="cursor-pointer" + > + +
+
+ +
+
+

{agent.name}

+ {agent.description && ( +

+ {agent.description} +

+ )} +
+
+
+ +
+ + {PROVIDER_DISPLAY_NAMES[agent.provider]} + + {agent.modelId} +
+
+ + + {agent.toolCount > 0 && agent.collectionCount > 0 + ? `${agent.toolCount} + ${agent.collectionCount} collections` + : agent.collectionCount > 0 + ? `${agent.collectionCount} collection${agent.collectionCount !== 1 ? 's' : ''}` + : agent.toolCount} + + + + + {formatDate(agent.updatedAt)} + + + +
+ e.stopPropagation()} + > + + + +
+
+
+ )) + )} + +
+
); } diff --git a/apps/web/src/app/dashboard/collections/page.tsx b/apps/web/src/app/dashboard/collections/page.tsx index 3727990..60e5828 100644 --- a/apps/web/src/app/dashboard/collections/page.tsx +++ b/apps/web/src/app/dashboard/collections/page.tsx @@ -3,16 +3,22 @@ import { Badge } from '@tpmjs/ui/Badge/Badge'; import { Button } from '@tpmjs/ui/Button/Button'; import { Icon } from '@tpmjs/ui/Icon/Icon'; -import { Input } from '@tpmjs/ui/Input/Input'; -import Link from 'next/link'; +import { + Table, + TableBody, + TableCell, + TableEmpty, + TableHead, + TableHeader, + TableRow, +} from '@tpmjs/ui/Table/Table'; import { useRouter } from 'next/navigation'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { CollectionForm } from '~/components/collections/CollectionForm'; import { DashboardLayout } from '~/components/dashboard/DashboardLayout'; interface Collection { id: string; - slug: string | null; name: string; description: string | null; toolCount: number; @@ -20,172 +26,13 @@ interface Collection { updatedAt: string; } -function formatRelativeDate(dateString: string): string { +function formatDate(dateString: string): string { const date = new Date(dateString); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) { - const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); - if (diffHours === 0) { - const diffMinutes = Math.floor(diffMs / (1000 * 60)); - return diffMinutes <= 1 ? 'Just now' : `${diffMinutes}m ago`; - } - return `${diffHours}h ago`; - } - if (diffDays === 1) return 'Yesterday'; - if (diffDays < 7) return `${diffDays}d ago`; - if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`; - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); -} - -function CollectionCard({ - collection, - onDelete, - isDeleting, - username, -}: { - collection: Collection; - onDelete: (id: string) => void; - isDeleting: boolean; - username: string | null; -}) { - const router = useRouter(); - const [showCopied, setShowCopied] = useState(false); - - const mcpUrl = collection.slug && username - ? `${typeof window !== 'undefined' ? window.location.origin : ''}/api/mcp/${username}/${collection.slug}/http` - : null; - - const handleCopyMcpUrl = (e: React.MouseEvent) => { - e.stopPropagation(); - if (mcpUrl) { - navigator.clipboard.writeText(mcpUrl); - setShowCopied(true); - setTimeout(() => setShowCopied(false), 2000); - } - }; - - return ( -
router.push(`/dashboard/collections/${collection.id}`)} - > - {/* Header */} -
-
-
- -
-
-

- {collection.name} -

-
- - {collection.isPublic ? 'Public' : 'Private'} - -
-
-
- - {formatRelativeDate(collection.updatedAt)} - -
- - {/* Description */} - {collection.description ? ( -

- {collection.description} -

- ) : ( -

- No description -

- )} - - {/* Stats */} -
-
- - - {collection.toolCount} tool{collection.toolCount !== 1 ? 's' : ''} - -
- {mcpUrl && ( -
- - MCP Ready -
- )} -
- - {/* Actions */} -
- {mcpUrl && ( - - )} - e.stopPropagation()} - className={mcpUrl ? '' : 'flex-1'} - > - - - -
-
- ); -} - -function CollectionCardSkeleton() { - return ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ); + return date.toLocaleDateString('en-US', { + month: 'short', + day: 'numeric', + year: 'numeric', + }); } export default function CollectionsPage(): React.ReactElement { @@ -196,8 +43,6 @@ export default function CollectionsPage(): React.ReactElement { const [showCreateForm, setShowCreateForm] = useState(false); const [isCreating, setIsCreating] = useState(false); const [deletingId, setDeletingId] = useState(null); - const [searchQuery, setSearchQuery] = useState(''); - const [username, setUsername] = useState(null); const fetchCollections = useCallback(async () => { try { @@ -221,32 +66,10 @@ export default function CollectionsPage(): React.ReactElement { } }, [router]); - // Fetch user info for MCP URL - useEffect(() => { - fetch('/api/user/profile') - .then(res => res.json()) - .then(data => { - if (data.success && data.data?.username) { - setUsername(data.data.username); - } - }) - .catch(() => {}); - }, []); - useEffect(() => { fetchCollections(); }, [fetchCollections]); - const filteredCollections = useMemo(() => { - if (!searchQuery.trim()) return collections; - const query = searchQuery.toLowerCase(); - return collections.filter( - (collection) => - collection.name.toLowerCase().includes(query) || - collection.description?.toLowerCase().includes(query) - ); - }, [collections, searchQuery]); - const handleCreate = async (data: { name: string; description?: string; isPublic: boolean }) => { setIsCreating(true); @@ -273,7 +96,8 @@ export default function CollectionsPage(): React.ReactElement { } }; - const handleDelete = async (id: string) => { + const handleDelete = async (id: string, e: React.MouseEvent) => { + e.stopPropagation(); if ( !confirm('Are you sure you want to delete this collection? This action cannot be undone.') ) { @@ -314,14 +138,10 @@ export default function CollectionsPage(): React.ReactElement { } >
-
- -
-

Something went wrong

-

{error}

- + +

Error

+

{error}

+
); @@ -330,7 +150,11 @@ export default function CollectionsPage(): React.ReactElement { return ( 0 + ? `${collections.length} collection${collections.length !== 1 ? 's' : ''}` + : undefined + } actions={ !showCreateForm && ( - - - -
-
- )} - - {/* No Results State */} - {!isLoading && collections.length > 0 && filteredCollections.length === 0 && ( -
-
- -
-

No collections found

-

- No collections match "{searchQuery}" -

- -
- )} - - {/* Collections Grid */} - {!isLoading && filteredCollections.length > 0 && ( - <> - {searchQuery && ( -

- Showing {filteredCollections.length} of {collections.length} collections -

- )} -
- {filteredCollections.map((collection) => ( - + + + + Name + Tools + Visibility + Updated + Actions + + + + {isLoading ? ( + // Loading skeleton + <> + {[0, 1, 2].map((idx) => ( + + +
+
+
+
+
+
+
+ + +
+ + +
+ + +
+ + +
+ + + ))} + + ) : collections.length === 0 ? ( + + +
+ } + title="No collections yet" + description="Create a collection to organize your tools. Collections make it easy to group related tools together and share them with your agents." + action={ + + } /> - ))} -
- - )} - - {/* Help Section */} - {!isLoading && collections.length > 0 && !showCreateForm && ( -
-
-
- -
-
-

Connect to MCP clients

-

- Each public collection has an MCP URL you can use with Claude Desktop, Cursor, - Windsurf, or any MCP-compatible client. Copy the URL and add it to your client's configuration. -

- - Learn more about MCP integration → - -
-
-
- )} + ) : ( + collections.map((collection) => ( + router.push(`/dashboard/collections/${collection.id}`)} + className="cursor-pointer" + > + +
+
+ +
+
+

{collection.name}

+ {collection.description && ( +

+ {collection.description} +

+ )} +
+
+
+ + {collection.toolCount} + + + + {collection.isPublic ? 'Public' : 'Private'} + + + + + {formatDate(collection.updatedAt)} + + + +
+ +
+
+
+ )) + )} + +
+
); }