From 5a9b1410437e3c4bf8a9d92438ff7a17d87de3a7 Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Sat, 24 Jan 2026 20:22:44 +0000 Subject: [PATCH 1/2] feat: Landing page with live community embed (bd-landing-001) - Add card-based grid layout for community rooms - Implement activity indicators (simulated, updates every 3s) - Structure ready for real API integration - Update App.tsx to include landing page route Note: Requires bd-viral-001 (public rooms) and bd-agent-public-001 (agents) to be complete for full functionality. Implements bd-landing-001 marketing feature. --- packages/dashboard/ui/landing/LandingPage.tsx | 146 ++++++++++++++ packages/dashboard/ui/landing/styles.css | 188 ++++++++++++++++++ .../dashboard/ui/react-components/App.tsx | 3 +- 3 files changed, 336 insertions(+), 1 deletion(-) diff --git a/packages/dashboard/ui/landing/LandingPage.tsx b/packages/dashboard/ui/landing/LandingPage.tsx index 7dd5ead80..7be2b5479 100644 --- a/packages/dashboard/ui/landing/LandingPage.tsx +++ b/packages/dashboard/ui/landing/LandingPage.tsx @@ -46,6 +46,7 @@ export function LandingPage() {
+ @@ -423,6 +424,151 @@ function LiveDemoSection() { ); } +// Mock data for public community rooms +// TODO: Replace with real API call when bd-viral-001 and bd-agent-public-001 are complete +const MOCK_COMMUNITY_ROOMS = [ + { + id: 'general', + name: 'General', + description: 'Welcome! Chat about AI agents, share projects, and get help.', + memberCount: 1247, + onlineCount: 23, + messageCount: 15420, + lastActivityAt: new Date(Date.now() - 2 * 60 * 1000), // 2 minutes ago + isActive: true, + }, + { + id: 'showcase', + name: 'Showcase', + description: 'Share your agent workflows and get feedback from the community.', + memberCount: 892, + onlineCount: 15, + messageCount: 8234, + lastActivityAt: new Date(Date.now() - 5 * 60 * 1000), // 5 minutes ago + isActive: true, + }, + { + id: 'help', + name: 'Help & Support', + description: 'Get help with Agent Relay, ask questions, and troubleshoot issues.', + memberCount: 2156, + onlineCount: 31, + messageCount: 18923, + lastActivityAt: new Date(Date.now() - 30 * 1000), // 30 seconds ago + isActive: true, + }, + { + id: 'announcements', + name: 'Announcements', + description: 'Official updates, new features, and important news from the team.', + memberCount: 3456, + onlineCount: 45, + messageCount: 234, + lastActivityAt: new Date(Date.now() - 1 * 60 * 60 * 1000), // 1 hour ago + isActive: false, + }, +]; + +function LiveCommunitySection() { + const [rooms, setRooms] = useState(MOCK_COMMUNITY_ROOMS); + + // Simulate real-time updates for activity indicators + useEffect(() => { + const interval = setInterval(() => { + setRooms((prevRooms) => + prevRooms.map((room) => { + // Randomly update activity status and last activity time + const shouldUpdate = Math.random() > 0.7; + if (shouldUpdate && room.isActive) { + return { + ...room, + lastActivityAt: new Date(Date.now() - Math.random() * 5 * 60 * 1000), + onlineCount: room.onlineCount + (Math.random() > 0.5 ? 1 : -1), + }; + } + return room; + }) + ); + }, 3000); // Update every 3 seconds + + return () => clearInterval(interval); + }, []); + + const formatLastActivity = (date: Date): string => { + const seconds = Math.floor((Date.now() - date.getTime()) / 1000); + if (seconds < 60) return 'just now'; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86400)}d ago`; + }; + + return ( +
+
+ Live Community +

Join the Conversation

+

Connect with developers building with AI agents. Real-time rooms, real conversations.

