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 { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { Reactions } from '../components/Reactions';
|
import { Reactions } from '../components/Reactions';
|
||||||
|
import { MentionAutocomplete } from '../components/MentionAutocomplete';
|
||||||
import { Thread } from './Thread';
|
import { Thread } from './Thread';
|
||||||
import type { SocialChannel, SocialPost } from '../types';
|
import type { SocialChannel, SocialPost } from '../types';
|
||||||
import type { FormEvent } from 'react';
|
import type { FormEvent } from 'react';
|
||||||
|
|
@ -43,13 +44,22 @@ function ChannelCard({
|
||||||
}
|
}
|
||||||
|
|
||||||
function PostInChannel({ post, onOpenThread }: { post: SocialPost; onOpenThread: (postId: string) => void }) {
|
function PostInChannel({ post, onOpenThread }: { post: SocialPost; onOpenThread: (postId: string) => void }) {
|
||||||
|
const profileUrl = post.authorUrlKey
|
||||||
|
? `/BARAAA/agents/${post.authorUrlKey}`
|
||||||
|
: `/BARAAA/agents/${post.authorName}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
||||||
<div className="flex items-center gap-2">
|
<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()}
|
{post.authorName.slice(0, 2).toUpperCase()}
|
||||||
</div>
|
</a>
|
||||||
<span className="font-semibold text-gray-900 text-sm">{post.authorName}</span>
|
<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>
|
<span className="text-gray-400 text-xs ml-auto">{timeAgo(post.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 text-gray-800 whitespace-pre-wrap break-words text-sm leading-relaxed">
|
<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,
|
refetchInterval: 15000,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function handleSubmit(e: FormEvent) {
|
async function submitPost() {
|
||||||
e.preventDefault();
|
|
||||||
if (!newPost.trim() || sending) return;
|
if (!newPost.trim() || sending) return;
|
||||||
|
|
||||||
setSending(true);
|
setSending(true);
|
||||||
|
|
@ -107,6 +116,11 @@ function ChannelView({ channelId }: { channelId: string }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
submitPost();
|
||||||
|
}
|
||||||
|
|
||||||
if (openThreadId) {
|
if (openThreadId) {
|
||||||
return <Thread postId={openThreadId} onBack={() => setOpenThreadId(null)} />;
|
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">
|
<form onSubmit={handleSubmit} className="p-4 border-t border-gray-200 bg-white">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<textarea
|
<MentionAutocomplete
|
||||||
value={newPost}
|
value={newPost}
|
||||||
onChange={(e) => setNewPost(e.target.value)}
|
onChange={setNewPost}
|
||||||
placeholder="Write a post..."
|
onSubmit={submitPost}
|
||||||
rows={2}
|
placeholder="Write a post... (use @ to mention agents)"
|
||||||
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"
|
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
|
rows={2}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@ import { useState } from 'react';
|
||||||
import { RoomList } from '../components/RoomList';
|
import { RoomList } from '../components/RoomList';
|
||||||
import { MessageThread } from '../components/MessageThread';
|
import { MessageThread } from '../components/MessageThread';
|
||||||
|
|
||||||
interface ChatProps {
|
export function Chat() {
|
||||||
onLogout: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Chat({ onLogout }: ChatProps) {
|
|
||||||
const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
|
const [selectedRoomId, setSelectedRoomId] = useState<string | null>(null);
|
||||||
|
|
||||||
return (
|
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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { api } from '../lib/api';
|
import { api } from '../lib/api';
|
||||||
import { Reactions } from '../components/Reactions';
|
import { Reactions } from '../components/Reactions';
|
||||||
|
import { MentionAutocomplete } from '../components/MentionAutocomplete';
|
||||||
import type { SocialPost } from '../types';
|
import type { SocialPost } from '../types';
|
||||||
|
|
||||||
function timeAgo(dateStr: string): string {
|
function timeAgo(dateStr: string): string {
|
||||||
|
|
@ -16,6 +17,10 @@ function timeAgo(dateStr: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function PostItem({ post, isParent }: { post: SocialPost; isParent?: boolean }) {
|
function PostItem({ post, isParent }: { post: SocialPost; isParent?: boolean }) {
|
||||||
|
const profileUrl = post.authorUrlKey
|
||||||
|
? `/BARAAA/agents/${post.authorUrlKey}`
|
||||||
|
: `/BARAAA/agents/${post.authorName}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className={`bg-white rounded-lg border p-4 ${
|
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="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()}
|
{post.authorName.slice(0, 2).toUpperCase()}
|
||||||
</div>
|
</a>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<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>}
|
{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>
|
<span className="text-gray-400 text-xs ml-auto">{timeAgo(post.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -68,12 +78,16 @@ export function Thread({ postId, onBack }: ThreadProps) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const submitReply = () => {
|
||||||
e.preventDefault();
|
|
||||||
if (!replyText.trim() || replyMutation.isPending) return;
|
if (!replyText.trim() || replyMutation.isPending) return;
|
||||||
replyMutation.mutate(replyText);
|
replyMutation.mutate(replyText);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
submitReply();
|
||||||
|
};
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full text-gray-500">Loading thread...</div>
|
<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">
|
<form onSubmit={handleSubmit} className="p-4 border-t border-gray-200 bg-white">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<textarea
|
<MentionAutocomplete
|
||||||
value={replyText}
|
value={replyText}
|
||||||
onChange={(e) => setReplyText(e.target.value)}
|
onChange={setReplyText}
|
||||||
placeholder="Write a reply..."
|
onSubmit={submitReply}
|
||||||
rows={2}
|
placeholder="Write a reply... (use @ to mention agents)"
|
||||||
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"
|
|
||||||
disabled={replyMutation.isPending}
|
disabled={replyMutation.isPending}
|
||||||
|
rows={2}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export interface SocialPost {
|
||||||
channelName?: string;
|
channelName?: string;
|
||||||
authorAgentId: string;
|
authorAgentId: string;
|
||||||
authorName: string;
|
authorName: string;
|
||||||
|
authorUrlKey?: string | null;
|
||||||
body: string;
|
body: string;
|
||||||
parentPostId?: string | null;
|
parentPostId?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue