feat(web): add interactive animated features section to homepage
- Add FeaturesSection with animated counters, terminal demo, and flow diagram - Add ToolConnectionViz canvas animation showing tools → TPMJS → agents flow - Add interactive tool grid with hover effects - Add feature cards with scroll-triggered reveal animations - Add FEATURES.md documenting all platform capabilities
This commit is contained in:
parent
c677110bfe
commit
08cd131088
4 changed files with 1410 additions and 44 deletions
389
apps/web/FEATURES.md
Normal file
389
apps/web/FEATURES.md
Normal file
|
|
@ -0,0 +1,389 @@
|
|||
# TPMJS Features
|
||||
|
||||
TPMJS is the universal tool registry for AI agents. Discover, publish, and execute AI tools with one URL.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Tool Discovery](#tool-discovery)
|
||||
- [Tool Execution](#tool-execution)
|
||||
- [AI Agents](#ai-agents)
|
||||
- [Collections](#collections)
|
||||
- [MCP Integration](#mcp-integration)
|
||||
- [Publishing Tools](#publishing-tools)
|
||||
- [API Access](#api-access)
|
||||
- [Dashboard](#dashboard)
|
||||
- [Community Features](#community-features)
|
||||
|
||||
---
|
||||
|
||||
## Tool Discovery
|
||||
|
||||
### Search & Browse
|
||||
|
||||
- **Full-text search** - BM25-ranked semantic search across 170+ tools
|
||||
- **Category filtering** - Filter by category (research, web, data, engineering, security, utilities, etc.)
|
||||
- **Health status filtering** - Filter by import/execution health (Healthy, Degraded, Broken)
|
||||
- **Official tools** - Browse verified official TPMJS tools
|
||||
- **Virtualized browsing** - Smooth scrolling through large tool lists
|
||||
|
||||
### Tool Details
|
||||
|
||||
Each tool page displays:
|
||||
|
||||
- Package name and version
|
||||
- Description and usage examples
|
||||
- Input parameter schemas (extracted from Zod)
|
||||
- Required environment variables
|
||||
- Quality score (0.0 - 1.0)
|
||||
- Monthly npm downloads
|
||||
- Health status indicators
|
||||
- Community ratings and reviews
|
||||
|
||||
### Quality Scoring
|
||||
|
||||
Tools are automatically scored based on:
|
||||
|
||||
- Documentation tier (Rich, Basic, Minimal)
|
||||
- npm download count
|
||||
- GitHub stars
|
||||
- Import and execution health
|
||||
|
||||
---
|
||||
|
||||
## Tool Execution
|
||||
|
||||
### Interactive Playground
|
||||
|
||||
Test any tool directly in the browser:
|
||||
|
||||
- AI-generated test parameters
|
||||
- Real-time streaming output
|
||||
- Execution history
|
||||
- Error reporting with health status
|
||||
|
||||
### Sandboxed Execution
|
||||
|
||||
- Tools execute in isolated Deno runtime
|
||||
- Secure sandboxing on Railway infrastructure
|
||||
- Rate limiting by tier (Free, Pro, Enterprise)
|
||||
- Automatic timeout handling
|
||||
|
||||
### Dynamic Tool Loading (Beta)
|
||||
|
||||
- Tools discovered based on conversation context
|
||||
- AI automatically selects relevant tools
|
||||
- No manual configuration required
|
||||
|
||||
---
|
||||
|
||||
## AI Agents
|
||||
|
||||
### Create Custom Agents
|
||||
|
||||
Build AI agents with:
|
||||
|
||||
- **System prompts** - Define agent behavior and personality
|
||||
- **Model selection** - Choose from available AI models
|
||||
- **Tool selection** - Pick specific tools or entire collections
|
||||
- **Temperature control** - Adjust response creativity
|
||||
|
||||
### Agent Features
|
||||
|
||||
- **Web chat interface** - Test agents directly in browser
|
||||
- **Conversation history** - Save and review past conversations
|
||||
- **Public sharing** - Share agents via unique URLs
|
||||
- **Forking** - Clone and customize existing agents
|
||||
- **Analytics** - Track execution history and usage
|
||||
|
||||
### Agent Management
|
||||
|
||||
- Create, edit, and delete agents
|
||||
- Toggle public/private visibility
|
||||
- View fork relationships
|
||||
- Monitor health and performance
|
||||
|
||||
---
|
||||
|
||||
## Collections
|
||||
|
||||
### Curated Tool Bundles
|
||||
|
||||
Group related tools into collections:
|
||||
|
||||
- **Create collections** - Bundle tools for specific use cases
|
||||
- **AI-generated descriptions** - Auto-generate use case descriptions
|
||||
- **Public sharing** - Share collections with the community
|
||||
- **Forking** - Clone and customize collections
|
||||
|
||||
### Collection Features
|
||||
|
||||
- Add/remove tools dynamically
|
||||
- Set visibility (public/private)
|
||||
- View included tools with full metadata
|
||||
- Copy MCP configuration for instant use
|
||||
|
||||
---
|
||||
|
||||
## MCP Integration
|
||||
|
||||
### Universal Compatibility
|
||||
|
||||
TPMJS works with any MCP-compatible client:
|
||||
|
||||
| Client | Support |
|
||||
|--------|---------|
|
||||
| Claude Desktop | Full integration |
|
||||
| Cursor | Full integration |
|
||||
| Windsurf | Full integration |
|
||||
| Any MCP client | JSON-RPC 2.0 |
|
||||
|
||||
### Quick Start
|
||||
|
||||
Add to `claude_desktop_config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"tpmjs": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@anthropic/mcp-remote",
|
||||
"https://tpmjs.com/api/mcp/ajax/ajax-collection/sse"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### MCP Endpoints
|
||||
|
||||
- **Collection endpoints** - Each collection exposes an MCP server
|
||||
- **Authentication** - API key-based auth via headers
|
||||
- **Streaming** - Server-Sent Events (SSE) for real-time responses
|
||||
|
||||
### MCP Protocol
|
||||
|
||||
Full JSON-RPC 2.0 implementation:
|
||||
|
||||
- `initialize` - Establish session
|
||||
- `tools/list` - Get available tools
|
||||
- `tools/call` - Execute tools
|
||||
|
||||
---
|
||||
|
||||
## Publishing Tools
|
||||
|
||||
### Auto-Discovery
|
||||
|
||||
Publish tools to npm and they appear on TPMJS within minutes:
|
||||
|
||||
1. Add `tpmjs` keyword to `package.json`
|
||||
2. Configure the `tpmjs` field with metadata
|
||||
3. Publish to npm
|
||||
4. Tool appears in 2-15 minutes
|
||||
|
||||
### Package Configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@your-scope/your-tool",
|
||||
"keywords": ["tpmjs"],
|
||||
"tpmjs": {
|
||||
"category": "utilities",
|
||||
"tools": ["myTool", "anotherTool"],
|
||||
"envVars": ["API_KEY"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Discovery Methods
|
||||
|
||||
- **Changes feed** - Monitors npm real-time (every 2 min)
|
||||
- **Keyword search** - Active npm search (every 15 min)
|
||||
- **Manual curation** - High-quality verified tools
|
||||
|
||||
### Package Generator
|
||||
|
||||
Quick start with the CLI generator:
|
||||
|
||||
```bash
|
||||
npx @tpmjs/create-basic-tools
|
||||
```
|
||||
|
||||
Creates a production-ready package with:
|
||||
|
||||
- 2-3 example tools
|
||||
- Complete TypeScript setup
|
||||
- Best practices included
|
||||
|
||||
---
|
||||
|
||||
## API Access
|
||||
|
||||
### REST API
|
||||
|
||||
```
|
||||
GET /api/tools - List tools with filters
|
||||
GET /api/tools/search - BM25 semantic search
|
||||
GET /api/tools/[pkg]/[tool] - Get tool details
|
||||
POST /api/tools/execute/[pkg]/[tool] - Execute tool
|
||||
GET /api/public/collections - List collections
|
||||
GET /api/public/agents - List agents
|
||||
GET /api/stats - Platform statistics
|
||||
```
|
||||
|
||||
### Authentication
|
||||
|
||||
Generate API keys with granular scopes:
|
||||
|
||||
| Scope | Permission |
|
||||
|-------|------------|
|
||||
| `mcp:execute` | Execute tools via MCP |
|
||||
| `agent:chat` | Agent conversations |
|
||||
| `bridge:connect` | Bridge connections |
|
||||
| `collection:read` | Read collections |
|
||||
| `usage:read` | View usage analytics |
|
||||
|
||||
### Rate Limits
|
||||
|
||||
| Tier | Requests/Hour |
|
||||
|------|---------------|
|
||||
| Free | 100 |
|
||||
| Pro | 1,000 |
|
||||
| Enterprise | 10,000 |
|
||||
|
||||
### Streaming Responses
|
||||
|
||||
All execution endpoints support SSE streaming for real-time output.
|
||||
|
||||
---
|
||||
|
||||
## Dashboard
|
||||
|
||||
### Overview
|
||||
|
||||
- Welcome screen with quick stats
|
||||
- Recent activity stream
|
||||
- Quick access to agents and collections
|
||||
|
||||
### Agents Management
|
||||
|
||||
- View all your agents
|
||||
- Create new agents
|
||||
- Edit agent configuration
|
||||
- Test via chat interface
|
||||
- View conversation history
|
||||
|
||||
### Collections Management
|
||||
|
||||
- View all your collections
|
||||
- Create and edit collections
|
||||
- Add/remove tools
|
||||
- Toggle visibility
|
||||
|
||||
### Likes & Bookmarks
|
||||
|
||||
Save your favorites:
|
||||
|
||||
- `/dashboard/likes/tools` - Liked tools
|
||||
- `/dashboard/likes/agents` - Liked agents
|
||||
- `/dashboard/likes/collections` - Liked collections
|
||||
|
||||
### Settings
|
||||
|
||||
- **Profile** - Name, email, avatar
|
||||
- **API Keys** - Generate and manage TPMJS keys
|
||||
- **Third-Party Keys** - Store external API credentials
|
||||
- **Bridge** - Configure Bridge integration
|
||||
|
||||
### Usage Analytics
|
||||
|
||||
- API call statistics
|
||||
- Execution counts and success rates
|
||||
- Error tracking
|
||||
- Usage against rate limits
|
||||
|
||||
---
|
||||
|
||||
## Community Features
|
||||
|
||||
### Social Engagement
|
||||
|
||||
- **Likes** - Like tools, agents, and collections
|
||||
- **Ratings** - Rate tools (1-5 stars)
|
||||
- **Reviews** - Leave comments on tools
|
||||
|
||||
### Sharing
|
||||
|
||||
- **Public profiles** - View user profiles at `/[username]`
|
||||
- **Copy options** - Multiple formats for sharing:
|
||||
- Direct URLs
|
||||
- MCP configurations
|
||||
- API endpoints
|
||||
|
||||
### Forking
|
||||
|
||||
Clone and customize:
|
||||
|
||||
- Fork public agents
|
||||
- Fork public collections
|
||||
- Track fork relationships
|
||||
- Attribution to original creators
|
||||
|
||||
---
|
||||
|
||||
## Framework Compatibility
|
||||
|
||||
TPMJS tools work with popular AI frameworks:
|
||||
|
||||
- **Vercel AI SDK** - Native compatibility
|
||||
- **LangChain** - Full support
|
||||
- **LlamaIndex** - Compatible
|
||||
- **Custom frameworks** - JSON-RPC protocol
|
||||
|
||||
---
|
||||
|
||||
## Security
|
||||
|
||||
- **Sandboxed execution** - Isolated Deno runtime
|
||||
- **API key rotation** - Secure key management
|
||||
- **Rate limiting** - Protection against abuse
|
||||
- **Email verification** - Account security
|
||||
- **Scoped permissions** - Fine-grained access control
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
Comprehensive guides available:
|
||||
|
||||
- [API Documentation](/docs/api) - REST and MCP APIs
|
||||
- [Publishing Guide](/publish) - How to publish tools
|
||||
- [MCP Tutorial](/docs/tutorials/mcp) - MCP client setup
|
||||
- [Custom Executors](/docs/tutorials/custom-executor) - Self-hosted execution
|
||||
- [Bridge Setup](/docs/tutorials/bridge) - External tool integration
|
||||
- [Agent Tutorial](/docs/tutorials/agents) - Building AI agents
|
||||
|
||||
---
|
||||
|
||||
## Platform Stats
|
||||
|
||||
- **170+ tools** indexed and searchable
|
||||
- **Multiple categories** covering diverse use cases
|
||||
- **Real-time sync** with npm registry
|
||||
- **Automatic health monitoring**
|
||||
- **Quality scoring** for all tools
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. **Browse tools** at [tpmjs.com/tool/tool-search](https://tpmjs.com/tool/tool-search)
|
||||
2. **Try the playground** to test tools interactively
|
||||
3. **Create an account** to build agents and collections
|
||||
4. **Generate an API key** for programmatic access
|
||||
5. **Configure MCP** for Claude, Cursor, or Windsurf
|
||||
|
||||
---
|
||||
|
||||
*TPMJS - The Tool Package Manager for AI*
|
||||
|
|
@ -5,6 +5,7 @@ import { Container } from '@tpmjs/ui/Container/Container';
|
|||
import Link from 'next/link';
|
||||
import { AppHeader } from '../components/AppHeader';
|
||||
// import { ArchitectureDiagramWrapper } from '../components/home/ArchitectureDiagramWrapper';
|
||||
import { FeaturesSection } from '../components/home/FeaturesSection';
|
||||
import { HeroSection } from '../components/home/HeroSection';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
|
@ -84,6 +85,9 @@ export default async function HomePage(): Promise<React.ReactElement> {
|
|||
{/* Hero Section - Dithered Design */}
|
||||
<HeroSection stats={data.stats} />
|
||||
|
||||
{/* Features Section - Interactive & Animated */}
|
||||
<FeaturesSection />
|
||||
|
||||
{/* Architecture Diagram Section - temporarily disabled
|
||||
<section className="py-16 bg-background border-b border-border">
|
||||
<Container size="xl" padding="lg">
|
||||
|
|
@ -188,58 +192,104 @@ export default async function HomePage(): Promise<React.ReactElement> {
|
|||
</section>
|
||||
|
||||
{/* Integration Section */}
|
||||
<section className="py-16 bg-surface border-y border-border">
|
||||
<section className="py-20 bg-surface border-y border-border">
|
||||
<Container size="xl" padding="lg">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-foreground">
|
||||
Works Everywhere
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-16">
|
||||
<p className="font-mono text-xs text-primary uppercase tracking-widest mb-3">
|
||||
universal compatibility
|
||||
</p>
|
||||
<h2 className="font-mono text-3xl md:text-4xl font-semibold mb-4 text-foreground lowercase">
|
||||
works everywhere
|
||||
</h2>
|
||||
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto">
|
||||
<p className="text-base text-foreground-secondary max-w-xl mx-auto font-sans">
|
||||
Use TPMJS tools with any MCP-compatible AI client. One URL, instant access.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
|
||||
<div className="p-6 border border-border rounded-lg bg-background text-center">
|
||||
<div className="text-2xl mb-3 font-mono text-brutalist-accent">Claude</div>
|
||||
<p className="text-sm text-foreground-secondary">Desktop, Projects, and API</p>
|
||||
</div>
|
||||
<div className="p-6 border border-border rounded-lg bg-background text-center">
|
||||
<div className="text-2xl mb-3 font-mono text-brutalist-accent">Cursor</div>
|
||||
<p className="text-sm text-foreground-secondary">AI-powered code editor</p>
|
||||
</div>
|
||||
<div className="p-6 border border-border rounded-lg bg-background text-center">
|
||||
<div className="text-2xl mb-3 font-mono text-brutalist-accent">Windsurf</div>
|
||||
<p className="text-sm text-foreground-secondary">Agentic IDE</p>
|
||||
</div>
|
||||
<div className="p-6 border border-border rounded-lg bg-background text-center">
|
||||
<div className="text-2xl mb-3 font-mono text-brutalist-accent">Any MCP</div>
|
||||
<p className="text-sm text-foreground-secondary">Open protocol support</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code Example */}
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="bg-background border border-border rounded-lg overflow-hidden">
|
||||
<div className="px-4 py-2 border-b border-border bg-surface-secondary text-xs font-mono text-foreground-secondary">
|
||||
claude_desktop_config.json
|
||||
{/* Client Cards - Fieldset Style */}
|
||||
<fieldset className="border border-dashed border-border p-8 mb-12">
|
||||
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
|
||||
supported clients
|
||||
</legend>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="group p-6 border border-dashed border-border hover:border-primary hover:bg-primary/5 transition-all">
|
||||
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center mb-4">
|
||||
<span className="font-mono text-lg text-primary">C</span>
|
||||
</div>
|
||||
<div className="font-mono text-lg font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
Claude
|
||||
</div>
|
||||
<p className="font-mono text-xs text-foreground-tertiary mt-1">
|
||||
desktop / projects / api
|
||||
</p>
|
||||
</div>
|
||||
<div className="group p-6 border border-dashed border-border hover:border-primary hover:bg-primary/5 transition-all">
|
||||
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center mb-4">
|
||||
<span className="font-mono text-lg text-primary">{'>_'}</span>
|
||||
</div>
|
||||
<div className="font-mono text-lg font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
Cursor
|
||||
</div>
|
||||
<p className="font-mono text-xs text-foreground-tertiary mt-1">
|
||||
ai-powered code editor
|
||||
</p>
|
||||
</div>
|
||||
<div className="group p-6 border border-dashed border-border hover:border-primary hover:bg-primary/5 transition-all">
|
||||
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center mb-4">
|
||||
<span className="font-mono text-lg text-primary">W</span>
|
||||
</div>
|
||||
<div className="font-mono text-lg font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
Windsurf
|
||||
</div>
|
||||
<p className="font-mono text-xs text-foreground-tertiary mt-1">
|
||||
agentic ide
|
||||
</p>
|
||||
</div>
|
||||
<div className="group p-6 border border-dashed border-border hover:border-primary hover:bg-primary/5 transition-all">
|
||||
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center mb-4">
|
||||
<span className="font-mono text-lg text-primary">*</span>
|
||||
</div>
|
||||
<div className="font-mono text-lg font-medium text-foreground group-hover:text-primary transition-colors">
|
||||
Any MCP
|
||||
</div>
|
||||
<p className="font-mono text-xs text-foreground-tertiary mt-1">
|
||||
open protocol support
|
||||
</p>
|
||||
</div>
|
||||
<pre className="p-4 text-sm font-mono text-foreground overflow-x-auto">
|
||||
<code>{`{
|
||||
"mcpServers": {
|
||||
"tpmjs": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@anthropic/mcp-remote",
|
||||
"https://tpmjs.com/api/mcp/ajax/ajax-collection/sse"]
|
||||
}
|
||||
}
|
||||
}`}</code>
|
||||
</pre>
|
||||
</div>
|
||||
<p className="text-center mt-4 text-sm text-foreground-secondary">
|
||||
Add this to your config and get instant access to 170+ tools.
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
{/* Code Example - Fieldset Style */}
|
||||
<fieldset className="border border-dashed border-border p-8 max-w-2xl mx-auto">
|
||||
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
|
||||
quick start
|
||||
</legend>
|
||||
<div className="mb-4">
|
||||
<p className="font-mono text-xs text-foreground-tertiary uppercase tracking-wider mb-2">
|
||||
claude_desktop_config.json
|
||||
</p>
|
||||
<div className="bg-background border border-border p-4 font-mono text-sm overflow-x-auto">
|
||||
<pre className="text-foreground">
|
||||
<span className="text-foreground-tertiary">{'{'}</span>
|
||||
{'\n '}<span className="text-primary">"mcpServers"</span>: <span className="text-foreground-tertiary">{'{'}</span>
|
||||
{'\n '}<span className="text-primary">"tpmjs"</span>: <span className="text-foreground-tertiary">{'{'}</span>
|
||||
{'\n '}<span className="text-primary">"command"</span>: <span className="text-success">"npx"</span>,
|
||||
{'\n '}<span className="text-primary">"args"</span>: [<span className="text-success">"-y"</span>, <span className="text-success">"@anthropic/mcp-remote"</span>,
|
||||
{'\n '}<span className="text-success">"https://tpmjs.com/api/mcp/ajax/ajax-collection/sse"</span>]
|
||||
{'\n '}<span className="text-foreground-tertiary">{'}'}</span>
|
||||
{'\n '}<span className="text-foreground-tertiary">{'}'}</span>
|
||||
{'\n'}<span className="text-foreground-tertiary">{'}'}</span>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 pt-4 border-t border-dashed border-border">
|
||||
<div className="w-2 h-2 bg-success rounded-full animate-pulse" />
|
||||
<p className="font-mono text-xs text-foreground-secondary">
|
||||
add to config → instant access to <span className="text-primary font-medium">170+ tools</span>
|
||||
</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
</Container>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
607
apps/web/src/components/home/FeaturesSection.tsx
Normal file
607
apps/web/src/components/home/FeaturesSection.tsx
Normal file
|
|
@ -0,0 +1,607 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Container } from '@tpmjs/ui/Container/Container';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { ToolConnectionViz } from './ToolConnectionViz';
|
||||
|
||||
// ============================================================================
|
||||
// Animated Terminal Component
|
||||
// ============================================================================
|
||||
|
||||
function AnimatedTerminal(): React.ReactElement {
|
||||
const [currentLine, setCurrentLine] = useState(0);
|
||||
const [displayedText, setDisplayedText] = useState('');
|
||||
const [isTyping, setIsTyping] = useState(true);
|
||||
|
||||
const lines = [
|
||||
{ type: 'input', text: '$ npx @tpmjs/tools-unsandbox' },
|
||||
{ type: 'output', text: '✓ Tool loaded: executeCodeAsync' },
|
||||
{ type: 'input', text: '$ execute --lang python --code "print(sum(range(100)))"' },
|
||||
{ type: 'output', text: '→ Spinning up secure sandbox...' },
|
||||
{ type: 'output', text: '→ Executing code...' },
|
||||
{ type: 'success', text: '✓ Output: 4950' },
|
||||
{ type: 'input', text: '$ _' },
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
if (currentLine >= lines.length) {
|
||||
// Reset after delay
|
||||
const timeout = setTimeout(() => {
|
||||
setCurrentLine(0);
|
||||
setDisplayedText('');
|
||||
setIsTyping(true);
|
||||
}, 3000);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
|
||||
const line = lines[currentLine];
|
||||
if (!line) return;
|
||||
|
||||
if (line.type === 'input') {
|
||||
// Type out input lines character by character
|
||||
let charIndex = 0;
|
||||
setIsTyping(true);
|
||||
const interval = setInterval(() => {
|
||||
if (charIndex <= line.text.length) {
|
||||
setDisplayedText(line.text.slice(0, charIndex));
|
||||
charIndex++;
|
||||
} else {
|
||||
clearInterval(interval);
|
||||
setIsTyping(false);
|
||||
setTimeout(() => {
|
||||
setCurrentLine((prev) => prev + 1);
|
||||
setDisplayedText('');
|
||||
}, 500);
|
||||
}
|
||||
}, 50);
|
||||
return () => clearInterval(interval);
|
||||
} else {
|
||||
// Show output lines instantly
|
||||
setDisplayedText(line.text);
|
||||
setIsTyping(false);
|
||||
const timeout = setTimeout(() => {
|
||||
setCurrentLine((prev) => prev + 1);
|
||||
setDisplayedText('');
|
||||
}, 800);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [currentLine]);
|
||||
|
||||
const getLineColor = (type: string) => {
|
||||
switch (type) {
|
||||
case 'input':
|
||||
return 'text-foreground';
|
||||
case 'output':
|
||||
return 'text-foreground-secondary';
|
||||
case 'success':
|
||||
return 'text-success';
|
||||
case 'error':
|
||||
return 'text-error';
|
||||
default:
|
||||
return 'text-foreground-secondary';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#1a1715] rounded-none border-2 border-foreground overflow-hidden shadow-[8px_8px_0_0_rgba(166,89,45,0.3)]">
|
||||
{/* Terminal Header */}
|
||||
<div className="flex items-center gap-2 px-4 py-3 bg-[#2a2520] border-b border-foreground/20">
|
||||
<div className="flex gap-2">
|
||||
<div className="w-3 h-3 rounded-full bg-error/80" />
|
||||
<div className="w-3 h-3 rounded-full bg-warning/80" />
|
||||
<div className="w-3 h-3 rounded-full bg-success/80" />
|
||||
</div>
|
||||
<span className="ml-4 font-mono text-xs text-foreground/40 uppercase tracking-wider">
|
||||
tpmjs terminal
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Terminal Content */}
|
||||
<div className="p-6 font-mono text-sm min-h-[280px]">
|
||||
{/* Previous lines */}
|
||||
{lines.slice(0, currentLine).map((line, i) => (
|
||||
<div key={i} className={`${getLineColor(line.type)} mb-1`}>
|
||||
{line.text}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Current line being typed */}
|
||||
{currentLine < lines.length && (
|
||||
<div className={`${getLineColor(lines[currentLine]?.type || 'input')} flex`}>
|
||||
<span>{displayedText}</span>
|
||||
{isTyping && (
|
||||
<span className="ml-0.5 w-2 h-5 bg-primary animate-pulse" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feature Card with Hover Animation
|
||||
// ============================================================================
|
||||
|
||||
interface FeatureCardProps {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
stats?: string;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
function FeatureCard({ icon, title, description, stats, delay = 0 }: FeatureCardProps): React.ReactElement {
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting) {
|
||||
setTimeout(() => setIsVisible(true), delay);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.2 }
|
||||
);
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [delay]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className={`
|
||||
relative p-6 border-2 border-dashed border-border bg-surface
|
||||
transition-all duration-300 ease-out cursor-pointer
|
||||
${isVisible ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-8'}
|
||||
${isHovered ? 'border-primary bg-primary/5 shadow-[4px_4px_0_0_rgba(166,89,45,0.2)]' : ''}
|
||||
`}
|
||||
>
|
||||
{/* Animated corner accent */}
|
||||
<div
|
||||
className={`
|
||||
absolute top-0 left-0 w-0 h-0 border-t-[3px] border-l-[3px] border-primary
|
||||
transition-all duration-300
|
||||
${isHovered ? 'w-8 h-8' : 'w-0 h-0'}
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
absolute bottom-0 right-0 w-0 h-0 border-b-[3px] border-r-[3px] border-primary
|
||||
transition-all duration-300
|
||||
${isHovered ? 'w-8 h-8' : 'w-0 h-0'}
|
||||
`}
|
||||
/>
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`
|
||||
w-12 h-12 flex items-center justify-center mb-4
|
||||
border-2 border-dashed transition-all duration-300
|
||||
${isHovered ? 'border-primary bg-primary/10' : 'border-border bg-surface-secondary'}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
icon={icon as any}
|
||||
size="md"
|
||||
className={`transition-colors duration-300 ${isHovered ? 'text-primary' : 'text-foreground-secondary'}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<h3 className="font-mono text-lg font-semibold mb-2 text-foreground lowercase">
|
||||
{title}
|
||||
</h3>
|
||||
<p className="font-sans text-sm text-foreground-secondary leading-relaxed mb-4">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
{/* Stats badge */}
|
||||
{stats && (
|
||||
<Badge
|
||||
variant={isHovered ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="transition-all duration-300"
|
||||
>
|
||||
{stats}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Animated Counter
|
||||
// ============================================================================
|
||||
|
||||
interface AnimatedCounterProps {
|
||||
end: number;
|
||||
duration?: number;
|
||||
suffix?: string;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
function AnimatedCounter({ end, duration = 2000, suffix = '', prefix = '' }: AnimatedCounterProps): React.ReactElement {
|
||||
const [count, setCount] = useState(0);
|
||||
const [hasStarted, setHasStarted] = useState(false);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
if (entry?.isIntersecting && !hasStarted) {
|
||||
setHasStarted(true);
|
||||
}
|
||||
},
|
||||
{ threshold: 0.5 }
|
||||
);
|
||||
|
||||
if (ref.current) {
|
||||
observer.observe(ref.current);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [hasStarted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasStarted) return;
|
||||
|
||||
let startTime: number;
|
||||
let animationFrame: number;
|
||||
|
||||
const animate = (timestamp: number) => {
|
||||
if (!startTime) startTime = timestamp;
|
||||
const progress = Math.min((timestamp - startTime) / duration, 1);
|
||||
|
||||
// Easing function for smooth animation
|
||||
const easeOutQuart = 1 - Math.pow(1 - progress, 4);
|
||||
setCount(Math.floor(easeOutQuart * end));
|
||||
|
||||
if (progress < 1) {
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
animationFrame = requestAnimationFrame(animate);
|
||||
return () => cancelAnimationFrame(animationFrame);
|
||||
}, [hasStarted, end, duration]);
|
||||
|
||||
return (
|
||||
<span ref={ref} className="tabular-nums">
|
||||
{prefix}{count.toLocaleString()}{suffix}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Flow Diagram Component
|
||||
// ============================================================================
|
||||
|
||||
function FlowDiagram(): React.ReactElement {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setActiveStep((prev) => (prev + 1) % 4);
|
||||
}, 2000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const steps = [
|
||||
{ label: 'npm publish', icon: 'box', desc: 'publish to npm' },
|
||||
{ label: 'auto-discover', icon: 'search', desc: 'indexed in minutes' },
|
||||
{ label: 'validate', icon: 'check', desc: 'health checks run' },
|
||||
{ label: 'available', icon: 'globe', desc: 'ready for agents' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4 md:gap-0">
|
||||
{steps.map((step, i) => (
|
||||
<div key={i} className="flex items-center">
|
||||
{/* Step */}
|
||||
<div
|
||||
className={`
|
||||
relative flex flex-col items-center p-4 transition-all duration-500
|
||||
${activeStep === i ? 'scale-110' : 'scale-100 opacity-60'}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
w-16 h-16 flex items-center justify-center border-2 mb-3
|
||||
transition-all duration-500
|
||||
${activeStep === i
|
||||
? 'border-primary bg-primary/10 shadow-[0_0_20px_rgba(166,89,45,0.3)]'
|
||||
: 'border-dashed border-border bg-surface'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Icon
|
||||
icon={step.icon as any}
|
||||
size="lg"
|
||||
className={`transition-colors duration-500 ${activeStep === i ? 'text-primary' : 'text-foreground-tertiary'}`}
|
||||
/>
|
||||
</div>
|
||||
<span className={`font-mono text-sm font-medium transition-colors duration-500 ${activeStep === i ? 'text-primary' : 'text-foreground'}`}>
|
||||
{step.label}
|
||||
</span>
|
||||
<span className="font-mono text-xs text-foreground-tertiary mt-1">
|
||||
{step.desc}
|
||||
</span>
|
||||
|
||||
{/* Pulse ring when active */}
|
||||
{activeStep === i && (
|
||||
<div className="absolute inset-0 flex items-start justify-center pt-4">
|
||||
<div className="w-16 h-16 border-2 border-primary/50 animate-ping" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
{i < steps.length - 1 && (
|
||||
<div className="hidden md:flex items-center mx-4">
|
||||
<div
|
||||
className={`
|
||||
h-0.5 w-12 transition-all duration-500
|
||||
${activeStep > i ? 'bg-primary' : 'bg-border'}
|
||||
`}
|
||||
/>
|
||||
<div
|
||||
className={`
|
||||
w-0 h-0 border-t-4 border-b-4 border-l-8
|
||||
border-t-transparent border-b-transparent
|
||||
transition-all duration-500
|
||||
${activeStep > i ? 'border-l-primary' : 'border-l-border'}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Interactive Tool Grid
|
||||
// ============================================================================
|
||||
|
||||
function InteractiveToolGrid(): React.ReactElement {
|
||||
const tools = [
|
||||
{ name: 'web-scraper', category: 'web', color: 'bg-info' },
|
||||
{ name: 'code-executor', category: 'sandbox', color: 'bg-success' },
|
||||
{ name: 'pdf-parser', category: 'data', color: 'bg-warning' },
|
||||
{ name: 'image-gen', category: 'ai', color: 'bg-error' },
|
||||
{ name: 'db-query', category: 'data', color: 'bg-info' },
|
||||
{ name: 'api-caller', category: 'web', color: 'bg-success' },
|
||||
{ name: 'file-convert', category: 'utilities', color: 'bg-warning' },
|
||||
{ name: 'text-analyze', category: 'ai', color: 'bg-error' },
|
||||
{ name: 'email-send', category: 'integration', color: 'bg-info' },
|
||||
];
|
||||
|
||||
const [hoveredTool, setHoveredTool] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{tools.map((tool, i) => (
|
||||
<div
|
||||
key={tool.name}
|
||||
onMouseEnter={() => setHoveredTool(tool.name)}
|
||||
onMouseLeave={() => setHoveredTool(null)}
|
||||
className={`
|
||||
relative p-3 border border-dashed border-border bg-surface
|
||||
transition-all duration-300 cursor-pointer
|
||||
${hoveredTool === tool.name ? 'border-primary scale-105 z-10 shadow-lg' : ''}
|
||||
`}
|
||||
style={{
|
||||
animationDelay: `${i * 100}ms`,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${tool.color}`} />
|
||||
<span className="font-mono text-xs truncate">{tool.name}</span>
|
||||
</div>
|
||||
{hoveredTool === tool.name && (
|
||||
<div className="absolute -top-8 left-1/2 -translate-x-1/2 bg-foreground text-background px-2 py-1 text-xs font-mono whitespace-nowrap z-20">
|
||||
{tool.category}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Features Section
|
||||
// ============================================================================
|
||||
|
||||
export function FeaturesSection(): React.ReactElement {
|
||||
return (
|
||||
<section className="py-24 bg-background relative overflow-hidden">
|
||||
{/* Subtle grid background */}
|
||||
<div className="absolute inset-0 opacity-[0.02]">
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(to right, currentColor 1px, transparent 1px),
|
||||
linear-gradient(to bottom, currentColor 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '60px 60px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Container size="xl" padding="lg" className="relative z-10">
|
||||
{/* Section Header */}
|
||||
<div className="text-center mb-20">
|
||||
<p className="font-mono text-xs text-primary uppercase tracking-widest mb-3">
|
||||
everything you need
|
||||
</p>
|
||||
<h2 className="font-mono text-4xl md:text-5xl font-bold mb-6 text-foreground lowercase tracking-tight">
|
||||
powerful features
|
||||
</h2>
|
||||
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto font-sans">
|
||||
From discovery to execution, TPMJS provides the complete infrastructure
|
||||
for AI tool development.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Row */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-20">
|
||||
{[
|
||||
{ value: 170, suffix: '+', label: 'tools indexed' },
|
||||
{ value: 15, suffix: ' min', label: 'discovery time' },
|
||||
{ value: 99, suffix: '%', label: 'uptime' },
|
||||
{ value: 4, suffix: '', label: 'mcp clients' },
|
||||
].map((stat, i) => (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="text-center p-6 border border-dashed border-border bg-surface hover:border-primary transition-colors"
|
||||
>
|
||||
<div className="font-mono text-4xl md:text-5xl font-bold text-primary mb-2">
|
||||
<AnimatedCounter end={stat.value} suffix={stat.suffix} duration={1500 + i * 200} />
|
||||
</div>
|
||||
<div className="font-mono text-sm text-foreground-secondary uppercase tracking-wider">
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Main Feature Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 mb-20">
|
||||
{/* Left: Terminal Demo */}
|
||||
<div>
|
||||
<fieldset className="border border-dashed border-border p-6">
|
||||
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
|
||||
live execution
|
||||
</legend>
|
||||
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
||||
Execute any tool directly from your terminal or AI agent. Secure sandboxed
|
||||
execution with real-time streaming output.
|
||||
</p>
|
||||
<AnimatedTerminal />
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
{/* Right: Tool Grid */}
|
||||
<div>
|
||||
<fieldset className="border border-dashed border-border p-6 h-full">
|
||||
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
|
||||
tool registry
|
||||
</legend>
|
||||
<p className="font-sans text-sm text-foreground-secondary mb-6">
|
||||
Browse 170+ tools across multiple categories. Each tool is validated,
|
||||
documented, and ready to use.
|
||||
</p>
|
||||
<InteractiveToolGrid />
|
||||
<div className="mt-6 flex justify-center">
|
||||
<Link href="/tool/tool-search">
|
||||
<Button variant="outline" size="sm">
|
||||
Browse All Tools
|
||||
<Icon icon="chevronRight" size="sm" className="ml-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Flow Diagram */}
|
||||
<fieldset className="border border-dashed border-border p-8 mb-20">
|
||||
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
|
||||
how it works
|
||||
</legend>
|
||||
<FlowDiagram />
|
||||
</fieldset>
|
||||
|
||||
{/* Interactive Connection Visualization */}
|
||||
<fieldset className="border border-dashed border-border p-8 mb-20 overflow-hidden">
|
||||
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
|
||||
tools → tpmjs → agents
|
||||
</legend>
|
||||
<p className="font-sans text-sm text-foreground-secondary mb-6 text-center max-w-2xl mx-auto">
|
||||
TPMJS acts as the central hub connecting npm packages to AI agents.
|
||||
Watch data flow in real-time as tools serve agent requests.
|
||||
</p>
|
||||
<ToolConnectionViz />
|
||||
</fieldset>
|
||||
|
||||
{/* Feature Cards Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-16">
|
||||
<FeatureCard
|
||||
icon="search"
|
||||
title="instant discovery"
|
||||
description="Tools are automatically discovered from npm within 2-15 minutes. Just add the tpmjs keyword and publish."
|
||||
stats="auto-sync"
|
||||
delay={0}
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="shield"
|
||||
title="secure execution"
|
||||
description="Every tool runs in an isolated Deno sandbox. Rate limiting, timeout handling, and error recovery built-in."
|
||||
stats="sandboxed"
|
||||
delay={100}
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="zap"
|
||||
title="quality scoring"
|
||||
description="Automatic scoring based on documentation, downloads, and health status. Find the best tools instantly."
|
||||
stats="0.0 - 1.0"
|
||||
delay={200}
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="users"
|
||||
title="ai agents"
|
||||
description="Build custom AI agents with curated tool collections. Share publicly or keep private."
|
||||
stats="unlimited"
|
||||
delay={300}
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="folder"
|
||||
title="collections"
|
||||
description="Group related tools into collections. Perfect for specific use cases or team workflows."
|
||||
stats="shareable"
|
||||
delay={400}
|
||||
/>
|
||||
<FeatureCard
|
||||
icon="code"
|
||||
title="rest & mcp api"
|
||||
description="Full REST API and MCP protocol support. Works with Claude, Cursor, Windsurf, and any compatible client."
|
||||
stats="json-rpc 2.0"
|
||||
delay={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div className="text-center">
|
||||
<div className="inline-flex flex-col sm:flex-row gap-4">
|
||||
<Link href="/tool/tool-search">
|
||||
<Button size="lg" variant="default" className="min-w-[200px]">
|
||||
Explore Tools
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/publish">
|
||||
<Button size="lg" variant="outline" className="min-w-[200px]">
|
||||
Publish Your Tool
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
320
apps/web/src/components/home/ToolConnectionViz.tsx
Normal file
320
apps/web/src/components/home/ToolConnectionViz.tsx
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
interface Node {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
vx: number;
|
||||
vy: number;
|
||||
type: 'tool' | 'agent' | 'hub';
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Connection {
|
||||
from: string;
|
||||
to: string;
|
||||
progress: number;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export function ToolConnectionViz(): React.ReactElement {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const animationRef = useRef<number | null>(null);
|
||||
const [dimensions, setDimensions] = useState({ width: 800, height: 400 });
|
||||
const nodesRef = useRef<Node[]>([]);
|
||||
const connectionsRef = useRef<Connection[]>([]);
|
||||
const mouseRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
// Initialize nodes and connections
|
||||
useEffect(() => {
|
||||
const tools = [
|
||||
{ label: 'web-scraper', color: '#3380CC' },
|
||||
{ label: 'code-exec', color: '#327D52' },
|
||||
{ label: 'pdf-parse', color: '#D9A020' },
|
||||
{ label: 'img-gen', color: '#C44545' },
|
||||
{ label: 'api-call', color: '#3380CC' },
|
||||
{ label: 'db-query', color: '#327D52' },
|
||||
];
|
||||
|
||||
const agents = [
|
||||
{ label: 'Claude', color: '#A6592D' },
|
||||
{ label: 'GPT', color: '#A6592D' },
|
||||
{ label: 'Agent', color: '#A6592D' },
|
||||
];
|
||||
|
||||
const nodes: Node[] = [];
|
||||
|
||||
// Central hub (TPMJS)
|
||||
nodes.push({
|
||||
id: 'hub',
|
||||
x: dimensions.width / 2,
|
||||
y: dimensions.height / 2,
|
||||
vx: 0,
|
||||
vy: 0,
|
||||
type: 'hub',
|
||||
label: 'TPMJS',
|
||||
color: '#A6592D',
|
||||
});
|
||||
|
||||
// Tools on the left
|
||||
tools.forEach((tool, i) => {
|
||||
const angle = (Math.PI / (tools.length + 1)) * (i + 1) - Math.PI / 2;
|
||||
const radius = 150;
|
||||
nodes.push({
|
||||
id: `tool-${i}`,
|
||||
x: dimensions.width / 2 - radius + Math.cos(angle) * 80,
|
||||
y: dimensions.height / 2 + Math.sin(angle) * 120,
|
||||
vx: (Math.random() - 0.5) * 0.5,
|
||||
vy: (Math.random() - 0.5) * 0.5,
|
||||
type: 'tool',
|
||||
label: tool.label,
|
||||
color: tool.color,
|
||||
});
|
||||
});
|
||||
|
||||
// Agents on the right
|
||||
agents.forEach((agent, i) => {
|
||||
const angle = (Math.PI / (agents.length + 1)) * (i + 1) + Math.PI / 2;
|
||||
const radius = 150;
|
||||
nodes.push({
|
||||
id: `agent-${i}`,
|
||||
x: dimensions.width / 2 + radius + Math.cos(angle) * 80,
|
||||
y: dimensions.height / 2 + Math.sin(angle) * 80,
|
||||
vx: (Math.random() - 0.5) * 0.5,
|
||||
vy: (Math.random() - 0.5) * 0.5,
|
||||
type: 'agent',
|
||||
label: agent.label,
|
||||
color: agent.color,
|
||||
});
|
||||
});
|
||||
|
||||
nodesRef.current = nodes;
|
||||
|
||||
// Create connections
|
||||
const connections: Connection[] = [];
|
||||
tools.forEach((_, i) => {
|
||||
connections.push({
|
||||
from: `tool-${i}`,
|
||||
to: 'hub',
|
||||
progress: Math.random(),
|
||||
active: Math.random() > 0.5,
|
||||
});
|
||||
});
|
||||
agents.forEach((_, i) => {
|
||||
connections.push({
|
||||
from: 'hub',
|
||||
to: `agent-${i}`,
|
||||
progress: Math.random(),
|
||||
active: Math.random() > 0.5,
|
||||
});
|
||||
});
|
||||
connectionsRef.current = connections;
|
||||
}, [dimensions]);
|
||||
|
||||
// Handle resize
|
||||
useEffect(() => {
|
||||
const updateDimensions = () => {
|
||||
if (containerRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
setDimensions({ width: rect.width, height: 400 });
|
||||
}
|
||||
};
|
||||
|
||||
updateDimensions();
|
||||
window.addEventListener('resize', updateDimensions);
|
||||
return () => window.removeEventListener('resize', updateDimensions);
|
||||
}, []);
|
||||
|
||||
// Handle mouse movement
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (containerRef.current) {
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
mouseRef.current = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', handleMouseMove);
|
||||
return () => window.removeEventListener('mousemove', handleMouseMove);
|
||||
}, []);
|
||||
|
||||
// Animation loop
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const animate = () => {
|
||||
ctx.clearRect(0, 0, dimensions.width, dimensions.height);
|
||||
|
||||
const nodes = nodesRef.current;
|
||||
const connections = connectionsRef.current;
|
||||
|
||||
// Update node positions (subtle floating)
|
||||
nodes.forEach((node) => {
|
||||
if (node.type !== 'hub') {
|
||||
node.x += node.vx;
|
||||
node.y += node.vy;
|
||||
|
||||
// Bounce off boundaries
|
||||
if (node.x < 50 || node.x > dimensions.width - 50) node.vx *= -1;
|
||||
if (node.y < 50 || node.y > dimensions.height - 50) node.vy *= -1;
|
||||
|
||||
// Mouse repulsion
|
||||
const dx = mouseRef.current.x - node.x;
|
||||
const dy = mouseRef.current.y - node.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < 100) {
|
||||
node.vx -= (dx / dist) * 0.1;
|
||||
node.vy -= (dy / dist) * 0.1;
|
||||
}
|
||||
|
||||
// Damping
|
||||
node.vx *= 0.99;
|
||||
node.vy *= 0.99;
|
||||
}
|
||||
});
|
||||
|
||||
// Draw connections
|
||||
connections.forEach((conn) => {
|
||||
const fromNode = nodes.find((n) => n.id === conn.from);
|
||||
const toNode = nodes.find((n) => n.id === conn.to);
|
||||
if (!fromNode || !toNode) return;
|
||||
|
||||
// Update progress
|
||||
if (conn.active) {
|
||||
conn.progress += 0.01;
|
||||
if (conn.progress > 1) {
|
||||
conn.progress = 0;
|
||||
conn.active = Math.random() > 0.3;
|
||||
}
|
||||
} else {
|
||||
if (Math.random() > 0.995) {
|
||||
conn.active = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Draw line
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(fromNode.x, fromNode.y);
|
||||
ctx.lineTo(toNode.x, toNode.y);
|
||||
ctx.strokeStyle = 'rgba(166, 89, 45, 0.2)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.stroke();
|
||||
|
||||
// Draw data packet
|
||||
if (conn.active) {
|
||||
const packetX = fromNode.x + (toNode.x - fromNode.x) * conn.progress;
|
||||
const packetY = fromNode.y + (toNode.y - fromNode.y) * conn.progress;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(packetX, packetY, 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#A6592D';
|
||||
ctx.fill();
|
||||
|
||||
// Glow effect
|
||||
ctx.beginPath();
|
||||
ctx.arc(packetX, packetY, 8, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(166, 89, 45, 0.3)';
|
||||
ctx.fill();
|
||||
}
|
||||
});
|
||||
|
||||
// Draw nodes
|
||||
nodes.forEach((node) => {
|
||||
// Node background
|
||||
if (node.type === 'hub') {
|
||||
// Central hub - larger with glow
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, 40, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(166, 89, 45, 0.1)';
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, 30, 0, Math.PI * 2);
|
||||
ctx.fillStyle = '#A6592D';
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, 30, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = 'rgba(166, 89, 45, 0.5)';
|
||||
ctx.lineWidth = 3;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
// Regular nodes
|
||||
const size = node.type === 'agent' ? 24 : 20;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, size + 4, 0, Math.PI * 2);
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, size, 0, Math.PI * 2);
|
||||
ctx.fillStyle = node.color;
|
||||
ctx.fill();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, size, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = 'rgba(0, 0, 0, 0.1)';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Label
|
||||
ctx.font = '11px monospace';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = node.type === 'hub' ? '#FFFFFF' : '#1a1715';
|
||||
if (node.type === 'hub') {
|
||||
ctx.font = 'bold 12px monospace';
|
||||
ctx.fillText(node.label, node.x, node.y + 4);
|
||||
} else {
|
||||
ctx.fillText(node.label, node.x, node.y + (node.type === 'agent' ? 40 : 36));
|
||||
}
|
||||
});
|
||||
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
};
|
||||
|
||||
animate();
|
||||
|
||||
return () => {
|
||||
if (animationRef.current) {
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
}
|
||||
};
|
||||
}, [dimensions]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative w-full">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
className="w-full"
|
||||
style={{ height: '400px' }}
|
||||
/>
|
||||
|
||||
{/* Labels */}
|
||||
<div className="absolute left-4 top-1/2 -translate-y-1/2 text-left">
|
||||
<span className="font-mono text-xs text-foreground-tertiary uppercase tracking-wider">
|
||||
tools
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute right-4 top-1/2 -translate-y-1/2 text-right">
|
||||
<span className="font-mono text-xs text-foreground-tertiary uppercase tracking-wider">
|
||||
agents
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue