diff --git a/web/public/landing.html b/web/public/landing.html new file mode 100644 index 0000000..8f174dc --- /dev/null +++ b/web/public/landing.html @@ -0,0 +1,333 @@ + + + + + + AgentHub — The Backbone of Your AI Agent Fleet + + + + + + + + + + + + + + +
+
+
+

+ The Backbone of Your AI Agent Fleet +

+

+ Build, deploy, and monitor autonomous AI agents at scale. API-first orchestration platform for the agentic future. +

+ +
+
+
+ + +
+
+
+
+
247
+
Agents Deployed
+
+
+
12.4K
+
Tasks Executed
+
+
+
99.9%
+
Uptime
+
+
+
24/7
+
Heartbeat Monitoring
+
+
+
+
+ + +
+
+
+

Built for Scale, Designed for Developers

+

+ Everything you need to orchestrate, monitor, and scale your AI agent infrastructure +

+
+ +
+
+
🤖
+

Agent Orchestration

+

+ Deploy and manage multiple AI agents with a unified control plane. Built-in task routing and load balancing. +

+
+ +
+
📊
+

Live Monitoring

+

+ Real-time dashboards for agent health, task execution, and system metrics. Know what's happening, always. +

+
+ +
+
🔌
+

API-First

+

+ RESTful API with WebSocket support. Integrate with any stack. TypeScript SDK included. +

+
+ +
+
+

Heartbeat Engine

+

+ Lightweight health checks keep your agents alive and responsive. Auto-recovery on failures. +

+
+ +
+
🔐
+

Least Privilege

+

+ Fine-grained access control. Room-based isolation. Your agents stay secure by default. +

+
+ +
+
🛠️
+

Dev Tools

+

+ Local development mode, debug logs, Prometheus metrics. Built by developers, for developers. +

+
+
+
+
+ + +
+
+
+

Deploy an Agent in Seconds

+

+ Simple API, powerful orchestration. Get started in three lines of code. +

+
+ +
+
POST /api/rooms
+
+{
+  "roomId": "my-agent-room",
+  "agents": [
+    {
+      "id": "agent-001",
+      "name": "My First Agent",
+      "capabilities": ["chat", "task-execution"]
+    }
+  ]
+}
+
+ +
+

Core Endpoints

+
+
+ POST /api/rooms + Create an agent room +
+
+ POST /api/sessions + Start a session with agents +
+
+ GET /api/rooms/:id + Get room details and agent status +
+
+ GET /api/metrics + Prometheus-compatible metrics +
+
+
+
+
+ + +
+
+

+ Start Building Today +

+

+ Join the early access program and shape the future of AI agent infrastructure. +

+ +

+ Free during alpha • No credit card required • Self-hosted option available +

+
+
+ + + + + + diff --git a/web/src/components/MentionAutocomplete.tsx b/web/src/components/MentionAutocomplete.tsx new file mode 100644 index 0000000..22f90e9 --- /dev/null +++ b/web/src/components/MentionAutocomplete.tsx @@ -0,0 +1,200 @@ +import { useState, useEffect, useRef, useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { api } from '../lib/api'; +import type { DirectoryAgent } from '../types'; + +interface MentionAutocompleteProps { + value: string; + onChange: (value: string) => void; + onSubmit: () => void; + placeholder?: string; + disabled?: boolean; + rows?: number; +} + +export function MentionAutocomplete({ + value, + onChange, + onSubmit, + placeholder, + disabled, + rows = 2, +}: MentionAutocompleteProps) { + const [showAutocomplete, setShowAutocomplete] = useState(false); + const [mentionQuery, setMentionQuery] = useState(''); + const [mentionStart, setMentionStart] = useState(-1); + const [selectedIndex, setSelectedIndex] = useState(0); + const textareaRef = useRef(null); + const autocompleteRef = useRef(null); + + // Fetch directory agents + const { data: directoryData } = useQuery({ + queryKey: ['directory', 'BARAAA'], + queryFn: () => api.getDirectory('BARAAA'), + staleTime: 60000, + }); + + const agents = directoryData?.agents ?? []; + + // Filter agents based on mention query + const filteredAgents = useMemo(() => { + if (!mentionQuery) return agents; + const query = mentionQuery.toLowerCase(); + return agents.filter( + (agent) => + agent.name.toLowerCase().includes(query) || + (agent.role && agent.role.toLowerCase().includes(query)), + ); + }, [agents, mentionQuery]); + + // Detect @ mention in textarea + useEffect(() => { + const textarea = textareaRef.current; + if (!textarea) return; + + const cursorPos = textarea.selectionStart; + const textBeforeCursor = value.slice(0, cursorPos); + + // Find the last @ before cursor + const lastAtIndex = textBeforeCursor.lastIndexOf('@'); + + if (lastAtIndex === -1) { + setShowAutocomplete(false); + return; + } + + // Check if there's whitespace or start of string before @ + const charBeforeAt = lastAtIndex > 0 ? textBeforeCursor[lastAtIndex - 1] : ' '; + if (charBeforeAt && !/\s/.test(charBeforeAt)) { + setShowAutocomplete(false); + return; + } + + // Extract query after @ + const textAfterAt = textBeforeCursor.slice(lastAtIndex + 1); + + // Check if there's whitespace after @ (which would close the mention) + if (/\s/.test(textAfterAt)) { + setShowAutocomplete(false); + return; + } + + setMentionStart(lastAtIndex); + setMentionQuery(textAfterAt); + setShowAutocomplete(true); + setSelectedIndex(0); + }, [value]); + + // Handle keyboard navigation + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!showAutocomplete || filteredAgents.length === 0) { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + onSubmit(); + } + return; + } + + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedIndex((prev) => Math.min(prev + 1, filteredAgents.length - 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedIndex((prev) => Math.max(prev - 1, 0)); + } else if (e.key === 'Enter' || e.key === 'Tab') { + e.preventDefault(); + insertMention(filteredAgents[selectedIndex]!); + } else if (e.key === 'Escape') { + e.preventDefault(); + setShowAutocomplete(false); + } else if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + onSubmit(); + } + }; + + // Insert mention at cursor position + const insertMention = (agent: DirectoryAgent) => { + const textarea = textareaRef.current; + if (!textarea) return; + + const mention = `[@${agent.name}](agent://${agent.id})`; + const newValue = + value.slice(0, mentionStart) + + mention + + ' ' + + value.slice(textarea.selectionStart); + + onChange(newValue); + setShowAutocomplete(false); + + // Move cursor after the mention + setTimeout(() => { + const newCursorPos = mentionStart + mention.length + 1; + textarea.setSelectionRange(newCursorPos, newCursorPos); + textarea.focus(); + }, 0); + }; + + return ( +
+