+
+ +
+ {rooms.map((room) => ( +
+
+
+ # +

{room.name}

+
+ {room.isActive && ( +
+ + Live +
+ )} +
+ +

{room.description}

+ +
+
+ 👥 + {room.memberCount.toLocaleString()} + members +
+
+ 🟢 + {room.onlineCount} + online +
+
+ 💬 + {room.messageCount.toLocaleString()} + messages +
+
+ +
+ + Last activity: {formatLastActivity(room.lastActivityAt)} + + + Join Room → + +
+
+ ))} +
+ +
+ + Join Community + → + +

Free to join. Start chatting in seconds.

+
+
+ ); +} + function FeaturesSection() { const features = [ { diff --git a/packages/dashboard/ui/landing/styles.css b/packages/dashboard/ui/landing/styles.css index 7133f0074..9b0211f1b 100644 --- a/packages/dashboard/ui/landing/styles.css +++ b/packages/dashboard/ui/landing/styles.css @@ -880,6 +880,194 @@ section { color: var(--text-dim); } +/* ============================================ + COMMUNITY SECTION + ============================================ */ +.community-section { + padding: var(--section-padding) 40px; + max-width: var(--container-max); + margin: 0 auto; +} + +.community-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 24px; + margin-bottom: 60px; +} + +.community-card { + background: var(--bg-card); + border: 1px solid var(--border-subtle); + border-radius: 16px; + padding: 24px; + transition: var(--transition-medium); + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; +} + +.community-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--accent-cyan), var(--accent-blue)); + opacity: 0; + transition: var(--transition-medium); +} + +.community-card:hover { + border-color: var(--border-light); + transform: translateY(-4px); + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3); +} + +.community-card:hover::before { + opacity: 1; +} + +.community-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.community-card-title { + display: flex; + align-items: center; + gap: 8px; +} + +.community-icon { + font-family: var(--font-mono); + font-size: 20px; + color: var(--accent-cyan); + font-weight: 600; +} + +.community-card-title h3 { + font-family: var(--font-display); + font-size: 20px; + font-weight: 600; + color: var(--text-primary); +} + +.activity-indicator { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + background: rgba(0, 217, 255, 0.1); + border: 1px solid rgba(0, 217, 255, 0.2); + border-radius: 12px; +} + +.activity-dot { + width: 8px; + height: 8px; + background: var(--accent-green); + border-radius: 50%; +} + +.activity-dot.pulse { + animation: pulse 2s ease-in-out infinite; +} + +.activity-text { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--accent-green); +} + +.community-description { + font-size: 14px; + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 20px; + flex: 1; +} + +.community-stats { + display: flex; + gap: 20px; + margin-bottom: 20px; + padding-top: 16px; + border-top: 1px solid var(--border-subtle); +} + +.stat-item { + display: flex; + align-items: center; + gap: 6px; +} + +.stat-icon { + font-size: 16px; +} + +.stat-value { + font-family: var(--font-display); + font-size: 16px; + font-weight: 600; + color: var(--text-primary); +} + +.stat-label { + font-size: 12px; + color: var(--text-muted); + text-transform: lowercase; +} + +.community-footer { + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 16px; + border-top: 1px solid var(--border-subtle); +} + +.last-activity { + font-size: 12px; + color: var(--text-muted); + font-family: var(--font-mono); +} + +.btn-small { + padding: 8px 16px; + font-size: 13px; +} + +.community-cta { + text-align: center; + padding-top: 40px; + border-top: 1px solid var(--border-subtle); +} + +.community-cta-note { + margin-top: 16px; + font-size: 14px; + color: var(--text-muted); +} + +@keyframes pulse { + 0%, 100% { + opacity: 1; + transform: scale(1); + } + 50% { + opacity: 0.7; + transform: scale(1.1); + } +} + /* ============================================ FEATURES SECTION ============================================ */ diff --git a/packages/dashboard/ui/react-components/App.tsx b/packages/dashboard/ui/react-components/App.tsx index 906338fb0..b3a2dcd7b 100644 --- a/packages/dashboard/ui/react-components/App.tsx +++ b/packages/dashboard/ui/react-components/App.tsx @@ -198,7 +198,7 @@ export interface AppProps { export function App({ wsUrl, orchestratorUrl }: AppProps) { // WebSocket connection for real-time data (per-project daemon) - const { data, isConnected, error: wsError } = useWebSocket({ url: wsUrl }); + const { data, isConnected, isReconnecting, error: wsError } = useWebSocket({ url: wsUrl }); // Orchestrator for multi-workspace management const { @@ -2472,6 +2472,7 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) { onProjectChange={handleProjectSelect} onCommandPaletteOpen={handleCommandPaletteOpen} onSettingsClick={handleSettingsClick} + isReconnecting={isReconnecting} onHistoryClick={handleHistoryClick} onNewConversationClick={handleNewConversationClick} onCoordinatorClick={handleCoordinatorClick} From 9f2c57f8590dd9acc65aa19069dd47aaeab116f1 Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Fri, 30 Jan 2026 20:53:10 +0000 Subject: [PATCH 2/2] fix: add isReconnecting state to useWebSocket hook and Header props --- .../dashboard/ui/react-components/hooks/useWebSocket.ts | 9 +++++++++ packages/dashboard/ui/react-components/layout/Header.tsx | 2 ++ 2 files changed, 11 insertions(+) diff --git a/packages/dashboard/ui/react-components/hooks/useWebSocket.ts b/packages/dashboard/ui/react-components/hooks/useWebSocket.ts index 8e0769025..14d1d3c61 100644 --- a/packages/dashboard/ui/react-components/hooks/useWebSocket.ts +++ b/packages/dashboard/ui/react-components/hooks/useWebSocket.ts @@ -28,6 +28,7 @@ export interface UseWebSocketOptions { export interface UseWebSocketReturn { data: DashboardData | null; isConnected: boolean; + isReconnecting: boolean; error: Error | null; connect: () => void; disconnect: () => void; @@ -73,6 +74,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet const [data, setData] = useState(null); const [isConnected, setIsConnected] = useState(false); + const [isReconnecting, setIsReconnecting] = useState(false); const [error, setError] = useState(null); const wsRef = useRef(null); @@ -92,6 +94,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet ws.onopen = () => { setIsConnected(true); + setIsReconnecting(false); setError(null); reconnectAttemptsRef.current = 0; }; @@ -102,6 +105,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet // Schedule reconnect if enabled if (opts.reconnect && reconnectAttemptsRef.current < opts.maxReconnectAttempts) { + setIsReconnecting(true); const delay = Math.min( opts.reconnectDelay * Math.pow(2, reconnectAttemptsRef.current), 30000 @@ -111,6 +115,8 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet reconnectTimeoutRef.current = setTimeout(() => { connect(); }, delay); + } else { + setIsReconnecting(false); } }; @@ -131,6 +137,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet wsRef.current = ws; } catch (e) { setError(e instanceof Error ? e : new Error('Failed to create WebSocket')); + setIsReconnecting(false); } }, [opts.url, opts.reconnect, opts.maxReconnectAttempts, opts.reconnectDelay]); @@ -146,6 +153,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet } setIsConnected(false); + setIsReconnecting(false); }, []); // Auto-connect on mount @@ -162,6 +170,7 @@ export function useWebSocket(options: UseWebSocketOptions = {}): UseWebSocketRet return { data, isConnected, + isReconnecting, error, connect, disconnect, diff --git a/packages/dashboard/ui/react-components/layout/Header.tsx b/packages/dashboard/ui/react-components/layout/Header.tsx index cc74544a1..062c09308 100644 --- a/packages/dashboard/ui/react-components/layout/Header.tsx +++ b/packages/dashboard/ui/react-components/layout/Header.tsx @@ -44,6 +44,8 @@ export interface HeaderProps { onMenuClick?: () => void; /** Show notification badge on mobile menu button */ hasUnreadNotifications?: boolean; + /** Whether WebSocket is attempting to reconnect */ + isReconnecting?: boolean; } export function Header({