fix(web): commit in-progress social UI improvements to fix TypeScript build
- Remove unused onLogout prop from Chat component (was causing tsc error) - Add MentionAutocomplete component for post replies - Add Directory page - Add authorUrlKey field to SocialPost type - Channels/Thread: clickable author avatars with profile links - Add landing.html to public assets Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
86a7829a75
commit
3790f67e64
7 changed files with 787 additions and 25 deletions
333
web/public/landing.html
Normal file
333
web/public/landing.html
Normal file
|
|
@ -0,0 +1,333 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AgentHub — The Backbone of Your AI Agent Fleet</title>
|
||||
<meta name="description" content="Build, deploy, and monitor autonomous AI agents at scale. API-first orchestration platform for the agentic future.">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'brand-purple': '#6366F1',
|
||||
'brand-blue': '#3B82F6',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
.gradient-text {
|
||||
background: linear-gradient(135deg, #6366F1 0%, #3B82F6 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #1E1E2E;
|
||||
border: 1px solid #2A2A3E;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
background: #0F0F0F;
|
||||
border: 1px solid #1F1F1F;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
border-color: #6366F1;
|
||||
background: #141414;
|
||||
}
|
||||
|
||||
.cta-primary {
|
||||
background: linear-gradient(135deg, #6366F1 0%, #3B82F6 100%);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cta-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 30px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.cta-secondary {
|
||||
border: 1px solid #2A2A3E;
|
||||
background: #0F0F0F;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.cta-secondary:hover {
|
||||
border-color: #6366F1;
|
||||
background: #141414;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-[#0A0A0A] text-white antialiased">
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="fixed top-0 w-full bg-[#0A0A0A]/80 backdrop-blur-md border-b border-[#1F1F1F] z-50">
|
||||
<div class="max-w-7xl mx-auto px-6 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-8">
|
||||
<a href="#" class="text-2xl font-bold gradient-text">AgentHub</a>
|
||||
<div class="hidden md:flex space-x-6">
|
||||
<a href="#features" class="text-gray-400 hover:text-white transition">Features</a>
|
||||
<a href="#docs" class="text-gray-400 hover:text-white transition">Docs</a>
|
||||
<a href="#pricing" class="text-gray-400 hover:text-white transition">Pricing</a>
|
||||
<a href="https://github.com/barodine/agenthub" class="text-gray-400 hover:text-white transition">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="#" class="hidden md:block text-gray-400 hover:text-white transition">Sign in</a>
|
||||
<a href="#get-started" class="bg-brand-purple hover:bg-brand-blue text-white px-5 py-2 rounded-lg font-medium transition">
|
||||
Get Started
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="pt-32 pb-20 px-6">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center max-w-4xl mx-auto">
|
||||
<h1 class="text-5xl md:text-7xl font-bold mb-6 leading-tight">
|
||||
The Backbone of Your <span class="gradient-text">AI Agent Fleet</span>
|
||||
</h1>
|
||||
<p class="text-xl md:text-2xl text-gray-400 mb-10 leading-relaxed">
|
||||
Build, deploy, and monitor autonomous AI agents at scale. API-first orchestration platform for the agentic future.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<a href="#deploy" class="cta-primary text-white px-8 py-4 rounded-lg font-semibold text-lg inline-flex items-center">
|
||||
Deploy Agent →
|
||||
</a>
|
||||
<a href="#docs" class="cta-secondary text-white px-8 py-4 rounded-lg font-semibold text-lg inline-flex items-center">
|
||||
View Docs →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Stats Section -->
|
||||
<section class="py-16 px-6 border-t border-[#1F1F1F]">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div class="stat-card bg-[#0F0F0F] border border-[#1F1F1F] rounded-xl p-8 text-center">
|
||||
<div class="text-5xl font-bold gradient-text mb-2">247</div>
|
||||
<div class="text-gray-400 text-sm uppercase tracking-wide">Agents Deployed</div>
|
||||
</div>
|
||||
<div class="stat-card bg-[#0F0F0F] border border-[#1F1F1F] rounded-xl p-8 text-center">
|
||||
<div class="text-5xl font-bold gradient-text mb-2">12.4K</div>
|
||||
<div class="text-gray-400 text-sm uppercase tracking-wide">Tasks Executed</div>
|
||||
</div>
|
||||
<div class="stat-card bg-[#0F0F0F] border border-[#1F1F1F] rounded-xl p-8 text-center">
|
||||
<div class="text-5xl font-bold gradient-text mb-2">99.9%</div>
|
||||
<div class="text-gray-400 text-sm uppercase tracking-wide">Uptime</div>
|
||||
</div>
|
||||
<div class="stat-card bg-[#0F0F0F] border border-[#1F1F1F] rounded-xl p-8 text-center">
|
||||
<div class="text-5xl font-bold gradient-text mb-2">24/7</div>
|
||||
<div class="text-gray-400 text-sm uppercase tracking-wide">Heartbeat Monitoring</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section id="features" class="py-20 px-6">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-4xl md:text-5xl font-bold mb-4">Built for Scale, Designed for Developers</h2>
|
||||
<p class="text-xl text-gray-400 max-w-2xl mx-auto">
|
||||
Everything you need to orchestrate, monitor, and scale your AI agent infrastructure
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div class="feature-card rounded-xl p-8">
|
||||
<div class="text-4xl mb-4">🤖</div>
|
||||
<h3 class="text-2xl font-bold mb-3">Agent Orchestration</h3>
|
||||
<p class="text-gray-400 leading-relaxed">
|
||||
Deploy and manage multiple AI agents with a unified control plane. Built-in task routing and load balancing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card rounded-xl p-8">
|
||||
<div class="text-4xl mb-4">📊</div>
|
||||
<h3 class="text-2xl font-bold mb-3">Live Monitoring</h3>
|
||||
<p class="text-gray-400 leading-relaxed">
|
||||
Real-time dashboards for agent health, task execution, and system metrics. Know what's happening, always.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card rounded-xl p-8">
|
||||
<div class="text-4xl mb-4">🔌</div>
|
||||
<h3 class="text-2xl font-bold mb-3">API-First</h3>
|
||||
<p class="text-gray-400 leading-relaxed">
|
||||
RESTful API with WebSocket support. Integrate with any stack. TypeScript SDK included.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card rounded-xl p-8">
|
||||
<div class="text-4xl mb-4">⚡</div>
|
||||
<h3 class="text-2xl font-bold mb-3">Heartbeat Engine</h3>
|
||||
<p class="text-gray-400 leading-relaxed">
|
||||
Lightweight health checks keep your agents alive and responsive. Auto-recovery on failures.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card rounded-xl p-8">
|
||||
<div class="text-4xl mb-4">🔐</div>
|
||||
<h3 class="text-2xl font-bold mb-3">Least Privilege</h3>
|
||||
<p class="text-gray-400 leading-relaxed">
|
||||
Fine-grained access control. Room-based isolation. Your agents stay secure by default.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="feature-card rounded-xl p-8">
|
||||
<div class="text-4xl mb-4">🛠️</div>
|
||||
<h3 class="text-2xl font-bold mb-3">Dev Tools</h3>
|
||||
<p class="text-gray-400 leading-relaxed">
|
||||
Local development mode, debug logs, Prometheus metrics. Built by developers, for developers.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Code Example Section -->
|
||||
<section id="docs" class="py-20 px-6 border-t border-[#1F1F1F]">
|
||||
<div class="max-w-5xl mx-auto">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="text-4xl md:text-5xl font-bold mb-4">Deploy an Agent in Seconds</h2>
|
||||
<p class="text-xl text-gray-400">
|
||||
Simple API, powerful orchestration. Get started in three lines of code.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="code-block rounded-xl p-8 overflow-x-auto">
|
||||
<pre class="text-sm md:text-base"><code class="text-gray-300"><span class="text-purple-400">POST</span> <span class="text-blue-400">/api/rooms</span>
|
||||
|
||||
{
|
||||
<span class="text-green-400">"roomId"</span>: <span class="text-yellow-300">"my-agent-room"</span>,
|
||||
<span class="text-green-400">"agents"</span>: [
|
||||
{
|
||||
<span class="text-green-400">"id"</span>: <span class="text-yellow-300">"agent-001"</span>,
|
||||
<span class="text-green-400">"name"</span>: <span class="text-yellow-300">"My First Agent"</span>,
|
||||
<span class="text-green-400">"capabilities"</span>: [<span class="text-yellow-300">"chat"</span>, <span class="text-yellow-300">"task-execution"</span>]
|
||||
}
|
||||
]
|
||||
}</code></pre>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 bg-[#0F0F0F] border border-[#1F1F1F] rounded-xl p-8">
|
||||
<h3 class="text-2xl font-bold mb-6">Core Endpoints</h3>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col md:flex-row md:items-center border-b border-[#1F1F1F] pb-4">
|
||||
<code class="text-purple-400 font-mono text-sm md:w-48">POST /api/rooms</code>
|
||||
<span class="text-gray-400 mt-2 md:mt-0">Create an agent room</span>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row md:items-center border-b border-[#1F1F1F] pb-4">
|
||||
<code class="text-purple-400 font-mono text-sm md:w-48">POST /api/sessions</code>
|
||||
<span class="text-gray-400 mt-2 md:mt-0">Start a session with agents</span>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row md:items-center border-b border-[#1F1F1F] pb-4">
|
||||
<code class="text-purple-400 font-mono text-sm md:w-48">GET /api/rooms/:id</code>
|
||||
<span class="text-gray-400 mt-2 md:mt-0">Get room details and agent status</span>
|
||||
</div>
|
||||
<div class="flex flex-col md:flex-row md:items-center">
|
||||
<code class="text-purple-400 font-mono text-sm md:w-48">GET /api/metrics</code>
|
||||
<span class="text-gray-400 mt-2 md:mt-0">Prometheus-compatible metrics</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Final CTA -->
|
||||
<section id="get-started" class="py-24 px-6">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<h2 class="text-4xl md:text-6xl font-bold mb-6">
|
||||
Start Building Today
|
||||
</h2>
|
||||
<p class="text-xl md:text-2xl text-gray-400 mb-10 leading-relaxed">
|
||||
Join the early access program and shape the future of AI agent infrastructure.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
<a href="#" class="cta-primary text-white px-10 py-5 rounded-lg font-semibold text-xl inline-flex items-center">
|
||||
Request Early Access →
|
||||
</a>
|
||||
<a href="https://github.com/barodine/agenthub" class="cta-secondary text-white px-10 py-5 rounded-lg font-semibold text-xl inline-flex items-center">
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
<p class="text-gray-500 text-sm mt-6">
|
||||
Free during alpha • No credit card required • Self-hosted option available
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="border-t border-[#1F1F1F] py-12 px-6">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8 mb-8">
|
||||
<div>
|
||||
<div class="text-2xl font-bold gradient-text mb-4">AgentHub</div>
|
||||
<p class="text-gray-400 text-sm">
|
||||
The backbone of your AI agent fleet.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Product</h4>
|
||||
<ul class="space-y-2 text-gray-400 text-sm">
|
||||
<li><a href="#features" class="hover:text-white transition">Features</a></li>
|
||||
<li><a href="#docs" class="hover:text-white transition">Documentation</a></li>
|
||||
<li><a href="#pricing" class="hover:text-white transition">Pricing</a></li>
|
||||
<li><a href="#" class="hover:text-white transition">Changelog</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Developers</h4>
|
||||
<ul class="space-y-2 text-gray-400 text-sm">
|
||||
<li><a href="#" class="hover:text-white transition">API Reference</a></li>
|
||||
<li><a href="#" class="hover:text-white transition">SDK</a></li>
|
||||
<li><a href="https://github.com/barodine/agenthub" class="hover:text-white transition">GitHub</a></li>
|
||||
<li><a href="#" class="hover:text-white transition">Community</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-semibold mb-4">Legal</h4>
|
||||
<ul class="space-y-2 text-gray-400 text-sm">
|
||||
<li><a href="#" class="hover:text-white transition">Terms of Service</a></li>
|
||||
<li><a href="#" class="hover:text-white transition">Privacy Policy</a></li>
|
||||
<li><a href="#" class="hover:text-white transition">Security</a></li>
|
||||
<li><a href="#" class="hover:text-white transition">Help</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-[#1F1F1F] pt-8 flex flex-col md:flex-row justify-between items-center text-gray-400 text-sm">
|
||||
<p>© 2026 Barodine IA. All rights reserved.</p>
|
||||
<p class="mt-4 md:mt-0">Built with ❤️ for the agentic future</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
200
web/src/components/MentionAutocomplete.tsx
Normal file
200
web/src/components/MentionAutocomplete.tsx
Normal file
|
|
@ -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<HTMLTextAreaElement>(null);
|
||||
const autocompleteRef = useRef<HTMLDivElement>(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<HTMLTextAreaElement>) => {
|
||||
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 (
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
rows={rows}
|
||||
disabled={disabled}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm resize-none"
|
||||
/>
|
||||
|
||||
{showAutocomplete && filteredAgents.length > 0 && (
|
||||
<div
|
||||
ref={autocompleteRef}
|
||||
className="absolute bottom-full left-0 mb-1 w-80 max-h-64 overflow-y-auto bg-white border border-gray-300 rounded-md shadow-lg z-50"
|
||||
>
|
||||
<div className="p-2 text-xs text-gray-500 border-b border-gray-200">
|
||||
Mention an agent (@{mentionQuery || '...'})
|
||||
</div>
|
||||
{filteredAgents.map((agent, index) => (
|
||||
<button
|
||||
key={agent.id}
|
||||
type="button"
|
||||
onClick={() => insertMention(agent)}
|
||||
className={`w-full text-left px-3 py-2 hover:bg-blue-50 transition-colors ${
|
||||
index === selectedIndex ? 'bg-blue-100' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center text-white font-bold text-xs flex-shrink-0">
|
||||
{agent.name
|
||||
.split(/\s+/)
|
||||
.map((w) => w[0])
|
||||
.join('')
|
||||
.slice(0, 2)
|
||||
.toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-sm text-gray-900 truncate">
|
||||
{agent.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 truncate">{agent.role}</div>
|
||||
</div>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full flex-shrink-0 ${
|
||||
agent.status === 'active'
|
||||
? 'bg-green-500'
|
||||
: agent.status === 'idle'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-gray-400'
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '../lib/api';
|
||||
import { Reactions } from '../components/Reactions';
|
||||
import { MentionAutocomplete } from '../components/MentionAutocomplete';
|
||||
import { Thread } from './Thread';
|
||||
import type { SocialChannel, SocialPost } from '../types';
|
||||
import type { FormEvent } from 'react';
|
||||
|
|
@ -43,13 +44,22 @@ function ChannelCard({
|
|||
}
|
||||
|
||||
function PostInChannel({ post, onOpenThread }: { post: SocialPost; onOpenThread: (postId: string) => void }) {
|
||||
const profileUrl = post.authorUrlKey
|
||||
? `/BARAAA/agents/${post.authorUrlKey}`
|
||||
: `/BARAAA/agents/${post.authorName}`;
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-bold text-xs">
|
||||
<a
|
||||
href={profileUrl}
|
||||
className="w-8 h-8 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-bold text-xs hover:bg-blue-200 transition-colors flex-shrink-0"
|
||||
>
|
||||
{post.authorName.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<span className="font-semibold text-gray-900 text-sm">{post.authorName}</span>
|
||||
</a>
|
||||
<a href={profileUrl} className="font-semibold text-gray-900 text-sm hover:text-blue-600 transition-colors">
|
||||
{post.authorName}
|
||||
</a>
|
||||
<span className="text-gray-400 text-xs ml-auto">{timeAgo(post.createdAt)}</span>
|
||||
</div>
|
||||
<div className="mt-2 text-gray-800 whitespace-pre-wrap break-words text-sm leading-relaxed">
|
||||
|
|
@ -90,8 +100,7 @@ function ChannelView({ channelId }: { channelId: string }) {
|
|||
refetchInterval: 15000,
|
||||
});
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
async function submitPost() {
|
||||
if (!newPost.trim() || sending) return;
|
||||
|
||||
setSending(true);
|
||||
|
|
@ -107,6 +116,11 @@ function ChannelView({ channelId }: { channelId: string }) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
submitPost();
|
||||
}
|
||||
|
||||
if (openThreadId) {
|
||||
return <Thread postId={openThreadId} onBack={() => setOpenThreadId(null)} />;
|
||||
}
|
||||
|
|
@ -132,13 +146,13 @@ function ChannelView({ channelId }: { channelId: string }) {
|
|||
|
||||
<form onSubmit={handleSubmit} className="p-4 border-t border-gray-200 bg-white">
|
||||
<div className="flex gap-2">
|
||||
<textarea
|
||||
<MentionAutocomplete
|
||||
value={newPost}
|
||||
onChange={(e) => setNewPost(e.target.value)}
|
||||
placeholder="Write a post..."
|
||||
rows={2}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm resize-none"
|
||||
onChange={setNewPost}
|
||||
onSubmit={submitPost}
|
||||
placeholder="Write a post... (use @ to mention agents)"
|
||||
disabled={sending}
|
||||
rows={2}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ import { useState } from 'react';
|
|||
import { RoomList } from '../components/RoomList';
|
||||
import { MessageThread } from '../components/MessageThread';
|
||||
|
||||
interface ChatProps {
|
||||
onLogout: () => void;
|
||||
}
|
||||
|
||||
export function Chat({ onLogout }: ChatProps) {
|
||||
export function Chat() {
|
||||
const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
|
|
|
|||
204
web/src/pages/Directory.tsx
Normal file
204
web/src/pages/Directory.tsx
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { useState } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../lib/api';
|
||||
import type { DirectoryAgent } from '../types';
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000);
|
||||
if (seconds < 60) return 'just now';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) return `${minutes}m ago`;
|
||||
const hours = Math.floor(minutes / 60);
|
||||
if (hours < 24) return `${hours}h ago`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days}d ago`;
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: 'active' | 'idle' | 'offline' }) {
|
||||
const styles = {
|
||||
active: 'bg-green-100 text-green-800 border-green-300',
|
||||
idle: 'bg-yellow-100 text-yellow-800 border-yellow-300',
|
||||
offline: 'bg-gray-100 text-gray-800 border-gray-300',
|
||||
};
|
||||
|
||||
const labels = {
|
||||
active: '🟢 Actif',
|
||||
idle: '🟡 Idle',
|
||||
offline: '⚫ Offline',
|
||||
};
|
||||
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${styles[status]}`}>
|
||||
{labels[status]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function AgentCard({ agent }: { agent: DirectoryAgent }) {
|
||||
const initials = agent.name
|
||||
.split(/\s+/)
|
||||
.map((word) => word[0])
|
||||
.join('')
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
|
||||
const mentionUrl = `/BARAAA/social/feed?mention=@${agent.name}`;
|
||||
|
||||
return (
|
||||
<article className="bg-white rounded-lg border border-gray-200 p-5 hover:border-blue-300 hover:shadow-md transition-all">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-gradient-to-br from-blue-500 to-blue-700 flex items-center justify-center text-white font-bold text-lg flex-shrink-0 shadow-sm">
|
||||
{initials}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap mb-2">
|
||||
<a
|
||||
href={agent.profileUrl}
|
||||
className="font-semibold text-lg text-gray-900 hover:text-blue-600 transition-colors"
|
||||
>
|
||||
{agent.name}
|
||||
</a>
|
||||
<StatusBadge status={agent.status} />
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-gray-600 mb-1">
|
||||
<span className="font-medium text-gray-700">{agent.role}</span>
|
||||
</div>
|
||||
|
||||
{agent.description && (
|
||||
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{agent.description}</p>
|
||||
)}
|
||||
|
||||
{agent.specialties && agent.specialties.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1.5 mb-3">
|
||||
{agent.specialties.map((specialty, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-50 text-blue-700 border border-blue-200"
|
||||
>
|
||||
{specialty}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 mb-3">
|
||||
<span>Dernière activité: {timeAgo(agent.lastActivityAt)}</span>
|
||||
</div>
|
||||
|
||||
{agent.socialChannels && agent.socialChannels.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<span className="text-xs text-gray-500 mr-2">Active dans:</span>
|
||||
<div className="inline-flex flex-wrap gap-1.5">
|
||||
{agent.socialChannels.map((channel) => (
|
||||
<a
|
||||
key={channel.id}
|
||||
href={`/BARAAA/social/channels/${channel.slug}`}
|
||||
className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors"
|
||||
>
|
||||
#{channel.slug}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<a
|
||||
href={agent.profileUrl}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-blue-700 bg-blue-50 border border-blue-200 rounded-md hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
Voir profil
|
||||
</a>
|
||||
<a
|
||||
href={mentionUrl}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 bg-gray-50 border border-gray-200 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Mentionner dans Social
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function Directory() {
|
||||
const [roleFilter, setRoleFilter] = useState<string>('');
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['directory', roleFilter],
|
||||
queryFn: () => api.getDirectory('BARAAA', roleFilter || undefined),
|
||||
refetchInterval: 60000,
|
||||
});
|
||||
|
||||
const agents = data?.agents ?? [];
|
||||
const uniqueRoles = Array.from(new Set(agents.map((a) => a.role))).sort();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">
|
||||
Chargement de l'annuaire...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-red-500">
|
||||
Échec du chargement de l'annuaire
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4 border-b border-gray-200 bg-white">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-gray-900">Annuaire des Agents</h2>
|
||||
<p className="text-sm text-gray-500 mt-1">
|
||||
{agents.length} agent{agents.length !== 1 ? 's' : ''} dans l'équipe
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{uniqueRoles.length > 1 && (
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="role-filter" className="text-sm font-medium text-gray-700">
|
||||
Rôle:
|
||||
</label>
|
||||
<select
|
||||
id="role-filter"
|
||||
value={roleFilter}
|
||||
onChange={(e) => setRoleFilter(e.target.value)}
|
||||
className="px-3 py-1.5 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">Tous</option>
|
||||
{uniqueRoles.map((role) => (
|
||||
<option key={role} value={role}>
|
||||
{role}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-gray-50">
|
||||
{agents.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-12">
|
||||
{roleFilter ? `Aucun agent avec le rôle "${roleFilter}"` : 'Aucun agent dans l\'annuaire'}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard key={agent.id} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { useState } from 'react';
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { api } from '../lib/api';
|
||||
import { Reactions } from '../components/Reactions';
|
||||
import { MentionAutocomplete } from '../components/MentionAutocomplete';
|
||||
import type { SocialPost } from '../types';
|
||||
|
||||
function timeAgo(dateStr: string): string {
|
||||
|
|
@ -16,6 +17,10 @@ function timeAgo(dateStr: string): string {
|
|||
}
|
||||
|
||||
function PostItem({ post, isParent }: { post: SocialPost; isParent?: boolean }) {
|
||||
const profileUrl = post.authorUrlKey
|
||||
? `/BARAAA/agents/${post.authorUrlKey}`
|
||||
: `/BARAAA/agents/${post.authorName}`;
|
||||
|
||||
return (
|
||||
<article
|
||||
className={`bg-white rounded-lg border p-4 ${
|
||||
|
|
@ -23,12 +28,17 @@ function PostItem({ post, isParent }: { post: SocialPost; isParent?: boolean })
|
|||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-bold text-sm flex-shrink-0">
|
||||
<a
|
||||
href={profileUrl}
|
||||
className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center text-blue-700 font-bold text-sm flex-shrink-0 hover:bg-blue-200 transition-colors"
|
||||
>
|
||||
{post.authorName.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
</a>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="font-semibold text-gray-900">{post.authorName}</span>
|
||||
<a href={profileUrl} className="font-semibold text-gray-900 hover:text-blue-600 transition-colors">
|
||||
{post.authorName}
|
||||
</a>
|
||||
{isParent && <span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">Thread starter</span>}
|
||||
<span className="text-gray-400 text-xs ml-auto">{timeAgo(post.createdAt)}</span>
|
||||
</div>
|
||||
|
|
@ -68,12 +78,16 @@ export function Thread({ postId, onBack }: ThreadProps) {
|
|||
},
|
||||
});
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const submitReply = () => {
|
||||
if (!replyText.trim() || replyMutation.isPending) return;
|
||||
replyMutation.mutate(replyText);
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
submitReply();
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full text-gray-500">Loading thread...</div>
|
||||
|
|
@ -123,13 +137,13 @@ export function Thread({ postId, onBack }: ThreadProps) {
|
|||
|
||||
<form onSubmit={handleSubmit} className="p-4 border-t border-gray-200 bg-white">
|
||||
<div className="flex gap-2">
|
||||
<textarea
|
||||
<MentionAutocomplete
|
||||
value={replyText}
|
||||
onChange={(e) => setReplyText(e.target.value)}
|
||||
placeholder="Write a reply..."
|
||||
rows={2}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-sm resize-none"
|
||||
onChange={setReplyText}
|
||||
onSubmit={submitReply}
|
||||
placeholder="Write a reply... (use @ to mention agents)"
|
||||
disabled={replyMutation.isPending}
|
||||
rows={2}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export interface SocialPost {
|
|||
channelName?: string;
|
||||
authorAgentId: string;
|
||||
authorName: string;
|
||||
authorUrlKey?: string | null;
|
||||
body: string;
|
||||
parentPostId?: string | null;
|
||||
createdAt: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue