Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions packages/dashboard/ui/landing/LandingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function LandingPage() {
<main>
<HeroSection />
<LiveDemoSection />
<LiveCommunitySection />
<FeaturesSection />
<ProvidersSection />
<PricingSection />
Expand Down Expand Up @@ -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),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 onlineCount can become negative due to unbounded decrement

The simulated activity update for community rooms can cause onlineCount to become negative over time.

Click to expand

Issue

In LandingPage.tsx:486, the online count is updated randomly:

onlineCount: room.onlineCount + (Math.random() > 0.5 ? 1 : -1),

This randomly adds or subtracts 1 from the count every 3 seconds (when shouldUpdate is true). Since there's no lower bound check, over time the count can become negative.

Example Scenario

  • Room starts with onlineCount: 15
  • After several unlucky decrements: onlineCount: -3
  • UI displays "-3 online" which is nonsensical

Impact

  • Users see negative online counts (e.g., "-5 online") which looks like a bug
  • Damages credibility of the landing page's "live" community feature

Recommendation: Add a lower bound check: onlineCount: Math.max(0, room.onlineCount + (Math.random() > 0.5 ? 1 : -1))

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

};
}
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 (
<section id="community" className="community-section">
<div className="section-header">
<span className="section-tag">Live Community</span>
<h2>Join the Conversation</h2>
<p>Connect with developers building with AI agents. Real-time rooms, real conversations.</p>
</div>

<div className="community-grid">
{rooms.map((room) => (
<div key={room.id} className="community-card">
<div className="community-card-header">
<div className="community-card-title">
<span className="community-icon">#</span>
<h3>{room.name}</h3>
</div>
{room.isActive && (
<div className="activity-indicator">
<span className="activity-dot pulse" />
<span className="activity-text">Live</span>
</div>
)}
</div>

<p className="community-description">{room.description}</p>

<div className="community-stats">
<div className="stat-item">
<span className="stat-icon">👥</span>
<span className="stat-value">{room.memberCount.toLocaleString()}</span>
<span className="stat-label">members</span>
</div>
<div className="stat-item">
<span className="stat-icon">🟢</span>
<span className="stat-value">{room.onlineCount}</span>
<span className="stat-label">online</span>
</div>
<div className="stat-item">
<span className="stat-icon">💬</span>
<span className="stat-value">{room.messageCount.toLocaleString()}</span>
<span className="stat-label">messages</span>
</div>
</div>

<div className="community-footer">
<span className="last-activity">
Last activity: {formatLastActivity(room.lastActivityAt)}
</span>
<a href={`/app?channel=${room.id}`} className="btn-ghost btn-small">
Join Room →
</a>
</div>
</div>
))}
</div>

<div className="community-cta">
<a href="/signup" className="btn-primary btn-large">
<span>Join Community</span>
<span className="btn-arrow">→</span>
</a>
<p className="community-cta-note">Free to join. Start chatting in seconds.</p>
</div>
</section>
);
}

function FeaturesSection() {
const features = [
{
Expand Down
188 changes: 188 additions & 0 deletions packages/dashboard/ui/landing/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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
============================================ */
Expand Down
3 changes: 2 additions & 1 deletion packages/dashboard/ui/react-components/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -2472,6 +2472,7 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) {
onProjectChange={handleProjectSelect}
onCommandPaletteOpen={handleCommandPaletteOpen}
onSettingsClick={handleSettingsClick}
isReconnecting={isReconnecting}
onHistoryClick={handleHistoryClick}
onNewConversationClick={handleNewConversationClick}
onCoordinatorClick={handleCoordinatorClick}
Expand Down
Loading