diff --git a/apps/web/package.json b/apps/web/package.json index 79d11f3..fbb1429 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,9 +19,11 @@ "@tpmjs/types": "workspace:*", "@tpmjs/ui": "workspace:*", "@tpmjs/utils": "workspace:*", + "@types/d3": "^7.4.3", "@types/react-syntax-highlighter": "^15.5.13", "ai": "6.0.0-beta.124", "bm25": "^0.1.1", + "d3": "^7.9.0", "next": "^16.0.8", "next-themes": "^0.4.6", "openai": "^6.9.1", diff --git a/apps/web/src/app/sdk/page.tsx b/apps/web/src/app/sdk/page.tsx index f76a627..1e358e8 100644 --- a/apps/web/src/app/sdk/page.tsx +++ b/apps/web/src/app/sdk/page.tsx @@ -3,6 +3,7 @@ import { CodeBlock } from '@tpmjs/ui/CodeBlock/CodeBlock'; import { Container } from '@tpmjs/ui/Container/Container'; import Link from 'next/link'; import { AppHeader } from '~/components/AppHeader'; +import { SDKFlowDiagram } from '~/components/SDKFlowDiagram'; export const metadata = { title: 'SDK - Registry Tools | TPMJS', @@ -168,28 +169,7 @@ Use registrySearch to find tools, then registryExecute to run them.\`, {/* How It Works */}

How It Works

-
-
-                {`┌─────────────────────────────────────────────────────────────────┐
-│                      Your AI Agent                               │
-│  ┌─────────────┐  ┌────────────────┐  ┌─────────────────────┐   │
-│  │ Your Tools  │  │ registrySearch │  │   registryExecute   │   │
-│  └─────────────┘  └───────┬────────┘  └──────────┬──────────┘   │
-└───────────────────────────┼──────────────────────┼──────────────┘
-                            │                      │
-                            ▼                      ▼
-                  ┌─────────────────┐   ┌─────────────────────────┐
-                  │  TPMJS Registry │   │   Sandbox Executor      │
-                  │  tpmjs.com/api  │   │  executor.tpmjs.com     │
-                  └─────────────────┘   └─────────────────────────┘
-                            │                      │
-                            ▼                      ▼
-                  ┌─────────────────┐   ┌─────────────────────────┐
-                  │  Tool Metadata  │   │   Secure Deno Runtime   │
-                  │  1000+ tools    │   │   Isolated execution    │
-                  └─────────────────┘   └─────────────────────────┘`}
-              
-
+
{/* registrySearchTool */} diff --git a/apps/web/src/components/SDKFlowDiagram.tsx b/apps/web/src/components/SDKFlowDiagram.tsx new file mode 100644 index 0000000..b5ad8c5 --- /dev/null +++ b/apps/web/src/components/SDKFlowDiagram.tsx @@ -0,0 +1,562 @@ +'use client'; + +import * as d3 from 'd3'; +import { useEffect, useRef, useState } from 'react'; + +interface Node { + id: string; + label: string; + sublabel?: string; + x: number; + y: number; + width: number; + height: number; + type: 'agent' | 'tool' | 'service' | 'output'; + children?: string[]; +} + +interface Connection { + from: string; + to: string; + animated?: boolean; +} + +export function SDKFlowDiagram(): React.ReactElement { + const svgRef = useRef(null); + const containerRef = useRef(null); + const [hoveredNode, setHoveredNode] = useState(null); + const [dimensions, setDimensions] = useState({ width: 800, height: 500 }); + + useEffect(() => { + const updateDimensions = () => { + if (containerRef.current) { + const width = Math.min(containerRef.current.clientWidth, 900); + setDimensions({ width, height: 520 }); + } + }; + + updateDimensions(); + window.addEventListener('resize', updateDimensions); + return () => window.removeEventListener('resize', updateDimensions); + }, []); + + useEffect(() => { + if (!svgRef.current) return; + + const svg = d3.select(svgRef.current); + svg.selectAll('*').remove(); + + const { width } = dimensions; + const centerX = width / 2; + + // Node definitions + const nodes: Node[] = [ + // Agent container + { + id: 'agent', + label: 'Your AI Agent', + x: centerX, + y: 70, + width: Math.min(680, width - 40), + height: 100, + type: 'agent', + children: ['your-tools', 'registry-search', 'registry-execute'], + }, + // Tools inside agent + { + id: 'your-tools', + label: 'Your Tools', + x: centerX - Math.min(220, width * 0.25), + y: 70, + width: 120, + height: 44, + type: 'tool', + }, + { + id: 'registry-search', + label: 'registrySearch', + x: centerX, + y: 70, + width: 140, + height: 44, + type: 'tool', + }, + { + id: 'registry-execute', + label: 'registryExecute', + x: centerX + Math.min(220, width * 0.25), + y: 70, + width: 140, + height: 44, + type: 'tool', + }, + // Services + { + id: 'registry', + label: 'TPMJS Registry', + sublabel: 'tpmjs.com/api', + x: centerX - Math.min(140, width * 0.16), + y: 240, + width: 160, + height: 60, + type: 'service', + }, + { + id: 'executor', + label: 'Sandbox Executor', + sublabel: 'executor.tpmjs.com', + x: centerX + Math.min(140, width * 0.16), + y: 240, + width: 180, + height: 60, + type: 'service', + }, + // Outputs + { + id: 'metadata', + label: 'Tool Metadata', + sublabel: '1000+ tools', + x: centerX - Math.min(140, width * 0.16), + y: 400, + width: 150, + height: 60, + type: 'output', + }, + { + id: 'runtime', + label: 'Secure Deno Runtime', + sublabel: 'Isolated execution', + x: centerX + Math.min(140, width * 0.16), + y: 400, + width: 180, + height: 60, + type: 'output', + }, + ]; + + const connections: Connection[] = [ + { from: 'registry-search', to: 'registry', animated: true }, + { from: 'registry-execute', to: 'executor', animated: true }, + { from: 'registry', to: 'metadata', animated: true }, + { from: 'executor', to: 'runtime', animated: true }, + ]; + + // Create defs for gradients and filters + const defs = svg.append('defs'); + + // Glow filter + const glow = defs + .append('filter') + .attr('id', 'glow') + .attr('x', '-50%') + .attr('y', '-50%') + .attr('width', '200%') + .attr('height', '200%'); + glow.append('feGaussianBlur').attr('stdDeviation', '3').attr('result', 'coloredBlur'); + const glowMerge = glow.append('feMerge'); + glowMerge.append('feMergeNode').attr('in', 'coloredBlur'); + glowMerge.append('feMergeNode').attr('in', 'SourceGraphic'); + + // Subtle shadow + const shadow = defs + .append('filter') + .attr('id', 'shadow') + .attr('x', '-20%') + .attr('y', '-20%') + .attr('width', '140%') + .attr('height', '140%'); + shadow + .append('feDropShadow') + .attr('dx', '0') + .attr('dy', '2') + .attr('stdDeviation', '4') + .attr('flood-color', 'currentColor') + .attr('flood-opacity', '0.15'); + + // Arrow marker + defs + .append('marker') + .attr('id', 'arrowhead') + .attr('viewBox', '0 -5 10 10') + .attr('refX', 8) + .attr('refY', 0) + .attr('markerWidth', 6) + .attr('markerHeight', 6) + .attr('orient', 'auto') + .append('path') + .attr('d', 'M0,-5L10,0L0,5') + .attr('fill', 'currentColor') + .attr('class', 'text-foreground-tertiary'); + + // Animated dash pattern + defs + .append('pattern') + .attr('id', 'dash-pattern') + .attr('patternUnits', 'userSpaceOnUse') + .attr('width', '20') + .attr('height', '1') + .append('rect') + .attr('width', '10') + .attr('height', '1') + .attr('fill', 'currentColor'); + + const mainGroup = svg.append('g'); + + // Draw connections with animated flow + 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; + + const startY = fromNode.y + fromNode.height / 2 + 22; + const endY = toNode.y - toNode.height / 2; + const midY = (startY + endY) / 2; + + const pathData = `M ${fromNode.x} ${startY} + C ${fromNode.x} ${midY}, + ${toNode.x} ${midY}, + ${toNode.x} ${endY - 8}`; + + // Background path + mainGroup + .append('path') + .attr('d', pathData) + .attr('fill', 'none') + .attr('stroke', 'currentColor') + .attr('class', 'text-border') + .attr('stroke-width', 2) + .attr('opacity', 0.3); + + // Animated path + const animatedPath = mainGroup + .append('path') + .attr('d', pathData) + .attr('fill', 'none') + .attr('stroke', 'currentColor') + .attr('class', 'text-foreground') + .attr('stroke-width', 2) + .attr('stroke-dasharray', '8,12') + .attr('stroke-linecap', 'round') + .attr('marker-end', 'url(#arrowhead)'); + + // Animate the dash offset + if (conn.animated) { + const animate = () => { + animatedPath + .attr('stroke-dashoffset', 0) + .transition() + .duration(1500) + .ease(d3.easeLinear) + .attr('stroke-dashoffset', -40) + .on('end', animate); + }; + animate(); + } + + // Flowing particle effect + const particle = mainGroup + .append('circle') + .attr('r', 4) + .attr('fill', 'currentColor') + .attr('class', 'text-primary') + .attr('opacity', 0) + .attr('filter', 'url(#glow)'); + + const animateParticle = () => { + const pathNode = animatedPath.node(); + if (!pathNode) return; + const pathLength = (pathNode as SVGPathElement).getTotalLength(); + + particle + .attr('opacity', 0.8) + .transition() + .duration(2000) + .ease(d3.easeQuadInOut) + .attrTween('transform', () => { + return (t: number) => { + const point = (pathNode as SVGPathElement).getPointAtLength(t * pathLength); + return `translate(${point.x}, ${point.y})`; + }; + }) + .attr('opacity', 0) + .on('end', () => { + setTimeout(animateParticle, Math.random() * 1000 + 500); + }); + }; + setTimeout(animateParticle, Math.random() * 2000); + }); + + // Draw agent container + const agentNode = nodes.find((n) => n.id === 'agent'); + if (agentNode) { + const agentGroup = mainGroup + .append('g') + .attr('transform', `translate(${agentNode.x}, ${agentNode.y})`) + .style('cursor', 'pointer'); + + // Outer container with gradient border effect + agentGroup + .append('rect') + .attr('x', -agentNode.width / 2) + .attr('y', -agentNode.height / 2) + .attr('width', agentNode.width) + .attr('height', agentNode.height) + .attr('rx', 16) + .attr('fill', 'none') + .attr('stroke', 'currentColor') + .attr('class', 'text-border') + .attr('stroke-width', 2) + .attr('filter', 'url(#shadow)'); + + // Background fill + agentGroup + .append('rect') + .attr('x', -agentNode.width / 2 + 1) + .attr('y', -agentNode.height / 2 + 1) + .attr('width', agentNode.width - 2) + .attr('height', agentNode.height - 2) + .attr('rx', 15) + .attr('fill', 'currentColor') + .attr('class', 'text-surface') + .attr('opacity', 0.5); + + // Agent label + agentGroup + .append('text') + .attr('x', 0) + .attr('y', -agentNode.height / 2 + 24) + .attr('text-anchor', 'middle') + .attr('fill', 'currentColor') + .attr('class', 'text-foreground') + .attr('font-size', '14px') + .attr('font-weight', '600') + .attr('font-family', 'system-ui, -apple-system, sans-serif') + .text(agentNode.label); + } + + // Draw tool nodes inside agent + const toolNodes = nodes.filter((n) => n.type === 'tool'); + toolNodes.forEach((node, i) => { + const nodeGroup = mainGroup + .append('g') + .attr('transform', `translate(${node.x}, ${node.y})`) + .attr('class', 'tool-node') + .style('cursor', 'pointer') + .on('mouseenter', function () { + setHoveredNode(node.id); + d3.select(this).select('rect').transition().duration(200).attr('stroke-width', 2); + d3.select(this).select('.node-glow').transition().duration(200).attr('opacity', 0.3); + }) + .on('mouseleave', function () { + setHoveredNode(null); + d3.select(this).select('rect').transition().duration(200).attr('stroke-width', 1.5); + d3.select(this).select('.node-glow').transition().duration(200).attr('opacity', 0); + }); + + // Glow effect on hover + nodeGroup + .append('rect') + .attr('class', 'node-glow') + .attr('x', -node.width / 2 - 4) + .attr('y', -node.height / 2 - 4) + .attr('width', node.width + 8) + .attr('height', node.height + 8) + .attr('rx', 12) + .attr('fill', 'currentColor') + .attr('class', 'node-glow text-primary') + .attr('opacity', 0) + .attr('filter', 'url(#glow)'); + + // Main rectangle + nodeGroup + .append('rect') + .attr('x', -node.width / 2) + .attr('y', -node.height / 2) + .attr('width', node.width) + .attr('height', node.height) + .attr('rx', 8) + .attr('fill', 'currentColor') + .attr('class', 'text-background') + .attr('stroke', 'currentColor') + .attr('stroke-width', 1.5) + .style( + 'stroke', + node.id === 'your-tools' ? 'var(--color-border)' : 'var(--color-foreground)' + ); + + // Label + nodeGroup + .append('text') + .attr('x', 0) + .attr('y', 5) + .attr('text-anchor', 'middle') + .attr('fill', 'currentColor') + .attr('class', 'text-foreground') + .attr('font-size', '13px') + .attr('font-weight', '500') + .attr('font-family', 'ui-monospace, monospace') + .text(node.label); + + // Entrance animation + nodeGroup + .attr('opacity', 0) + .attr('transform', `translate(${node.x}, ${node.y - 20})`) + .transition() + .delay(200 + i * 100) + .duration(500) + .ease(d3.easeCubicOut) + .attr('opacity', 1) + .attr('transform', `translate(${node.x}, ${node.y})`); + }); + + // Draw service and output nodes + const otherNodes = nodes.filter((n) => n.type === 'service' || n.type === 'output'); + otherNodes.forEach((node, i) => { + const nodeGroup = mainGroup + .append('g') + .attr('transform', `translate(${node.x}, ${node.y})`) + .style('cursor', 'pointer') + .on('mouseenter', function () { + setHoveredNode(node.id); + d3.select(this).select('.main-rect').transition().duration(200).attr('stroke-width', 2); + d3.select(this).select('.node-glow').transition().duration(200).attr('opacity', 0.2); + }) + .on('mouseleave', function () { + setHoveredNode(null); + d3.select(this).select('.main-rect').transition().duration(200).attr('stroke-width', 1.5); + d3.select(this).select('.node-glow').transition().duration(200).attr('opacity', 0); + }); + + // Glow effect + nodeGroup + .append('rect') + .attr('class', 'node-glow') + .attr('x', -node.width / 2 - 4) + .attr('y', -node.height / 2 - 4) + .attr('width', node.width + 8) + .attr('height', node.height + 8) + .attr('rx', 14) + .attr('fill', 'currentColor') + .attr('class', 'node-glow text-primary') + .attr('opacity', 0) + .attr('filter', 'url(#glow)'); + + // Main rectangle + nodeGroup + .append('rect') + .attr('class', 'main-rect') + .attr('x', -node.width / 2) + .attr('y', -node.height / 2) + .attr('width', node.width) + .attr('height', node.height) + .attr('rx', 10) + .attr('fill', 'currentColor') + .attr('class', 'text-background') + .attr('stroke', 'currentColor') + .attr('stroke-width', 1.5) + .style( + 'stroke', + node.type === 'output' ? 'var(--color-border)' : 'var(--color-foreground)' + ); + + // Label + nodeGroup + .append('text') + .attr('x', 0) + .attr('y', node.sublabel ? -4 : 5) + .attr('text-anchor', 'middle') + .attr('fill', 'currentColor') + .attr('class', 'text-foreground') + .attr('font-size', '13px') + .attr('font-weight', '600') + .attr('font-family', 'system-ui, -apple-system, sans-serif') + .text(node.label); + + // Sublabel + if (node.sublabel) { + nodeGroup + .append('text') + .attr('x', 0) + .attr('y', 14) + .attr('text-anchor', 'middle') + .attr('fill', 'currentColor') + .attr('class', 'text-foreground-tertiary') + .attr('font-size', '11px') + .attr('font-family', 'ui-monospace, monospace') + .text(node.sublabel); + } + + // Entrance animation + nodeGroup + .attr('opacity', 0) + .attr('transform', `translate(${node.x}, ${node.y + 30})`) + .transition() + .delay(500 + i * 150) + .duration(600) + .ease(d3.easeCubicOut) + .attr('opacity', 1) + .attr('transform', `translate(${node.x}, ${node.y})`); + }); + }, [dimensions]); + + return ( +
+
+ {/* Subtle grid background */} +
+ + + + {/* Tooltip for hovered node */} + {hoveredNode && ( +
+ {hoveredNode === 'registry-search' && ( + + Search the registry for tools matching your needs + + )} + {hoveredNode === 'registry-execute' && ( + + Execute any tool in a secure sandbox + + )} + {hoveredNode === 'registry' && ( + 1000+ verified AI SDK tools + )} + {hoveredNode === 'executor' && ( + + Isolated Deno runtime for safe execution + + )} + {hoveredNode === 'metadata' && ( + + Tool schemas, descriptions, and health status + + )} + {hoveredNode === 'runtime' && ( + + Sandboxed execution with API key isolation + + )} +
+ )} +
+
+ ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6392560..d1fc1d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,10 +131,10 @@ importers: version: 10.4.22(postcss@8.5.6) eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-next: specifier: ^16.0.4 - version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) + version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) postcss: specifier: ^8.5.1 version: 8.5.6 @@ -180,6 +180,9 @@ importers: '@tpmjs/utils': specifier: workspace:* version: link:../../packages/utils + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 '@types/react-syntax-highlighter': specifier: ^15.5.13 version: 15.5.13 @@ -189,6 +192,9 @@ importers: bm25: specifier: ^0.1.1 version: 0.1.1 + d3: + specifier: ^7.9.0 + version: 7.9.0 next: specifier: ^16.0.8 version: 16.0.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -249,10 +255,10 @@ importers: version: 10.4.22(postcss@8.5.6) eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-next: specifier: ^16.0.4 - version: 16.0.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 16.0.4(eslint@9.39.1(jiti@1.21.7))(typescript@5.9.3) postcss: specifier: ^8.5.1 version: 8.5.6 @@ -9436,7 +9442,7 @@ snapshots: '@next/eslint-plugin-next': 16.0.4 eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) @@ -9479,7 +9485,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -9519,13 +9525,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)): + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)): dependencies: debug: 3.2.7 optionalDependencies: eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@1.21.7)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) transitivePeerDependencies: - supports-color @@ -9579,7 +9585,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@1.21.7) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@1.21.7)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3