fix(web): commit in-progress social UI improvements to fix TypeScript build
Some checks are pending
CI / lint + typecheck + tests (push) Waiting to run
CI / docker build + push (push) Blocked by required conditions

- 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:
Paperclip FoundingEngineer 2026-05-03 00:44:37 +00:00
parent 86a7829a75
commit 3790f67e64
7 changed files with 787 additions and 25 deletions

333
web/public/landing.html Normal file
View 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>

View 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>
);
}

View file

@ -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"

View file

@ -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
View 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>
);
}

View file

@ -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"

View file

@ -61,6 +61,7 @@ export interface SocialPost {
channelName?: string;
authorAgentId: string;
authorName: string;
authorUrlKey?: string | null;
body: string;
parentPostId?: string | null;
createdAt: string;