diff --git a/apps/web/package.json b/apps/web/package.json
index bd3648a..1f21a82 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -27,6 +27,8 @@
"@ai-sdk/openai": "3.0.7",
"@modelcontextprotocol/sdk": "^1.25.2",
"@prisma/client": "^6.19.1",
+ "@react-three/drei": "^10.7.7",
+ "@react-three/fiber": "^9.5.0",
"@tpmjs/db": "workspace:*",
"@tpmjs/env": "workspace:*",
"@tpmjs/npm-client": "workspace:*",
@@ -57,7 +59,9 @@
"remark-gfm": "^4.0.1",
"resend": "^6.7.0",
"sonner": "^2.0.7",
+ "swr": "^2.2.5",
"streamdown": "^1.6.11",
+ "three": "^0.182.0",
"zod": "^4.3.5"
},
"devDependencies": {
@@ -68,6 +72,7 @@
"@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
+ "@types/three": "^0.182.0",
"autoprefixer": "^10.4.23",
"dotenv": "^17.2.3",
"eslint": "^9.39.2",
diff --git a/apps/web/src/app/about/page.tsx b/apps/web/src/app/about/page.tsx
new file mode 100644
index 0000000..3eabc68
--- /dev/null
+++ b/apps/web/src/app/about/page.tsx
@@ -0,0 +1,57 @@
+import { Container } from '@tpmjs/ui/Container/Container';
+import { Icon } from '@tpmjs/ui/Icon/Icon';
+import type { Metadata } from 'next';
+import { AppHeader } from '~/components/AppHeader';
+
+export const metadata: Metadata = {
+ title: 'About',
+ description: 'About TPMJS and its creator',
+};
+
+export default function AboutPage(): React.ReactElement {
+ return (
+
+
+
+
+ About TPMJS
+
+
+
+ TPMJS (Tool Package Manager for JavaScript) is the npm registry for AI agent tools.
+ It was started in 2024 to make it easy for developers to publish and discover tools
+ that AI agents can use.
+
+
+
Creator
+
+
+ TPMJS was created by Ajax Davis (Thomas Davis).
+
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/app/agents/page.tsx b/apps/web/src/app/agents/page.tsx
index 538e941..26f078d 100644
--- a/apps/web/src/app/agents/page.tsx
+++ b/apps/web/src/app/agents/page.tsx
@@ -9,30 +9,12 @@ import { LoadingState } from '@tpmjs/ui/LoadingState/LoadingState';
import { PageHeader } from '@tpmjs/ui/PageHeader/PageHeader';
import { Select } from '@tpmjs/ui/Select/Select';
import Link from 'next/link';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useCallback, useMemo, useState } from 'react';
import { TableVirtuoso } from 'react-virtuoso';
import { AppHeader } from '~/components/AppHeader';
import { CopyDropdown, getAgentCopyOptions } from '~/components/CopyDropdown';
import { LikeButton } from '~/components/LikeButton';
-
-interface PublicAgent {
- id: string;
- uid: string;
- name: string;
- description: string | null;
- provider: string;
- modelId: string;
- likeCount: number;
- toolCount: number;
- collectionCount: number;
- createdAt: string;
- createdBy: {
- id: string;
- name: string;
- image: string | null;
- username: string | null;
- };
-}
+import { type PublicAgent, useAgents } from '~/hooks/useAgents';
type SortOption = 'likes' | 'recent' | 'tools';
@@ -57,68 +39,24 @@ function truncateText(text: string, maxLength: number): string {
}
export default function PublicAgentsPage(): React.ReactElement {
- const [agents, setAgents] = useState([]);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const [hasMore, setHasMore] = useState(false);
const [search, setSearch] = useState('');
const [sort, setSort] = useState('likes');
- const loadingMore = useRef(false);
- const fetchAgents = useCallback(
- async (offset: number, resetList = false) => {
- try {
- if (loadingMore.current && !resetList) return;
- loadingMore.current = true;
+ // Fetch agents using SWR
+ const { data, isLoading, error: swrError, mutate } = useAgents({ sort });
- const params = new URLSearchParams({
- limit: '100',
- offset: String(offset),
- sort,
- });
+ const agents = data?.agents ?? [];
+ const hasMore = data?.pagination.hasMore ?? false;
+ const error = swrError?.message ?? null;
- const response = await fetch(`/api/public/agents?${params}`);
- const data = await response.json();
-
- if (data.success) {
- if (resetList || offset === 0) {
- setAgents(data.data);
- } else {
- setAgents((prev) => [...prev, ...data.data]);
- }
- setHasMore(data.pagination.hasMore);
- } else {
- setError(data.error?.message || 'Failed to fetch agents');
- }
- } catch (err) {
- console.error('Failed to fetch agents:', err);
- setError('Failed to fetch agents');
- } finally {
- setIsLoading(false);
- loadingMore.current = false;
- }
- },
- [sort]
- );
-
- useEffect(() => {
- setIsLoading(true);
- fetchAgents(0, true);
- }, [fetchAgents]);
-
- const loadMore = useCallback(() => {
- if (!hasMore || loadingMore.current) return;
- fetchAgents(agents.length);
- }, [hasMore, agents.length, fetchAgents]);
-
- // Filter and sort agents
+ // Filter and sort agents (client-side search)
const filteredAgents = useMemo(() => {
let result = agents;
if (search) {
const query = search.toLowerCase();
result = result.filter(
- (a) =>
+ (a: PublicAgent) =>
a.name.toLowerCase().includes(query) ||
a.description?.toLowerCase().includes(query) ||
a.provider.toLowerCase().includes(query)
@@ -253,7 +191,7 @@ export default function PublicAgentsPage(): React.ReactElement {
{/* Content */}
{error ? (
- fetchAgents(0, true)} />
+ mutate()} />
) : isLoading ? (
) : filteredAgents.length === 0 ? (
@@ -269,7 +207,6 @@ export default function PublicAgentsPage(): React.ReactElement {
style={{ height: 'calc(100vh - 350px)', minHeight: '400px' }}
data={filteredAgents}
overscan={30}
- endReached={loadMore}
fixedHeaderContent={TableHeader}
itemContent={TableRow}
components={{
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index f92a486..cbf14a1 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -5,6 +5,7 @@ import Script from 'next/script';
import { Toaster } from 'sonner';
import { AppFooter } from '../components/AppFooter';
import { ThemeProvider } from '../components/providers/ThemeProvider';
+import { SWRProvider } from '../components/SWRProvider';
import './globals.css';
const spaceGrotesk = Space_Grotesk({
@@ -157,11 +158,13 @@ export default function RootLayout({
enableSystem={true}
disableTransitionOnChange={false}
>
-
-
+
+
+
+